Merge branch 'master' into release
This commit is contained in:
		
						commit
						bb455d7788
					
				| 
						 | 
					@ -100,8 +100,7 @@ MEMCACHED_SERVERS=127.0.0.1:11211:100
 | 
				
			||||||
REDIS_SERVERS=127.0.0.1:6379:0
 | 
					REDIS_SERVERS=127.0.0.1:6379:0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Queue driver to use
 | 
					# Queue driver to use
 | 
				
			||||||
# Queue not really currently used but may be configurable in the future.
 | 
					# Can be 'sync', 'database' or 'redis'
 | 
				
			||||||
# Would advise not to change this for now.
 | 
					 | 
				
			||||||
QUEUE_CONNECTION=sync
 | 
					QUEUE_CONNECTION=sync
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Storage system to use
 | 
					# Storage system to use
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -126,7 +126,7 @@ Zenahr Barzani (Zenahr) :: German; Japanese; Dutch; German Informal
 | 
				
			||||||
tatsuya.info :: Japanese
 | 
					tatsuya.info :: Japanese
 | 
				
			||||||
fadiapp :: Arabic
 | 
					fadiapp :: Arabic
 | 
				
			||||||
Jakub Bouček (jakubboucek) :: Czech
 | 
					Jakub Bouček (jakubboucek) :: Czech
 | 
				
			||||||
Marco (cdrfun) :: German
 | 
					Marco (cdrfun) :: German; German Informal
 | 
				
			||||||
10935336 :: Chinese Simplified
 | 
					10935336 :: Chinese Simplified
 | 
				
			||||||
孟繁阳 (FanyangMeng) :: Chinese Simplified
 | 
					孟繁阳 (FanyangMeng) :: Chinese Simplified
 | 
				
			||||||
Andrej Močan (andrejm) :: Slovenian
 | 
					Andrej Močan (andrejm) :: Slovenian
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,115 @@
 | 
				
			||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace BookStack\Actions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use BookStack\Auth\Permissions\PermissionService;
 | 
				
			||||||
 | 
					use BookStack\Entities\Models\Entity;
 | 
				
			||||||
 | 
					use BookStack\Interfaces\Loggable;
 | 
				
			||||||
 | 
					use Illuminate\Database\Eloquent\Builder;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Log;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ActivityLogger
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    protected $permissionService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function __construct(PermissionService $permissionService)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->permissionService = $permissionService;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Add a generic activity event to the database.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param string|Loggable $detail
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function add(string $type, $detail = '')
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $detailToStore = ($detail instanceof Loggable) ? $detail->logDescriptor() : $detail;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $activity = $this->newActivityForUser($type);
 | 
				
			||||||
 | 
					        $activity->detail = $detailToStore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($detail instanceof Entity) {
 | 
				
			||||||
 | 
					            $activity->entity_id = $detail->id;
 | 
				
			||||||
 | 
					            $activity->entity_type = $detail->getMorphClass();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $activity->save();
 | 
				
			||||||
 | 
					        $this->setNotification($type);
 | 
				
			||||||
 | 
					        $this->dispatchWebhooks($type, $detail);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Get a new activity instance for the current user.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected function newActivityForUser(string $type): Activity
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $ip = request()->ip() ?? '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return (new Activity())->forceFill([
 | 
				
			||||||
 | 
					            'type'     => strtolower($type),
 | 
				
			||||||
 | 
					            'user_id'  => user()->id,
 | 
				
			||||||
 | 
					            'ip'       => config('app.env') === 'demo' ? '127.0.0.1' : $ip,
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Removes the entity attachment from each of its activities
 | 
				
			||||||
 | 
					     * and instead uses the 'extra' field with the entities name.
 | 
				
			||||||
 | 
					     * Used when an entity is deleted.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function removeEntity(Entity $entity)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $entity->activity()->update([
 | 
				
			||||||
 | 
					            'detail'       => $entity->name,
 | 
				
			||||||
 | 
					            'entity_id'    => null,
 | 
				
			||||||
 | 
					            'entity_type'  => null,
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Flashes a notification message to the session if an appropriate message is available.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected function setNotification(string $type): void
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $notificationTextKey = 'activities.' . $type . '_notification';
 | 
				
			||||||
 | 
					        if (trans()->has($notificationTextKey)) {
 | 
				
			||||||
 | 
					            $message = trans($notificationTextKey);
 | 
				
			||||||
 | 
					            session()->flash('success', $message);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param string|Loggable $detail
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected function dispatchWebhooks(string $type, $detail): void
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $webhooks = Webhook::query()
 | 
				
			||||||
 | 
					            ->whereHas('trackedEvents', function (Builder $query) use ($type) {
 | 
				
			||||||
 | 
					                $query->where('event', '=', $type)
 | 
				
			||||||
 | 
					                    ->orWhere('event', '=', 'all');
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            ->where('active', '=', true)
 | 
				
			||||||
 | 
					            ->get();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        foreach ($webhooks as $webhook) {
 | 
				
			||||||
 | 
					            dispatch(new DispatchWebhookJob($webhook, $type, $detail));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 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);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -8,84 +8,25 @@ use BookStack\Entities\Models\Book;
 | 
				
			||||||
use BookStack\Entities\Models\Chapter;
 | 
					use BookStack\Entities\Models\Chapter;
 | 
				
			||||||
use BookStack\Entities\Models\Entity;
 | 
					use BookStack\Entities\Models\Entity;
 | 
				
			||||||
use BookStack\Entities\Models\Page;
 | 
					use BookStack\Entities\Models\Page;
 | 
				
			||||||
use BookStack\Interfaces\Loggable;
 | 
					 | 
				
			||||||
use Illuminate\Database\Eloquent\Builder;
 | 
					use Illuminate\Database\Eloquent\Builder;
 | 
				
			||||||
use Illuminate\Database\Eloquent\Relations\Relation;
 | 
					use Illuminate\Database\Eloquent\Relations\Relation;
 | 
				
			||||||
use Illuminate\Support\Facades\Log;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ActivityService
 | 
					class ActivityQueries
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    protected $activity;
 | 
					 | 
				
			||||||
    protected $permissionService;
 | 
					    protected $permissionService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function __construct(Activity $activity, PermissionService $permissionService)
 | 
					    public function __construct(PermissionService $permissionService)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->activity = $activity;
 | 
					 | 
				
			||||||
        $this->permissionService = $permissionService;
 | 
					        $this->permissionService = $permissionService;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Add activity data to database for an entity.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function addForEntity(Entity $entity, string $type)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $activity = $this->newActivityForUser($type);
 | 
					 | 
				
			||||||
        $entity->activity()->save($activity);
 | 
					 | 
				
			||||||
        $this->setNotification($type);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Add a generic activity event to the database.
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param string|Loggable $detail
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function add(string $type, $detail = '')
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if ($detail instanceof Loggable) {
 | 
					 | 
				
			||||||
            $detail = $detail->logDescriptor();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $activity = $this->newActivityForUser($type);
 | 
					 | 
				
			||||||
        $activity->detail = $detail;
 | 
					 | 
				
			||||||
        $activity->save();
 | 
					 | 
				
			||||||
        $this->setNotification($type);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Get a new activity instance for the current user.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    protected function newActivityForUser(string $type): Activity
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $ip = request()->ip() ?? '';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return $this->activity->newInstance()->forceFill([
 | 
					 | 
				
			||||||
            'type'     => strtolower($type),
 | 
					 | 
				
			||||||
            'user_id'  => user()->id,
 | 
					 | 
				
			||||||
            'ip'       => config('app.env') === 'demo' ? '127.0.0.1' : $ip,
 | 
					 | 
				
			||||||
        ]);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Removes the entity attachment from each of its activities
 | 
					 | 
				
			||||||
     * and instead uses the 'extra' field with the entities name.
 | 
					 | 
				
			||||||
     * Used when an entity is deleted.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function removeEntity(Entity $entity)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $entity->activity()->update([
 | 
					 | 
				
			||||||
            'detail'       => $entity->name,
 | 
					 | 
				
			||||||
            'entity_id'    => null,
 | 
					 | 
				
			||||||
            'entity_type'  => null,
 | 
					 | 
				
			||||||
        ]);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Gets the latest activity.
 | 
					     * Gets the latest activity.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function latest(int $count = 20, int $page = 0): array
 | 
					    public function latest(int $count = 20, int $page = 0): array
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $activityList = $this->permissionService
 | 
					        $activityList = $this->permissionService
 | 
				
			||||||
            ->filterRestrictedEntityRelations($this->activity->newQuery(), 'activities', 'entity_id', 'entity_type')
 | 
					            ->filterRestrictedEntityRelations(Activity::query(), '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)
 | 
				
			||||||
| 
						 | 
					@ -111,7 +52,7 @@ class ActivityService
 | 
				
			||||||
            $queryIds[(new Page())->getMorphClass()] = $entity->pages()->scopes('visible')->pluck('id');
 | 
					            $queryIds[(new Page())->getMorphClass()] = $entity->pages()->scopes('visible')->pluck('id');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $query = $this->activity->newQuery();
 | 
					        $query = Activity::query();
 | 
				
			||||||
        $query->where(function (Builder $query) use ($queryIds) {
 | 
					        $query->where(function (Builder $query) use ($queryIds) {
 | 
				
			||||||
            foreach ($queryIds as $morphClass => $idArr) {
 | 
					            foreach ($queryIds as $morphClass => $idArr) {
 | 
				
			||||||
                $query->orWhere(function (Builder $innerQuery) use ($morphClass, $idArr) {
 | 
					                $query->orWhere(function (Builder $innerQuery) use ($morphClass, $idArr) {
 | 
				
			||||||
| 
						 | 
					@ -138,7 +79,7 @@ class ActivityService
 | 
				
			||||||
    public function userActivity(User $user, int $count = 20, int $page = 0): array
 | 
					    public function userActivity(User $user, int $count = 20, int $page = 0): array
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $activityList = $this->permissionService
 | 
					        $activityList = $this->permissionService
 | 
				
			||||||
            ->filterRestrictedEntityRelations($this->activity->newQuery(), 'activities', 'entity_id', 'entity_type')
 | 
					            ->filterRestrictedEntityRelations(Activity::query(), 'activities', 'entity_id', 'entity_type')
 | 
				
			||||||
            ->orderBy('created_at', 'desc')
 | 
					            ->orderBy('created_at', 'desc')
 | 
				
			||||||
            ->where('user_id', '=', $user->id)
 | 
					            ->where('user_id', '=', $user->id)
 | 
				
			||||||
            ->skip($count * $page)
 | 
					            ->skip($count * $page)
 | 
				
			||||||
| 
						 | 
					@ -152,8 +93,6 @@ class ActivityService
 | 
				
			||||||
     * Filters out similar activity.
 | 
					     * Filters out similar activity.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param Activity[] $activities
 | 
					     * @param Activity[] $activities
 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @return array
 | 
					 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    protected function filterSimilar(iterable $activities): array
 | 
					    protected function filterSimilar(iterable $activities): array
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
| 
						 | 
					@ -170,32 +109,4 @@ class ActivityService
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return $newActivity;
 | 
					        return $newActivity;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Flashes a notification message to the session if an appropriate message is available.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    protected function setNotification(string $type)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $notificationTextKey = 'activities.' . $type . '_notification';
 | 
					 | 
				
			||||||
        if (trans()->has($notificationTextKey)) {
 | 
					 | 
				
			||||||
            $message = trans($notificationTextKey);
 | 
					 | 
				
			||||||
            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);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -53,4 +53,16 @@ class ActivityType
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const MFA_SETUP_METHOD = 'mfa_setup_method';
 | 
					    const MFA_SETUP_METHOD = 'mfa_setup_method';
 | 
				
			||||||
    const MFA_REMOVE_METHOD = 'mfa_remove_method';
 | 
					    const MFA_REMOVE_METHOD = 'mfa_remove_method';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const WEBHOOK_CREATE = 'webhook_create';
 | 
				
			||||||
 | 
					    const WEBHOOK_UPDATE = 'webhook_update';
 | 
				
			||||||
 | 
					    const WEBHOOK_DELETE = 'webhook_delete';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Get all the possible values.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static function all(): array
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return (new \ReflectionClass(static::class))->getConstants();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -45,7 +45,7 @@ class CommentRepo
 | 
				
			||||||
        $comment->parent_id = $parent_id;
 | 
					        $comment->parent_id = $parent_id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $entity->comments()->save($comment);
 | 
					        $entity->comments()->save($comment);
 | 
				
			||||||
        ActivityService::addForEntity($entity, ActivityType::COMMENTED_ON);
 | 
					        ActivityService::add(ActivityType::COMMENTED_ON, $entity);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return $comment;
 | 
					        return $comment;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,112 @@
 | 
				
			||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace BookStack\Actions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use BookStack\Auth\User;
 | 
				
			||||||
 | 
					use BookStack\Entities\Models\Entity;
 | 
				
			||||||
 | 
					use BookStack\Interfaces\Loggable;
 | 
				
			||||||
 | 
					use BookStack\Model;
 | 
				
			||||||
 | 
					use Illuminate\Bus\Queueable;
 | 
				
			||||||
 | 
					use Illuminate\Contracts\Queue\ShouldQueue;
 | 
				
			||||||
 | 
					use Illuminate\Foundation\Bus\Dispatchable;
 | 
				
			||||||
 | 
					use Illuminate\Queue\InteractsWithQueue;
 | 
				
			||||||
 | 
					use Illuminate\Queue\SerializesModels;
 | 
				
			||||||
 | 
					use Illuminate\Support\Carbon;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Http;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Log;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DispatchWebhookJob implements ShouldQueue
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    use Dispatchable;
 | 
				
			||||||
 | 
					    use InteractsWithQueue;
 | 
				
			||||||
 | 
					    use Queueable;
 | 
				
			||||||
 | 
					    use SerializesModels;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @var Webhook
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected $webhook;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @var string
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected $event;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @var string|Loggable
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected $detail;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @var User
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected $initiator;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @var int
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected $initiatedTime;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Create a new job instance.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return void
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function __construct(Webhook $webhook, string $event, $detail)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->webhook = $webhook;
 | 
				
			||||||
 | 
					        $this->event = $event;
 | 
				
			||||||
 | 
					        $this->detail = $detail;
 | 
				
			||||||
 | 
					        $this->initiator = user();
 | 
				
			||||||
 | 
					        $this->initiatedTime = time();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Execute the job.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return void
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function handle()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $response = Http::asJson()
 | 
				
			||||||
 | 
					            ->withOptions(['allow_redirects' => ['strict' => true]])
 | 
				
			||||||
 | 
					            ->timeout(3)
 | 
				
			||||||
 | 
					            ->post($this->webhook->endpoint, $this->buildWebhookData());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($response->failed()) {
 | 
				
			||||||
 | 
					            Log::error("Webhook call to endpoint {$this->webhook->endpoint} failed with status {$response->status()}");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected function buildWebhookData(): array
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $textParts = [
 | 
				
			||||||
 | 
					            $this->initiator->name,
 | 
				
			||||||
 | 
					            trans('activities.' . $this->event),
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($this->detail instanceof Entity) {
 | 
				
			||||||
 | 
					            $textParts[] = '"' . $this->detail->name . '"';
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $data = [
 | 
				
			||||||
 | 
					            'event'                    => $this->event,
 | 
				
			||||||
 | 
					            'text'                     => implode(' ', $textParts),
 | 
				
			||||||
 | 
					            'triggered_at'             => Carbon::createFromTimestampUTC($this->initiatedTime)->toISOString(),
 | 
				
			||||||
 | 
					            'triggered_by'             => $this->initiator->attributesToArray(),
 | 
				
			||||||
 | 
					            'triggered_by_profile_url' => $this->initiator->getProfileUrl(),
 | 
				
			||||||
 | 
					            'webhook_id'               => $this->webhook->id,
 | 
				
			||||||
 | 
					            'webhook_name'             => $this->webhook->name,
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (method_exists($this->detail, 'getUrl')) {
 | 
				
			||||||
 | 
					            $data['url'] = $this->detail->getUrl();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($this->detail instanceof Model) {
 | 
				
			||||||
 | 
					            $data['related_item'] = $this->detail->attributesToArray();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $data;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,75 @@
 | 
				
			||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace BookStack\Actions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use BookStack\Interfaces\Loggable;
 | 
				
			||||||
 | 
					use Illuminate\Database\Eloquent\Collection;
 | 
				
			||||||
 | 
					use Illuminate\Database\Eloquent\Factories\HasFactory;
 | 
				
			||||||
 | 
					use Illuminate\Database\Eloquent\Model;
 | 
				
			||||||
 | 
					use Illuminate\Database\Eloquent\Relations\HasMany;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @property int        $id
 | 
				
			||||||
 | 
					 * @property string     $name
 | 
				
			||||||
 | 
					 * @property string     $endpoint
 | 
				
			||||||
 | 
					 * @property Collection $trackedEvents
 | 
				
			||||||
 | 
					 * @property bool       $active
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class Webhook extends Model implements Loggable
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    protected $fillable = ['name', 'endpoint'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    use HasFactory;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Define the tracked event relation a webhook.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function trackedEvents(): HasMany
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return $this->hasMany(WebhookTrackedEvent::class);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Update the tracked events for a webhook from the given list of event types.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function updateTrackedEvents(array $events): void
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->trackedEvents()->delete();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $eventsToStore = array_intersect($events, array_values(ActivityType::all()));
 | 
				
			||||||
 | 
					        if (in_array('all', $events)) {
 | 
				
			||||||
 | 
					            $eventsToStore = ['all'];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $trackedEvents = [];
 | 
				
			||||||
 | 
					        foreach ($eventsToStore as $event) {
 | 
				
			||||||
 | 
					            $trackedEvents[] = new WebhookTrackedEvent(['event' => $event]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->trackedEvents()->saveMany($trackedEvents);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Check if this webhook tracks the given event.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function tracksEvent(string $event): bool
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return $this->trackedEvents->pluck('event')->contains($event);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Get a URL for this webhook within the settings interface.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function getUrl(string $path = ''): string
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return url('/settings/webhooks/' . $this->id . '/' . ltrim($path, '/'));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Get the string descriptor for this item.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function logDescriptor(): string
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return "({$this->id}) {$this->name}";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,18 @@
 | 
				
			||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace BookStack\Actions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Illuminate\Database\Eloquent\Factories\HasFactory;
 | 
				
			||||||
 | 
					use Illuminate\Database\Eloquent\Model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @property int    $id
 | 
				
			||||||
 | 
					 * @property int    $webhook_id
 | 
				
			||||||
 | 
					 * @property string $event
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class WebhookTrackedEvent extends Model
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    protected $fillable = ['event'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    use HasFactory;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,6 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace BookStack\Auth;
 | 
					namespace BookStack\Auth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use Activity;
 | 
					 | 
				
			||||||
use BookStack\Entities\EntityProvider;
 | 
					use BookStack\Entities\EntityProvider;
 | 
				
			||||||
use BookStack\Entities\Models\Book;
 | 
					use BookStack\Entities\Models\Book;
 | 
				
			||||||
use BookStack\Entities\Models\Bookshelf;
 | 
					use BookStack\Entities\Models\Bookshelf;
 | 
				
			||||||
| 
						 | 
					@ -218,14 +217,6 @@ class UserRepo
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Get the latest activity for a user.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function getActivity(User $user, int $count = 20, int $page = 0): array
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return Activity::userActivity($user, $count, $page);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Get the recently created content for this given user.
 | 
					     * Get the recently created content for this given user.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,7 @@
 | 
				
			||||||
return [
 | 
					return [
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Default driver to use for the queue
 | 
					    // Default driver to use for the queue
 | 
				
			||||||
    // Options: null, sync, redis
 | 
					    // Options: sync, database, redis
 | 
				
			||||||
    'default' => env('QUEUE_CONNECTION', 'sync'),
 | 
					    'default' => env('QUEUE_CONNECTION', 'sync'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Queue connection configuration
 | 
					    // Queue connection configuration
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,9 @@ namespace BookStack\Console\Commands;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use BookStack\Auth\UserRepo;
 | 
					use BookStack\Auth\UserRepo;
 | 
				
			||||||
use Illuminate\Console\Command;
 | 
					use Illuminate\Console\Command;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Validator;
 | 
				
			||||||
 | 
					use Illuminate\Validation\Rules\Password;
 | 
				
			||||||
 | 
					use Illuminate\Validation\Rules\Unique;
 | 
				
			||||||
use Symfony\Component\Console\Command\Command as SymfonyCommand;
 | 
					use Symfony\Component\Console\Command\Command as SymfonyCommand;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CreateAdmin extends Command
 | 
					class CreateAdmin extends Command
 | 
				
			||||||
| 
						 | 
					@ -45,43 +48,33 @@ class CreateAdmin extends Command
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function handle()
 | 
					    public function handle()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $email = trim($this->option('email'));
 | 
					        $details = $this->options();
 | 
				
			||||||
        if (empty($email)) {
 | 
					
 | 
				
			||||||
            $email = $this->ask('Please specify an email address for the new admin user');
 | 
					        if (empty($details['email'])) {
 | 
				
			||||||
 | 
					            $details['email'] = $this->ask('Please specify an email address for the new admin user');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (mb_strlen($email) < 5 || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
 | 
					        if (empty($details['name'])) {
 | 
				
			||||||
            $this->error('Invalid email address provided');
 | 
					            $details['name'] = $this->ask('Please specify a name for the new admin user');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (empty($details['password'])) {
 | 
				
			||||||
 | 
					            $details['password'] = $this->ask('Please specify a password for the new admin user (8 characters min)');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $validator = Validator::make($details, [
 | 
				
			||||||
 | 
					            'email'    => ['required', 'email', 'min:5', new Unique('users', 'email')],
 | 
				
			||||||
 | 
					            'name'     => ['required', 'min:2'],
 | 
				
			||||||
 | 
					            'password' => ['required', Password::default()],
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($validator->fails()) {
 | 
				
			||||||
 | 
					            foreach ($validator->errors()->all() as $error) {
 | 
				
			||||||
 | 
					                $this->error($error);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return SymfonyCommand::FAILURE;
 | 
					            return SymfonyCommand::FAILURE;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if ($this->userRepo->getByEmail($email) !== null) {
 | 
					        $user = $this->userRepo->create($validator->validated());
 | 
				
			||||||
            $this->error('A user with the provided email already exists!');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return SymfonyCommand::FAILURE;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $name = trim($this->option('name'));
 | 
					 | 
				
			||||||
        if (empty($name)) {
 | 
					 | 
				
			||||||
            $name = $this->ask('Please specify an name for the new admin user');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (mb_strlen($name) < 2) {
 | 
					 | 
				
			||||||
            $this->error('Invalid name provided');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return SymfonyCommand::FAILURE;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $password = trim($this->option('password'));
 | 
					 | 
				
			||||||
        if (empty($password)) {
 | 
					 | 
				
			||||||
            $password = $this->secret('Please specify a password for the new admin user');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (mb_strlen($password) < 5) {
 | 
					 | 
				
			||||||
            $this->error('Invalid password provided, Must be at least 5 characters');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return SymfonyCommand::FAILURE;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $user = $this->userRepo->create(['email' => $email, 'name' => $name, 'password' => $password]);
 | 
					 | 
				
			||||||
        $this->userRepo->attachSystemRole($user, 'admin');
 | 
					        $this->userRepo->attachSystemRole($user, 'admin');
 | 
				
			||||||
        $this->userRepo->downloadAndAssignUserAvatar($user);
 | 
					        $this->userRepo->downloadAndAssignUserAvatar($user);
 | 
				
			||||||
        $user->email_confirmed = true;
 | 
					        $user->email_confirmed = true;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,7 +18,7 @@ class Chapter extends BookChild
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public $searchFactor = 1.2;
 | 
					    public $searchFactor = 1.2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected $fillable = ['name', 'description', 'priority', 'book_id'];
 | 
					    protected $fillable = ['name', 'description', 'priority'];
 | 
				
			||||||
    protected $hidden = ['restricted', 'pivot', 'deleted_at'];
 | 
					    protected $hidden = ['restricted', 'pivot', 'deleted_at'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,6 +14,7 @@ use BookStack\Entities\Tools\SlugGenerator;
 | 
				
			||||||
use BookStack\Facades\Permissions;
 | 
					use BookStack\Facades\Permissions;
 | 
				
			||||||
use BookStack\Interfaces\Deletable;
 | 
					use BookStack\Interfaces\Deletable;
 | 
				
			||||||
use BookStack\Interfaces\Favouritable;
 | 
					use BookStack\Interfaces\Favouritable;
 | 
				
			||||||
 | 
					use BookStack\Interfaces\Loggable;
 | 
				
			||||||
use BookStack\Interfaces\Sluggable;
 | 
					use BookStack\Interfaces\Sluggable;
 | 
				
			||||||
use BookStack\Interfaces\Viewable;
 | 
					use BookStack\Interfaces\Viewable;
 | 
				
			||||||
use BookStack\Model;
 | 
					use BookStack\Model;
 | 
				
			||||||
| 
						 | 
					@ -45,7 +46,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
 | 
				
			||||||
 * @method static Builder withLastView()
 | 
					 * @method static Builder withLastView()
 | 
				
			||||||
 * @method static Builder withViewCount()
 | 
					 * @method static Builder withViewCount()
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
abstract class Entity extends Model implements Sluggable, Favouritable, Viewable, Deletable
 | 
					abstract class Entity extends Model implements Sluggable, Favouritable, Viewable, Deletable, Loggable
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    use SoftDeletes;
 | 
					    use SoftDeletes;
 | 
				
			||||||
    use HasCreatorAndUpdater;
 | 
					    use HasCreatorAndUpdater;
 | 
				
			||||||
| 
						 | 
					@ -321,4 +322,12 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
 | 
				
			||||||
            ->where('user_id', '=', user()->id)
 | 
					            ->where('user_id', '=', user()->id)
 | 
				
			||||||
            ->exists();
 | 
					            ->exists();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * {@inheritdoc}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function logDescriptor(): string
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return "({$this->id}) {$this->name}";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -91,7 +91,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);
 | 
					        Activity::add(ActivityType::BOOK_CREATE, $book);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return $book;
 | 
					        return $book;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -102,7 +102,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);
 | 
					        Activity::add(ActivityType::BOOK_UPDATE, $book);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return $book;
 | 
					        return $book;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -127,7 +127,7 @@ class BookRepo
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $trashCan = new TrashCan();
 | 
					        $trashCan = new TrashCan();
 | 
				
			||||||
        $trashCan->softDestroyBook($book);
 | 
					        $trashCan->softDestroyBook($book);
 | 
				
			||||||
        Activity::addForEntity($book, ActivityType::BOOK_DELETE);
 | 
					        Activity::add(ActivityType::BOOK_DELETE, $book);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $trashCan->autoClearOld();
 | 
					        $trashCan->autoClearOld();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -90,7 +90,7 @@ 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);
 | 
					        Activity::add(ActivityType::BOOKSHELF_CREATE, $shelf);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return $shelf;
 | 
					        return $shelf;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -106,7 +106,7 @@ class BookshelfRepo
 | 
				
			||||||
            $this->updateBooks($shelf, $bookIds);
 | 
					            $this->updateBooks($shelf, $bookIds);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Activity::addForEntity($shelf, ActivityType::BOOKSHELF_UPDATE);
 | 
					        Activity::add(ActivityType::BOOKSHELF_UPDATE, $shelf);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return $shelf;
 | 
					        return $shelf;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -177,7 +177,7 @@ class BookshelfRepo
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $trashCan = new TrashCan();
 | 
					        $trashCan = new TrashCan();
 | 
				
			||||||
        $trashCan->softDestroyShelf($shelf);
 | 
					        $trashCan->softDestroyShelf($shelf);
 | 
				
			||||||
        Activity::addForEntity($shelf, ActivityType::BOOKSHELF_DELETE);
 | 
					        Activity::add(ActivityType::BOOKSHELF_DELETE, $shelf);
 | 
				
			||||||
        $trashCan->autoClearOld();
 | 
					        $trashCan->autoClearOld();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,7 @@ namespace BookStack\Entities\Repos;
 | 
				
			||||||
use BookStack\Actions\ActivityType;
 | 
					use BookStack\Actions\ActivityType;
 | 
				
			||||||
use BookStack\Entities\Models\Book;
 | 
					use BookStack\Entities\Models\Book;
 | 
				
			||||||
use BookStack\Entities\Models\Chapter;
 | 
					use BookStack\Entities\Models\Chapter;
 | 
				
			||||||
 | 
					use BookStack\Entities\Models\Entity;
 | 
				
			||||||
use BookStack\Entities\Tools\BookContents;
 | 
					use BookStack\Entities\Tools\BookContents;
 | 
				
			||||||
use BookStack\Entities\Tools\TrashCan;
 | 
					use BookStack\Entities\Tools\TrashCan;
 | 
				
			||||||
use BookStack\Exceptions\MoveOperationException;
 | 
					use BookStack\Exceptions\MoveOperationException;
 | 
				
			||||||
| 
						 | 
					@ -49,7 +50,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);
 | 
					        Activity::add(ActivityType::CHAPTER_CREATE, $chapter);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return $chapter;
 | 
					        return $chapter;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -60,7 +61,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);
 | 
					        Activity::add(ActivityType::CHAPTER_UPDATE, $chapter);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return $chapter;
 | 
					        return $chapter;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -74,7 +75,7 @@ class ChapterRepo
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $trashCan = new TrashCan();
 | 
					        $trashCan = new TrashCan();
 | 
				
			||||||
        $trashCan->softDestroyChapter($chapter);
 | 
					        $trashCan->softDestroyChapter($chapter);
 | 
				
			||||||
        Activity::addForEntity($chapter, ActivityType::CHAPTER_DELETE);
 | 
					        Activity::add(ActivityType::CHAPTER_DELETE, $chapter);
 | 
				
			||||||
        $trashCan->autoClearOld();
 | 
					        $trashCan->autoClearOld();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -87,24 +88,36 @@ class ChapterRepo
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function move(Chapter $chapter, string $parentIdentifier): Book
 | 
					    public function move(Chapter $chapter, string $parentIdentifier): Book
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $stringExploded = explode(':', $parentIdentifier);
 | 
					 | 
				
			||||||
        $entityType = $stringExploded[0];
 | 
					 | 
				
			||||||
        $entityId = intval($stringExploded[1]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if ($entityType !== 'book') {
 | 
					 | 
				
			||||||
            throw new MoveOperationException('Chapters can only be moved into books');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /** @var Book $parent */
 | 
					        /** @var Book $parent */
 | 
				
			||||||
        $parent = Book::visible()->where('id', '=', $entityId)->first();
 | 
					        $parent = $this->findParentByIdentifier($parentIdentifier);
 | 
				
			||||||
        if ($parent === null) {
 | 
					        if (is_null($parent)) {
 | 
				
			||||||
            throw new MoveOperationException('Book to move chapter into not found');
 | 
					            throw new MoveOperationException('Book to move chapter into not found');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $chapter->changeBook($parent->id);
 | 
					        $chapter->changeBook($parent->id);
 | 
				
			||||||
        $chapter->rebuildPermissions();
 | 
					        $chapter->rebuildPermissions();
 | 
				
			||||||
        Activity::addForEntity($chapter, ActivityType::CHAPTER_MOVE);
 | 
					        Activity::add(ActivityType::CHAPTER_MOVE, $chapter);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return $parent;
 | 
					        return $parent;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Find a page parent entity via an identifier string in the format:
 | 
				
			||||||
 | 
					     * {type}:{id}
 | 
				
			||||||
 | 
					     * Example: (book:5).
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @throws MoveOperationException
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function findParentByIdentifier(string $identifier): ?Book
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $stringExploded = explode(':', $identifier);
 | 
				
			||||||
 | 
					        $entityType = $stringExploded[0];
 | 
				
			||||||
 | 
					        $entityId = intval($stringExploded[1]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($entityType !== 'book') {
 | 
				
			||||||
 | 
					            throw new MoveOperationException('Chapters can only be in books');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Book::visible()->where('id', '=', $entityId)->first();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -171,7 +171,7 @@ class PageRepo
 | 
				
			||||||
        $draft->indexForSearch();
 | 
					        $draft->indexForSearch();
 | 
				
			||||||
        $draft->refresh();
 | 
					        $draft->refresh();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Activity::addForEntity($draft, ActivityType::PAGE_CREATE);
 | 
					        Activity::add(ActivityType::PAGE_CREATE, $draft);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return $draft;
 | 
					        return $draft;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -205,7 +205,7 @@ class PageRepo
 | 
				
			||||||
            $this->savePageRevision($page, $summary);
 | 
					            $this->savePageRevision($page, $summary);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Activity::addForEntity($page, ActivityType::PAGE_UPDATE);
 | 
					        Activity::add(ActivityType::PAGE_UPDATE, $page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return $page;
 | 
					        return $page;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -281,7 +281,7 @@ class PageRepo
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $trashCan = new TrashCan();
 | 
					        $trashCan = new TrashCan();
 | 
				
			||||||
        $trashCan->softDestroyPage($page);
 | 
					        $trashCan->softDestroyPage($page);
 | 
				
			||||||
        Activity::addForEntity($page, ActivityType::PAGE_DELETE);
 | 
					        Activity::add(ActivityType::PAGE_DELETE, $page);
 | 
				
			||||||
        $trashCan->autoClearOld();
 | 
					        $trashCan->autoClearOld();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -312,7 +312,7 @@ class PageRepo
 | 
				
			||||||
        $summary = trans('entities.pages_revision_restored_from', ['id' => strval($revisionId), 'summary' => $revision->summary]);
 | 
					        $summary = trans('entities.pages_revision_restored_from', ['id' => strval($revisionId), 'summary' => $revision->summary]);
 | 
				
			||||||
        $this->savePageRevision($page, $summary);
 | 
					        $this->savePageRevision($page, $summary);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Activity::addForEntity($page, ActivityType::PAGE_RESTORE);
 | 
					        Activity::add(ActivityType::PAGE_RESTORE, $page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return $page;
 | 
					        return $page;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -341,56 +341,19 @@ class PageRepo
 | 
				
			||||||
        $page->changeBook($newBookId);
 | 
					        $page->changeBook($newBookId);
 | 
				
			||||||
        $page->rebuildPermissions();
 | 
					        $page->rebuildPermissions();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Activity::addForEntity($page, ActivityType::PAGE_MOVE);
 | 
					        Activity::add(ActivityType::PAGE_MOVE, $page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return $parent;
 | 
					        return $parent;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Copy an existing page in the system.
 | 
					     * Find a page parent entity via an identifier string in the format:
 | 
				
			||||||
     * Optionally providing a new parent via string identifier and a new name.
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @throws MoveOperationException
 | 
					 | 
				
			||||||
     * @throws PermissionsException
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function copy(Page $page, string $parentIdentifier = null, string $newName = null): Page
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $parent = $parentIdentifier ? $this->findParentByIdentifier($parentIdentifier) : $page->getParent();
 | 
					 | 
				
			||||||
        if ($parent === null) {
 | 
					 | 
				
			||||||
            throw new MoveOperationException('Book or chapter to move page into not found');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!userCan('page-create', $parent)) {
 | 
					 | 
				
			||||||
            throw new PermissionsException('User does not have permission to create a page within the new parent');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $copyPage = $this->getNewDraftPage($parent);
 | 
					 | 
				
			||||||
        $pageData = $page->getAttributes();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Update name
 | 
					 | 
				
			||||||
        if (!empty($newName)) {
 | 
					 | 
				
			||||||
            $pageData['name'] = $newName;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Copy tags from previous page if set
 | 
					 | 
				
			||||||
        if ($page->tags) {
 | 
					 | 
				
			||||||
            $pageData['tags'] = [];
 | 
					 | 
				
			||||||
            foreach ($page->tags as $tag) {
 | 
					 | 
				
			||||||
                $pageData['tags'][] = ['name' => $tag->name, 'value' => $tag->value];
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return $this->publishDraft($copyPage, $pageData);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Find a page parent entity via a identifier string in the format:
 | 
					 | 
				
			||||||
     * {type}:{id}
 | 
					     * {type}:{id}
 | 
				
			||||||
     * Example: (book:5).
 | 
					     * Example: (book:5).
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @throws MoveOperationException
 | 
					     * @throws MoveOperationException
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    protected function findParentByIdentifier(string $identifier): ?Entity
 | 
					    public function findParentByIdentifier(string $identifier): ?Entity
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $stringExploded = explode(':', $identifier);
 | 
					        $stringExploded = explode(':', $identifier);
 | 
				
			||||||
        $entityType = $stringExploded[0];
 | 
					        $entityType = $stringExploded[0];
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,147 @@
 | 
				
			||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace BookStack\Entities\Tools;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use BookStack\Actions\Tag;
 | 
				
			||||||
 | 
					use BookStack\Entities\Models\Book;
 | 
				
			||||||
 | 
					use BookStack\Entities\Models\Chapter;
 | 
				
			||||||
 | 
					use BookStack\Entities\Models\Entity;
 | 
				
			||||||
 | 
					use BookStack\Entities\Models\Page;
 | 
				
			||||||
 | 
					use BookStack\Entities\Repos\BookRepo;
 | 
				
			||||||
 | 
					use BookStack\Entities\Repos\ChapterRepo;
 | 
				
			||||||
 | 
					use BookStack\Entities\Repos\PageRepo;
 | 
				
			||||||
 | 
					use BookStack\Uploads\Image;
 | 
				
			||||||
 | 
					use BookStack\Uploads\ImageService;
 | 
				
			||||||
 | 
					use Illuminate\Http\UploadedFile;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Cloner
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @var PageRepo
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected $pageRepo;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @var ChapterRepo
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected $chapterRepo;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @var BookRepo
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected $bookRepo;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @var ImageService
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected $imageService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function __construct(PageRepo $pageRepo, ChapterRepo $chapterRepo, BookRepo $bookRepo, ImageService $imageService)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->pageRepo = $pageRepo;
 | 
				
			||||||
 | 
					        $this->chapterRepo = $chapterRepo;
 | 
				
			||||||
 | 
					        $this->bookRepo = $bookRepo;
 | 
				
			||||||
 | 
					        $this->imageService = $imageService;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Clone the given page into the given parent using the provided name.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function clonePage(Page $original, Entity $parent, string $newName): Page
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $copyPage = $this->pageRepo->getNewDraftPage($parent);
 | 
				
			||||||
 | 
					        $pageData = $original->getAttributes();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Update name & tags
 | 
				
			||||||
 | 
					        $pageData['name'] = $newName;
 | 
				
			||||||
 | 
					        $pageData['tags'] = $this->entityTagsToInputArray($original);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $this->pageRepo->publishDraft($copyPage, $pageData);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Clone the given page into the given parent using the provided name.
 | 
				
			||||||
 | 
					     * Clones all child pages.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function cloneChapter(Chapter $original, Book $parent, string $newName): Chapter
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $chapterDetails = $original->getAttributes();
 | 
				
			||||||
 | 
					        $chapterDetails['name'] = $newName;
 | 
				
			||||||
 | 
					        $chapterDetails['tags'] = $this->entityTagsToInputArray($original);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $copyChapter = $this->chapterRepo->create($chapterDetails, $parent);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (userCan('page-create', $copyChapter)) {
 | 
				
			||||||
 | 
					            /** @var Page $page */
 | 
				
			||||||
 | 
					            foreach ($original->getVisiblePages() as $page) {
 | 
				
			||||||
 | 
					                $this->clonePage($page, $copyChapter, $page->name);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $copyChapter;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Clone the given book.
 | 
				
			||||||
 | 
					     * Clones all child chapters & pages.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function cloneBook(Book $original, string $newName): Book
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $bookDetails = $original->getAttributes();
 | 
				
			||||||
 | 
					        $bookDetails['name'] = $newName;
 | 
				
			||||||
 | 
					        $bookDetails['tags'] = $this->entityTagsToInputArray($original);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $copyBook = $this->bookRepo->create($bookDetails);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $directChildren = $original->getDirectChildren();
 | 
				
			||||||
 | 
					        foreach ($directChildren as $child) {
 | 
				
			||||||
 | 
					            if ($child instanceof Chapter && userCan('chapter-create', $copyBook)) {
 | 
				
			||||||
 | 
					                $this->cloneChapter($child, $copyBook, $child->name);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if ($child instanceof Page && !$child->draft && userCan('page-create', $copyBook)) {
 | 
				
			||||||
 | 
					                $this->clonePage($child, $copyBook, $child->name);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($original->cover) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                $tmpImgFile = tmpfile();
 | 
				
			||||||
 | 
					                $uploadedFile = $this->imageToUploadedFile($original->cover, $tmpImgFile);
 | 
				
			||||||
 | 
					                $this->bookRepo->updateCoverImage($copyBook, $uploadedFile, false);
 | 
				
			||||||
 | 
					            } catch (\Exception $exception) {
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $copyBook;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Convert an image instance to an UploadedFile instance to mimic
 | 
				
			||||||
 | 
					     * a file being uploaded.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected function imageToUploadedFile(Image $image, &$tmpFile): ?UploadedFile
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $imgData = $this->imageService->getImageData($image);
 | 
				
			||||||
 | 
					        $tmpImgFilePath = stream_get_meta_data($tmpFile)['uri'];
 | 
				
			||||||
 | 
					        file_put_contents($tmpImgFilePath, $imgData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return new UploadedFile($tmpImgFilePath, basename($image->path));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Convert the tags on the given entity to the raw format
 | 
				
			||||||
 | 
					     * that's used for incoming request data.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected function entityTagsToInputArray(Entity $entity): array
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $tags = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /** @var Tag $tag */
 | 
				
			||||||
 | 
					        foreach ($entity->tags as $tag) {
 | 
				
			||||||
 | 
					            $tags[] = ['name' => $tag->name, 'value' => $tag->value];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $tags;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -35,7 +35,7 @@ class PermissionsUpdater
 | 
				
			||||||
        $entity->save();
 | 
					        $entity->save();
 | 
				
			||||||
        $entity->rebuildPermissions();
 | 
					        $entity->rebuildPermissions();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Activity::addForEntity($entity, ActivityType::PERMISSIONS_UPDATE);
 | 
					        Activity::add(ActivityType::PERMISSIONS_UPDATE, $entity);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -57,17 +57,17 @@ class SearchResultsFormatter
 | 
				
			||||||
    protected function highlightTagsContainingTerms(array $tags, array $terms): void
 | 
					    protected function highlightTagsContainingTerms(array $tags, array $terms): void
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        foreach ($tags as $tag) {
 | 
					        foreach ($tags as $tag) {
 | 
				
			||||||
            $tagName = strtolower($tag->name);
 | 
					            $tagName = mb_strtolower($tag->name);
 | 
				
			||||||
            $tagValue = strtolower($tag->value);
 | 
					            $tagValue = mb_strtolower($tag->value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            foreach ($terms as $term) {
 | 
					            foreach ($terms as $term) {
 | 
				
			||||||
                $termLower = strtolower($term);
 | 
					                $termLower = mb_strtolower($term);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (strpos($tagName, $termLower) !== false) {
 | 
					                if (mb_strpos($tagName, $termLower) !== false) {
 | 
				
			||||||
                    $tag->setAttribute('highlight_name', true);
 | 
					                    $tag->setAttribute('highlight_name', true);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (strpos($tagValue, $termLower) !== false) {
 | 
					                if (mb_strpos($tagValue, $termLower) !== false) {
 | 
				
			||||||
                    $tag->setAttribute('highlight_value', true);
 | 
					                    $tag->setAttribute('highlight_value', true);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
| 
						 | 
					@ -84,17 +84,17 @@ class SearchResultsFormatter
 | 
				
			||||||
    protected function getMatchPositions(string $text, array $terms): array
 | 
					    protected function getMatchPositions(string $text, array $terms): array
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $matchRefs = [];
 | 
					        $matchRefs = [];
 | 
				
			||||||
        $text = strtolower($text);
 | 
					        $text = mb_strtolower($text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        foreach ($terms as $term) {
 | 
					        foreach ($terms as $term) {
 | 
				
			||||||
            $offset = 0;
 | 
					            $offset = 0;
 | 
				
			||||||
            $term = strtolower($term);
 | 
					            $term = mb_strtolower($term);
 | 
				
			||||||
            $pos = strpos($text, $term, $offset);
 | 
					            $pos = mb_strpos($text, $term, $offset);
 | 
				
			||||||
            while ($pos !== false) {
 | 
					            while ($pos !== false) {
 | 
				
			||||||
                $end = $pos + strlen($term);
 | 
					                $end = $pos + mb_strlen($term);
 | 
				
			||||||
                $matchRefs[$pos] = $end;
 | 
					                $matchRefs[$pos] = $end;
 | 
				
			||||||
                $offset = $end;
 | 
					                $offset = $end;
 | 
				
			||||||
                $pos = strpos($text, $term, $offset);
 | 
					                $pos = mb_strpos($text, $term, $offset);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -141,7 +141,7 @@ class SearchResultsFormatter
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    protected function formatTextUsingMatchPositions(array $matchPositions, string $originalText, int $targetLength): string
 | 
					    protected function formatTextUsingMatchPositions(array $matchPositions, string $originalText, int $targetLength): string
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $maxEnd = strlen($originalText);
 | 
					        $maxEnd = mb_strlen($originalText);
 | 
				
			||||||
        $fetchAll = ($targetLength === 0);
 | 
					        $fetchAll = ($targetLength === 0);
 | 
				
			||||||
        $contextLength = ($fetchAll ? 0 : 32);
 | 
					        $contextLength = ($fetchAll ? 0 : 32);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -165,7 +165,7 @@ class SearchResultsFormatter
 | 
				
			||||||
                $contextStart = $start;
 | 
					                $contextStart = $start;
 | 
				
			||||||
                // Trims off '$startDiff' number of characters to bring it back to the start
 | 
					                // Trims off '$startDiff' number of characters to bring it back to the start
 | 
				
			||||||
                // if this current match zone.
 | 
					                // if this current match zone.
 | 
				
			||||||
                $content = substr($content, 0, strlen($content) + $startDiff);
 | 
					                $content = mb_substr($content, 0, mb_strlen($content) + $startDiff);
 | 
				
			||||||
                $contentTextLength += $startDiff;
 | 
					                $contentTextLength += $startDiff;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -176,16 +176,16 @@ class SearchResultsFormatter
 | 
				
			||||||
            } elseif ($fetchAll) {
 | 
					            } elseif ($fetchAll) {
 | 
				
			||||||
                // Or fill in gap since the previous match
 | 
					                // Or fill in gap since the previous match
 | 
				
			||||||
                $fillLength = $contextStart - $lastEnd;
 | 
					                $fillLength = $contextStart - $lastEnd;
 | 
				
			||||||
                $content .= e(substr($originalText, $lastEnd, $fillLength));
 | 
					                $content .= e(mb_substr($originalText, $lastEnd, $fillLength));
 | 
				
			||||||
                $contentTextLength += $fillLength;
 | 
					                $contentTextLength += $fillLength;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Add our content including the bolded matching text
 | 
					            // Add our content including the bolded matching text
 | 
				
			||||||
            $content .= e(substr($originalText, $contextStart, $start - $contextStart));
 | 
					            $content .= e(mb_substr($originalText, $contextStart, $start - $contextStart));
 | 
				
			||||||
            $contentTextLength += $start - $contextStart;
 | 
					            $contentTextLength += $start - $contextStart;
 | 
				
			||||||
            $content .= '<strong>' . e(substr($originalText, $start, $end - $start)) . '</strong>';
 | 
					            $content .= '<strong>' . e(mb_substr($originalText, $start, $end - $start)) . '</strong>';
 | 
				
			||||||
            $contentTextLength += $end - $start;
 | 
					            $contentTextLength += $end - $start;
 | 
				
			||||||
            $content .= e(substr($originalText, $end, $contextEnd - $end));
 | 
					            $content .= e(mb_substr($originalText, $end, $contextEnd - $end));
 | 
				
			||||||
            $contentTextLength += $contextEnd - $end;
 | 
					            $contentTextLength += $contextEnd - $end;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Update our last end position
 | 
					            // Update our last end position
 | 
				
			||||||
| 
						 | 
					@ -204,7 +204,7 @@ class SearchResultsFormatter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Just copy out the content if we haven't moved along anywhere.
 | 
					        // Just copy out the content if we haven't moved along anywhere.
 | 
				
			||||||
        if ($lastEnd === 0) {
 | 
					        if ($lastEnd === 0) {
 | 
				
			||||||
            $content = e(substr($originalText, 0, $targetLength));
 | 
					            $content = e(mb_substr($originalText, 0, $targetLength));
 | 
				
			||||||
            $contentTextLength = $targetLength;
 | 
					            $contentTextLength = $targetLength;
 | 
				
			||||||
            $lastEnd = $targetLength;
 | 
					            $lastEnd = $targetLength;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -213,7 +213,7 @@ class SearchResultsFormatter
 | 
				
			||||||
        $remainder = $targetLength - $contentTextLength;
 | 
					        $remainder = $targetLength - $contentTextLength;
 | 
				
			||||||
        if ($remainder > 10) {
 | 
					        if ($remainder > 10) {
 | 
				
			||||||
            $padEndLength = min($maxEnd - $lastEnd, $remainder);
 | 
					            $padEndLength = min($maxEnd - $lastEnd, $remainder);
 | 
				
			||||||
            $content .= e(substr($originalText, $lastEnd, $padEndLength));
 | 
					            $content .= e(mb_substr($originalText, $lastEnd, $padEndLength));
 | 
				
			||||||
            $lastEnd += $padEndLength;
 | 
					            $lastEnd += $padEndLength;
 | 
				
			||||||
            $contentTextLength += $padEndLength;
 | 
					            $contentTextLength += $padEndLength;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -223,7 +223,7 @@ class SearchResultsFormatter
 | 
				
			||||||
        $firstStart = $firstStart ?: 0;
 | 
					        $firstStart = $firstStart ?: 0;
 | 
				
			||||||
        if (!$fetchAll && $remainder > 10 && $firstStart !== 0) {
 | 
					        if (!$fetchAll && $remainder > 10 && $firstStart !== 0) {
 | 
				
			||||||
            $padStart = max(0, $firstStart - $remainder);
 | 
					            $padStart = max(0, $firstStart - $remainder);
 | 
				
			||||||
            $content = ($padStart === 0 ? '' : '...') . e(substr($originalText, $padStart, $firstStart - $padStart)) . substr($content, 4);
 | 
					            $content = ($padStart === 0 ? '' : '...') . e(mb_substr($originalText, $padStart, $firstStart - $padStart)) . mb_substr($content, 4);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Add ellipsis if we're not at the end
 | 
					        // Add ellipsis if we're not at the end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,9 @@ namespace BookStack\Facades;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use Illuminate\Support\Facades\Facade;
 | 
					use Illuminate\Support\Facades\Facade;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @see \BookStack\Actions\ActivityLogger
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
class Activity extends Facade
 | 
					class Activity extends Facade
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,6 +20,7 @@ class AuditLogController extends Controller
 | 
				
			||||||
            'date_from' => $request->get('date_from', ''),
 | 
					            'date_from' => $request->get('date_from', ''),
 | 
				
			||||||
            'date_to'   => $request->get('date_to', ''),
 | 
					            'date_to'   => $request->get('date_to', ''),
 | 
				
			||||||
            'user'      => $request->get('user', ''),
 | 
					            'user'      => $request->get('user', ''),
 | 
				
			||||||
 | 
					            'ip'        => $request->get('ip', ''),
 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $query = Activity::query()
 | 
					        $query = Activity::query()
 | 
				
			||||||
| 
						 | 
					@ -44,6 +45,9 @@ class AuditLogController extends Controller
 | 
				
			||||||
        if ($listDetails['date_to']) {
 | 
					        if ($listDetails['date_to']) {
 | 
				
			||||||
            $query->where('created_at', '<=', $listDetails['date_to']);
 | 
					            $query->where('created_at', '<=', $listDetails['date_to']);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        if ($listDetails['ip']) {
 | 
				
			||||||
 | 
					            $query->where('ip', 'like', $listDetails['ip'] . '%');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $activities = $query->paginate(100);
 | 
					        $activities = $query->paginate(100);
 | 
				
			||||||
        $activities->appends($listDetails);
 | 
					        $activities->appends($listDetails);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,11 +2,11 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace BookStack\Http\Controllers\Auth;
 | 
					namespace BookStack\Http\Controllers\Auth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use Activity;
 | 
					 | 
				
			||||||
use BookStack\Auth\Access\LoginService;
 | 
					use BookStack\Auth\Access\LoginService;
 | 
				
			||||||
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\Facades\Activity;
 | 
				
			||||||
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;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,7 @@ use Illuminate\Foundation\Auth\RegistersUsers;
 | 
				
			||||||
use Illuminate\Http\Request;
 | 
					use Illuminate\Http\Request;
 | 
				
			||||||
use Illuminate\Support\Facades\Hash;
 | 
					use Illuminate\Support\Facades\Hash;
 | 
				
			||||||
use Illuminate\Support\Facades\Validator;
 | 
					use Illuminate\Support\Facades\Validator;
 | 
				
			||||||
 | 
					use Illuminate\Validation\Rules\Password;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RegisterController extends Controller
 | 
					class RegisterController extends Controller
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
| 
						 | 
					@ -70,7 +71,7 @@ class RegisterController extends Controller
 | 
				
			||||||
        return Validator::make($data, [
 | 
					        return Validator::make($data, [
 | 
				
			||||||
            'name'     => ['required', 'min:2', 'max:255'],
 | 
					            'name'     => ['required', 'min:2', 'max:255'],
 | 
				
			||||||
            'email'    => ['required', 'email', 'max:255', 'unique:users'],
 | 
					            'email'    => ['required', 'email', 'max:255', 'unique:users'],
 | 
				
			||||||
            'password' => ['required', 'min:8'],
 | 
					            'password' => ['required', Password::default()],
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,6 +11,7 @@ use Exception;
 | 
				
			||||||
use Illuminate\Http\RedirectResponse;
 | 
					use Illuminate\Http\RedirectResponse;
 | 
				
			||||||
use Illuminate\Http\Request;
 | 
					use Illuminate\Http\Request;
 | 
				
			||||||
use Illuminate\Routing\Redirector;
 | 
					use Illuminate\Routing\Redirector;
 | 
				
			||||||
 | 
					use Illuminate\Validation\Rules\Password;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserInviteController extends Controller
 | 
					class UserInviteController extends Controller
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
| 
						 | 
					@ -55,7 +56,7 @@ class UserInviteController extends Controller
 | 
				
			||||||
    public function setPassword(Request $request, string $token)
 | 
					    public function setPassword(Request $request, string $token)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->validate($request, [
 | 
					        $this->validate($request, [
 | 
				
			||||||
            'password' => ['required', 'min:8'],
 | 
					            'password' => ['required', Password::default()],
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,15 +2,18 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace BookStack\Http\Controllers;
 | 
					namespace BookStack\Http\Controllers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use Activity;
 | 
					use BookStack\Actions\ActivityQueries;
 | 
				
			||||||
use BookStack\Actions\ActivityType;
 | 
					use BookStack\Actions\ActivityType;
 | 
				
			||||||
use BookStack\Actions\View;
 | 
					use BookStack\Actions\View;
 | 
				
			||||||
use BookStack\Entities\Models\Bookshelf;
 | 
					use BookStack\Entities\Models\Bookshelf;
 | 
				
			||||||
use BookStack\Entities\Repos\BookRepo;
 | 
					use BookStack\Entities\Repos\BookRepo;
 | 
				
			||||||
use BookStack\Entities\Tools\BookContents;
 | 
					use BookStack\Entities\Tools\BookContents;
 | 
				
			||||||
 | 
					use BookStack\Entities\Tools\Cloner;
 | 
				
			||||||
use BookStack\Entities\Tools\PermissionsUpdater;
 | 
					use BookStack\Entities\Tools\PermissionsUpdater;
 | 
				
			||||||
use BookStack\Entities\Tools\ShelfContext;
 | 
					use BookStack\Entities\Tools\ShelfContext;
 | 
				
			||||||
use BookStack\Exceptions\ImageUploadException;
 | 
					use BookStack\Exceptions\ImageUploadException;
 | 
				
			||||||
 | 
					use BookStack\Exceptions\NotFoundException;
 | 
				
			||||||
 | 
					use BookStack\Facades\Activity;
 | 
				
			||||||
use Illuminate\Http\Request;
 | 
					use Illuminate\Http\Request;
 | 
				
			||||||
use Illuminate\Validation\ValidationException;
 | 
					use Illuminate\Validation\ValidationException;
 | 
				
			||||||
use Throwable;
 | 
					use Throwable;
 | 
				
			||||||
| 
						 | 
					@ -101,7 +104,7 @@ class BookController extends Controller
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if ($bookshelf) {
 | 
					        if ($bookshelf) {
 | 
				
			||||||
            $bookshelf->appendBook($book);
 | 
					            $bookshelf->appendBook($book);
 | 
				
			||||||
            Activity::addForEntity($bookshelf, ActivityType::BOOKSHELF_UPDATE);
 | 
					            Activity::add(ActivityType::BOOKSHELF_UPDATE, $bookshelf);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return redirect($book->getUrl());
 | 
					        return redirect($book->getUrl());
 | 
				
			||||||
| 
						 | 
					@ -110,7 +113,7 @@ class BookController extends Controller
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Display the specified book.
 | 
					     * Display the specified book.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function show(Request $request, string $slug)
 | 
					    public function show(Request $request, ActivityQueries $activities, string $slug)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $book = $this->bookRepo->getBySlug($slug);
 | 
					        $book = $this->bookRepo->getBySlug($slug);
 | 
				
			||||||
        $bookChildren = (new BookContents($book))->getTree(true);
 | 
					        $bookChildren = (new BookContents($book))->getTree(true);
 | 
				
			||||||
| 
						 | 
					@ -128,7 +131,7 @@ class BookController extends Controller
 | 
				
			||||||
            'current'           => $book,
 | 
					            'current'           => $book,
 | 
				
			||||||
            'bookChildren'      => $bookChildren,
 | 
					            'bookChildren'      => $bookChildren,
 | 
				
			||||||
            'bookParentShelves' => $bookParentShelves,
 | 
					            'bookParentShelves' => $bookParentShelves,
 | 
				
			||||||
            'activity'          => Activity::entityActivity($book, 20, 1),
 | 
					            'activity'          => $activities->entityActivity($book, 20, 1),
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -224,4 +227,39 @@ class BookController extends Controller
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return redirect($book->getUrl());
 | 
					        return redirect($book->getUrl());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Show the view to copy a book.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @throws NotFoundException
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function showCopy(string $bookSlug)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $book = $this->bookRepo->getBySlug($bookSlug);
 | 
				
			||||||
 | 
					        $this->checkOwnablePermission('book-view', $book);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        session()->flashInput(['name' => $book->name]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return view('books.copy', [
 | 
				
			||||||
 | 
					            'book' => $book,
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Create a copy of a book within the requested target destination.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @throws NotFoundException
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function copy(Request $request, Cloner $cloner, string $bookSlug)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $book = $this->bookRepo->getBySlug($bookSlug);
 | 
				
			||||||
 | 
					        $this->checkOwnablePermission('book-view', $book);
 | 
				
			||||||
 | 
					        $this->checkPermission('book-create-all');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $newName = $request->get('name') ?: $book->name;
 | 
				
			||||||
 | 
					        $bookCopy = $cloner->cloneBook($book, $newName);
 | 
				
			||||||
 | 
					        $this->showSuccessNotification(trans('entities.books_copy_success'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return redirect($bookCopy->getUrl());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -71,7 +71,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::addForEntity($book, ActivityType::BOOK_SORT);
 | 
					            Activity::add(ActivityType::BOOK_SORT, $book);
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return redirect($book->getUrl());
 | 
					        return redirect($book->getUrl());
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace BookStack\Http\Controllers;
 | 
					namespace BookStack\Http\Controllers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use Activity;
 | 
					use BookStack\Actions\ActivityQueries;
 | 
				
			||||||
use BookStack\Actions\View;
 | 
					use BookStack\Actions\View;
 | 
				
			||||||
use BookStack\Entities\Models\Book;
 | 
					use BookStack\Entities\Models\Book;
 | 
				
			||||||
use BookStack\Entities\Repos\BookshelfRepo;
 | 
					use BookStack\Entities\Repos\BookshelfRepo;
 | 
				
			||||||
| 
						 | 
					@ -101,7 +101,7 @@ class BookshelfController extends Controller
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @throws NotFoundException
 | 
					     * @throws NotFoundException
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function show(string $slug)
 | 
					    public function show(ActivityQueries $activities, string $slug)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $shelf = $this->bookshelfRepo->getBySlug($slug);
 | 
					        $shelf = $this->bookshelfRepo->getBySlug($slug);
 | 
				
			||||||
        $this->checkOwnablePermission('book-view', $shelf);
 | 
					        $this->checkOwnablePermission('book-view', $shelf);
 | 
				
			||||||
| 
						 | 
					@ -124,7 +124,7 @@ class BookshelfController extends Controller
 | 
				
			||||||
            'shelf'                   => $shelf,
 | 
					            'shelf'                   => $shelf,
 | 
				
			||||||
            'sortedVisibleShelfBooks' => $sortedVisibleShelfBooks,
 | 
					            'sortedVisibleShelfBooks' => $sortedVisibleShelfBooks,
 | 
				
			||||||
            'view'                    => $view,
 | 
					            'view'                    => $view,
 | 
				
			||||||
            'activity'                => Activity::entityActivity($shelf, 20, 1),
 | 
					            'activity'                => $activities->entityActivity($shelf, 20, 1),
 | 
				
			||||||
            'order'                   => $order,
 | 
					            'order'                   => $order,
 | 
				
			||||||
            'sort'                    => $sort,
 | 
					            'sort'                    => $sort,
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,7 @@ use BookStack\Actions\View;
 | 
				
			||||||
use BookStack\Entities\Models\Book;
 | 
					use BookStack\Entities\Models\Book;
 | 
				
			||||||
use BookStack\Entities\Repos\ChapterRepo;
 | 
					use BookStack\Entities\Repos\ChapterRepo;
 | 
				
			||||||
use BookStack\Entities\Tools\BookContents;
 | 
					use BookStack\Entities\Tools\BookContents;
 | 
				
			||||||
 | 
					use BookStack\Entities\Tools\Cloner;
 | 
				
			||||||
use BookStack\Entities\Tools\NextPreviousContentLocator;
 | 
					use BookStack\Entities\Tools\NextPreviousContentLocator;
 | 
				
			||||||
use BookStack\Entities\Tools\PermissionsUpdater;
 | 
					use BookStack\Entities\Tools\PermissionsUpdater;
 | 
				
			||||||
use BookStack\Exceptions\MoveOperationException;
 | 
					use BookStack\Exceptions\MoveOperationException;
 | 
				
			||||||
| 
						 | 
					@ -190,6 +191,53 @@ class ChapterController extends Controller
 | 
				
			||||||
        return redirect($chapter->getUrl());
 | 
					        return redirect($chapter->getUrl());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Show the view to copy a chapter.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @throws NotFoundException
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function showCopy(string $bookSlug, string $chapterSlug)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
 | 
				
			||||||
 | 
					        $this->checkOwnablePermission('chapter-view', $chapter);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        session()->flashInput(['name' => $chapter->name]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return view('chapters.copy', [
 | 
				
			||||||
 | 
					            'book'    => $chapter->book,
 | 
				
			||||||
 | 
					            'chapter' => $chapter,
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Create a copy of a chapter within the requested target destination.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @throws NotFoundException
 | 
				
			||||||
 | 
					     * @throws Throwable
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function copy(Request $request, Cloner $cloner, string $bookSlug, string $chapterSlug)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
 | 
				
			||||||
 | 
					        $this->checkOwnablePermission('chapter-view', $chapter);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $entitySelection = $request->get('entity_selection') ?: null;
 | 
				
			||||||
 | 
					        $newParentBook = $entitySelection ? $this->chapterRepo->findParentByIdentifier($entitySelection) : $chapter->getParent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (is_null($newParentBook)) {
 | 
				
			||||||
 | 
					            $this->showErrorNotification(trans('errors.selected_book_not_found'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return redirect()->back();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->checkOwnablePermission('chapter-create', $newParentBook);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $newName = $request->get('name') ?: $chapter->name;
 | 
				
			||||||
 | 
					        $chapterCopy = $cloner->cloneChapter($chapter, $newParentBook, $newName);
 | 
				
			||||||
 | 
					        $this->showSuccessNotification(trans('entities.chapters_copy_success'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return redirect($chapterCopy->getUrl());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Show the Restrictions view.
 | 
					     * Show the Restrictions view.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace BookStack\Http\Controllers;
 | 
					namespace BookStack\Http\Controllers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use Activity;
 | 
					use BookStack\Actions\ActivityQueries;
 | 
				
			||||||
use BookStack\Entities\Models\Book;
 | 
					use BookStack\Entities\Models\Book;
 | 
				
			||||||
use BookStack\Entities\Models\Page;
 | 
					use BookStack\Entities\Models\Page;
 | 
				
			||||||
use BookStack\Entities\Queries\RecentlyViewed;
 | 
					use BookStack\Entities\Queries\RecentlyViewed;
 | 
				
			||||||
| 
						 | 
					@ -16,9 +16,9 @@ class HomeController extends Controller
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Display the homepage.
 | 
					     * Display the homepage.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function index()
 | 
					    public function index(ActivityQueries $activities)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $activity = Activity::latest(10);
 | 
					        $activity = $activities->latest(10);
 | 
				
			||||||
        $draftPages = [];
 | 
					        $draftPages = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if ($this->isSignedIn()) {
 | 
					        if ($this->isSignedIn()) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -67,7 +67,7 @@ class MaintenanceController extends Controller
 | 
				
			||||||
        $this->logActivity(ActivityType::MAINTENANCE_ACTION_RUN, 'send-test-email');
 | 
					        $this->logActivity(ActivityType::MAINTENANCE_ACTION_RUN, 'send-test-email');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            user()->notify(new TestEmail());
 | 
					            user()->notifyNow(new TestEmail());
 | 
				
			||||||
            $this->showSuccessNotification(trans('settings.maint_send_test_email_success', ['address' => user()->email]));
 | 
					            $this->showSuccessNotification(trans('settings.maint_send_test_email_success', ['address' => user()->email]));
 | 
				
			||||||
        } catch (\Exception $exception) {
 | 
					        } catch (\Exception $exception) {
 | 
				
			||||||
            $errorMessage = trans('errors.maintenance_test_email_failure') . "\n" . $exception->getMessage();
 | 
					            $errorMessage = trans('errors.maintenance_test_email_failure') . "\n" . $exception->getMessage();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,7 @@ use BookStack\Actions\View;
 | 
				
			||||||
use BookStack\Entities\Models\Page;
 | 
					use BookStack\Entities\Models\Page;
 | 
				
			||||||
use BookStack\Entities\Repos\PageRepo;
 | 
					use BookStack\Entities\Repos\PageRepo;
 | 
				
			||||||
use BookStack\Entities\Tools\BookContents;
 | 
					use BookStack\Entities\Tools\BookContents;
 | 
				
			||||||
 | 
					use BookStack\Entities\Tools\Cloner;
 | 
				
			||||||
use BookStack\Entities\Tools\NextPreviousContentLocator;
 | 
					use BookStack\Entities\Tools\NextPreviousContentLocator;
 | 
				
			||||||
use BookStack\Entities\Tools\PageContent;
 | 
					use BookStack\Entities\Tools\PageContent;
 | 
				
			||||||
use BookStack\Entities\Tools\PageEditActivity;
 | 
					use BookStack\Entities\Tools\PageEditActivity;
 | 
				
			||||||
| 
						 | 
					@ -447,26 +448,24 @@ class PageController extends Controller
 | 
				
			||||||
     * @throws NotFoundException
 | 
					     * @throws NotFoundException
 | 
				
			||||||
     * @throws Throwable
 | 
					     * @throws Throwable
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function copy(Request $request, string $bookSlug, string $pageSlug)
 | 
					    public function copy(Request $request, Cloner $cloner, string $bookSlug, string $pageSlug)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
 | 
					        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
 | 
				
			||||||
        $this->checkOwnablePermission('page-view', $page);
 | 
					        $this->checkOwnablePermission('page-view', $page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $entitySelection = $request->get('entity_selection', null) ?? null;
 | 
					        $entitySelection = $request->get('entity_selection') ?: null;
 | 
				
			||||||
        $newName = $request->get('name', null);
 | 
					        $newParent = $entitySelection ? $this->pageRepo->findParentByIdentifier($entitySelection) : $page->getParent();
 | 
				
			||||||
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
            $pageCopy = $this->pageRepo->copy($page, $entitySelection, $newName);
 | 
					 | 
				
			||||||
        } catch (Exception $exception) {
 | 
					 | 
				
			||||||
            if ($exception instanceof PermissionsException) {
 | 
					 | 
				
			||||||
                $this->showPermissionError();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (is_null($newParent)) {
 | 
				
			||||||
            $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
 | 
					            $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return redirect()->back();
 | 
					            return redirect()->back();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->checkOwnablePermission('page-create', $newParent);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $newName = $request->get('name') ?: $page->name;
 | 
				
			||||||
 | 
					        $pageCopy = $cloner->clonePage($page, $newParent, $newName);
 | 
				
			||||||
        $this->showSuccessNotification(trans('entities.pages_copy_success'));
 | 
					        $this->showSuccessNotification(trans('entities.pages_copy_success'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return redirect($pageCopy->getUrl());
 | 
					        return redirect($pageCopy->getUrl());
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,7 @@
 | 
				
			||||||
namespace BookStack\Http\Controllers;
 | 
					namespace BookStack\Http\Controllers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use BookStack\Auth\Permissions\PermissionsRepo;
 | 
					use BookStack\Auth\Permissions\PermissionsRepo;
 | 
				
			||||||
 | 
					use BookStack\Auth\Role;
 | 
				
			||||||
use BookStack\Exceptions\PermissionsException;
 | 
					use BookStack\Exceptions\PermissionsException;
 | 
				
			||||||
use Exception;
 | 
					use Exception;
 | 
				
			||||||
use Illuminate\Http\Request;
 | 
					use Illuminate\Http\Request;
 | 
				
			||||||
| 
						 | 
					@ -23,7 +24,7 @@ class RoleController extends Controller
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Show a listing of the roles in the system.
 | 
					     * Show a listing of the roles in the system.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function list()
 | 
					    public function index()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->checkPermission('user-roles-manage');
 | 
					        $this->checkPermission('user-roles-manage');
 | 
				
			||||||
        $roles = $this->permissionsRepo->getAllRoles();
 | 
					        $roles = $this->permissionsRepo->getAllRoles();
 | 
				
			||||||
| 
						 | 
					@ -34,11 +35,21 @@ class RoleController extends Controller
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Show the form to create a new role.
 | 
					     * Show the form to create a new role.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function create()
 | 
					    public function create(Request $request)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->checkPermission('user-roles-manage');
 | 
					        $this->checkPermission('user-roles-manage');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return view('settings.roles.create');
 | 
					        /** @var ?Role $role */
 | 
				
			||||||
 | 
					        $role = null;
 | 
				
			||||||
 | 
					        if ($request->has('copy_from')) {
 | 
				
			||||||
 | 
					            $role = Role::query()->find($request->get('copy_from'));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($role) {
 | 
				
			||||||
 | 
					            $role->display_name .= ' (' . trans('common.copy') . ')';
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return view('settings.roles.create', ['role' => $role]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
| 
						 | 
					@ -49,7 +60,7 @@ class RoleController extends Controller
 | 
				
			||||||
        $this->checkPermission('user-roles-manage');
 | 
					        $this->checkPermission('user-roles-manage');
 | 
				
			||||||
        $this->validate($request, [
 | 
					        $this->validate($request, [
 | 
				
			||||||
            'display_name' => ['required', 'min:3', 'max:180'],
 | 
					            'display_name' => ['required', 'min:3', 'max:180'],
 | 
				
			||||||
            'description'  => 'max:180',
 | 
					            'description'  => ['max:180'],
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $this->permissionsRepo->saveNewRole($request->all());
 | 
					        $this->permissionsRepo->saveNewRole($request->all());
 | 
				
			||||||
| 
						 | 
					@ -84,7 +95,7 @@ class RoleController extends Controller
 | 
				
			||||||
        $this->checkPermission('user-roles-manage');
 | 
					        $this->checkPermission('user-roles-manage');
 | 
				
			||||||
        $this->validate($request, [
 | 
					        $this->validate($request, [
 | 
				
			||||||
            'display_name' => ['required', 'min:3', 'max:180'],
 | 
					            'display_name' => ['required', 'min:3', 'max:180'],
 | 
				
			||||||
            'description'  => 'max:180',
 | 
					            'description'  => ['max:180'],
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $this->permissionsRepo->updateRole($id, $request->all());
 | 
					        $this->permissionsRepo->updateRole($id, $request->all());
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,7 @@ use BookStack\Uploads\ImageRepo;
 | 
				
			||||||
use Exception;
 | 
					use Exception;
 | 
				
			||||||
use Illuminate\Http\Request;
 | 
					use Illuminate\Http\Request;
 | 
				
			||||||
use Illuminate\Support\Str;
 | 
					use Illuminate\Support\Str;
 | 
				
			||||||
 | 
					use Illuminate\Validation\Rules\Password;
 | 
				
			||||||
use Illuminate\Validation\ValidationException;
 | 
					use Illuminate\Validation\ValidationException;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserController extends Controller
 | 
					class UserController extends Controller
 | 
				
			||||||
| 
						 | 
					@ -82,7 +83,7 @@ class UserController extends Controller
 | 
				
			||||||
        $sendInvite = ($request->get('send_invite', 'false') === 'true');
 | 
					        $sendInvite = ($request->get('send_invite', 'false') === 'true');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if ($authMethod === 'standard' && !$sendInvite) {
 | 
					        if ($authMethod === 'standard' && !$sendInvite) {
 | 
				
			||||||
            $validationRules['password'] = ['required', 'min:6'];
 | 
					            $validationRules['password'] = ['required', Password::default()];
 | 
				
			||||||
            $validationRules['password-confirm'] = ['required', 'same:password'];
 | 
					            $validationRules['password-confirm'] = ['required', 'same:password'];
 | 
				
			||||||
        } elseif ($authMethod === 'ldap' || $authMethod === 'saml2' || $authMethod === 'openid') {
 | 
					        } elseif ($authMethod === 'ldap' || $authMethod === 'saml2' || $authMethod === 'openid') {
 | 
				
			||||||
            $validationRules['external_auth_id'] = ['required'];
 | 
					            $validationRules['external_auth_id'] = ['required'];
 | 
				
			||||||
| 
						 | 
					@ -155,11 +156,11 @@ class UserController extends Controller
 | 
				
			||||||
        $this->checkPermissionOrCurrentUser('users-manage', $id);
 | 
					        $this->checkPermissionOrCurrentUser('users-manage', $id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $this->validate($request, [
 | 
					        $this->validate($request, [
 | 
				
			||||||
            'name'             => 'min:2',
 | 
					            'name'             => ['min:2'],
 | 
				
			||||||
            'email'            => ['min:2', 'email', 'unique:users,email,' . $id],
 | 
					            'email'            => ['min:2', 'email', 'unique:users,email,' . $id],
 | 
				
			||||||
            'password'         => ['min:6', 'required_with:password_confirm'],
 | 
					            'password'         => ['required_with:password_confirm', Password::default()],
 | 
				
			||||||
            'password-confirm' => ['same:password', 'required_with:password'],
 | 
					            'password-confirm' => ['same:password', 'required_with:password'],
 | 
				
			||||||
            'setting'          => 'array',
 | 
					            'setting'          => ['array'],
 | 
				
			||||||
            'profile_image'    => array_merge(['nullable'], $this->getImageValidationRules()),
 | 
					            'profile_image'    => array_merge(['nullable'], $this->getImageValidationRules()),
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,6 +2,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace BookStack\Http\Controllers;
 | 
					namespace BookStack\Http\Controllers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use BookStack\Actions\ActivityQueries;
 | 
				
			||||||
use BookStack\Auth\UserRepo;
 | 
					use BookStack\Auth\UserRepo;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserProfileController extends Controller
 | 
					class UserProfileController extends Controller
 | 
				
			||||||
| 
						 | 
					@ -9,11 +10,11 @@ class UserProfileController extends Controller
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Show the user profile page.
 | 
					     * Show the user profile page.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function show(UserRepo $repo, string $slug)
 | 
					    public function show(UserRepo $repo, ActivityQueries $activities, string $slug)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $user = $repo->getBySlug($slug);
 | 
					        $user = $repo->getBySlug($slug);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $userActivity = $repo->getActivity($user);
 | 
					        $userActivity = $activities->userActivity($user);
 | 
				
			||||||
        $recentlyCreated = $repo->getRecentlyCreated($user, 5);
 | 
					        $recentlyCreated = $repo->getRecentlyCreated($user, 5);
 | 
				
			||||||
        $assetCounts = $repo->getAssetCounts($user);
 | 
					        $assetCounts = $repo->getAssetCounts($user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,124 @@
 | 
				
			||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace BookStack\Http\Controllers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use BookStack\Actions\ActivityType;
 | 
				
			||||||
 | 
					use BookStack\Actions\Webhook;
 | 
				
			||||||
 | 
					use Illuminate\Http\Request;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class WebhookController extends Controller
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public function __construct()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->middleware([
 | 
				
			||||||
 | 
					            'can:settings-manage',
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Show all webhooks configured in the system.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function index()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $webhooks = Webhook::query()
 | 
				
			||||||
 | 
					            ->orderBy('name', 'desc')
 | 
				
			||||||
 | 
					            ->with('trackedEvents')
 | 
				
			||||||
 | 
					            ->get();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return view('settings.webhooks.index', ['webhooks' => $webhooks]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Show the view for creating a new webhook in the system.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function create()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return view('settings.webhooks.create');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Store a new webhook in the system.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function store(Request $request)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $validated = $this->validate($request, [
 | 
				
			||||||
 | 
					            'name'     => ['required', 'max:150'],
 | 
				
			||||||
 | 
					            'endpoint' => ['required', 'url', 'max:500'],
 | 
				
			||||||
 | 
					            'events'   => ['required', 'array'],
 | 
				
			||||||
 | 
					            'active'   => ['required'],
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $webhook = new Webhook($validated);
 | 
				
			||||||
 | 
					        $webhook->active = $validated['active'] === 'true';
 | 
				
			||||||
 | 
					        $webhook->save();
 | 
				
			||||||
 | 
					        $webhook->updateTrackedEvents(array_values($validated['events']));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->logActivity(ActivityType::WEBHOOK_CREATE, $webhook);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return redirect('/settings/webhooks');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Show the view to edit an existing webhook.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function edit(string $id)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /** @var Webhook $webhook */
 | 
				
			||||||
 | 
					        $webhook = Webhook::query()
 | 
				
			||||||
 | 
					            ->with('trackedEvents')
 | 
				
			||||||
 | 
					            ->findOrFail($id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return view('settings.webhooks.edit', ['webhook' => $webhook]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Update an existing webhook with the provided request data.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function update(Request $request, string $id)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $validated = $this->validate($request, [
 | 
				
			||||||
 | 
					            'name'     => ['required', 'max:150'],
 | 
				
			||||||
 | 
					            'endpoint' => ['required', 'url', 'max:500'],
 | 
				
			||||||
 | 
					            'events'   => ['required', 'array'],
 | 
				
			||||||
 | 
					            'active'   => ['required'],
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /** @var Webhook $webhook */
 | 
				
			||||||
 | 
					        $webhook = Webhook::query()->findOrFail($id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $webhook->active = $validated['active'] === 'true';
 | 
				
			||||||
 | 
					        $webhook->fill($validated)->save();
 | 
				
			||||||
 | 
					        $webhook->updateTrackedEvents($validated['events']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->logActivity(ActivityType::WEBHOOK_UPDATE, $webhook);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return redirect('/settings/webhooks');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Show the view to delete a webhook.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function delete(string $id)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /** @var Webhook $webhook */
 | 
				
			||||||
 | 
					        $webhook = Webhook::query()->findOrFail($id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return view('settings.webhooks.delete', ['webhook' => $webhook]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Destroy a webhook from the system.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function destroy(string $id)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /** @var Webhook $webhook */
 | 
				
			||||||
 | 
					        $webhook = Webhook::query()->findOrFail($id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $webhook->trackedEvents()->delete();
 | 
				
			||||||
 | 
					        $webhook->delete();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->logActivity(ActivityType::WEBHOOK_DELETE, $webhook);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return redirect('/settings/webhooks');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -11,6 +11,7 @@ use BookStack\Auth\Access\LoginService;
 | 
				
			||||||
use BookStack\Auth\Access\RegistrationService;
 | 
					use BookStack\Auth\Access\RegistrationService;
 | 
				
			||||||
use Illuminate\Support\Facades\Auth;
 | 
					use Illuminate\Support\Facades\Auth;
 | 
				
			||||||
use Illuminate\Support\ServiceProvider;
 | 
					use Illuminate\Support\ServiceProvider;
 | 
				
			||||||
 | 
					use Illuminate\Validation\Rules\Password;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AuthServiceProvider extends ServiceProvider
 | 
					class AuthServiceProvider extends ServiceProvider
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
| 
						 | 
					@ -21,6 +22,12 @@ class AuthServiceProvider extends ServiceProvider
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function boot()
 | 
					    public function boot()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        // Password Configuration
 | 
				
			||||||
 | 
					        Password::defaults(function () {
 | 
				
			||||||
 | 
					            return Password::min(8);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Custom guards
 | 
				
			||||||
        Auth::extend('api-token', function ($app, $name, array $config) {
 | 
					        Auth::extend('api-token', function ($app, $name, array $config) {
 | 
				
			||||||
            return new ApiTokenGuard($app['request'], $app->make(LoginService::class));
 | 
					            return new ApiTokenGuard($app['request'], $app->make(LoginService::class));
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace BookStack\Providers;
 | 
					namespace BookStack\Providers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use BookStack\Actions\ActivityService;
 | 
					use BookStack\Actions\ActivityLogger;
 | 
				
			||||||
use BookStack\Auth\Permissions\PermissionService;
 | 
					use BookStack\Auth\Permissions\PermissionService;
 | 
				
			||||||
use BookStack\Theming\ThemeService;
 | 
					use BookStack\Theming\ThemeService;
 | 
				
			||||||
use BookStack\Uploads\ImageService;
 | 
					use BookStack\Uploads\ImageService;
 | 
				
			||||||
| 
						 | 
					@ -28,7 +28,7 @@ class CustomFacadeProvider extends ServiceProvider
 | 
				
			||||||
    public function register()
 | 
					    public function register()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->app->singleton('activity', function () {
 | 
					        $this->app->singleton('activity', function () {
 | 
				
			||||||
            return $this->app->make(ActivityService::class);
 | 
					            return $this->app->make(ActivityLogger::class);
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $this->app->singleton('images', function () {
 | 
					        $this->app->singleton('images', function () {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,25 @@
 | 
				
			||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Database\Factories\Actions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use BookStack\Actions\Webhook;
 | 
				
			||||||
 | 
					use Illuminate\Database\Eloquent\Factories\Factory;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class WebhookFactory extends Factory
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    protected $model = Webhook::class;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Define the model's default state.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return array
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function definition()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					            'name'     => 'My webhook for ' . $this->faker->country(),
 | 
				
			||||||
 | 
					            'endpoint' => $this->faker->url,
 | 
				
			||||||
 | 
					            'active'   => true,
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,23 @@
 | 
				
			||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Database\Factories\Actions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use BookStack\Actions\ActivityType;
 | 
				
			||||||
 | 
					use BookStack\Actions\Webhook;
 | 
				
			||||||
 | 
					use Illuminate\Database\Eloquent\Factories\Factory;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class WebhookTrackedEventFactory extends Factory
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Define the model's default state.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return array
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function definition()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					            'webhook_id' => Webhook::factory(),
 | 
				
			||||||
 | 
					            'event'      => ActivityType::all()[array_rand(ActivityType::all())],
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -2,6 +2,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Database\Factories\Auth;
 | 
					namespace Database\Factories\Auth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use BookStack\Auth\User;
 | 
				
			||||||
use Illuminate\Database\Eloquent\Factories\Factory;
 | 
					use Illuminate\Database\Eloquent\Factories\Factory;
 | 
				
			||||||
use Illuminate\Support\Str;
 | 
					use Illuminate\Support\Str;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,7 +13,7 @@ class UserFactory extends Factory
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @var string
 | 
					     * @var string
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    protected $model = \BookStack\Auth\User::class;
 | 
					    protected $model = User::class;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Define the model's default state.
 | 
					     * Define the model's default state.
 | 
				
			||||||
| 
						 | 
					@ -26,7 +27,7 @@ class UserFactory extends Factory
 | 
				
			||||||
        return [
 | 
					        return [
 | 
				
			||||||
            'name'            => $name,
 | 
					            'name'            => $name,
 | 
				
			||||||
            'email'           => $this->faker->email,
 | 
					            'email'           => $this->faker->email,
 | 
				
			||||||
            'slug'            => \Illuminate\Support\Str::slug($name . '-' . \Illuminate\Support\Str::random(5)),
 | 
					            'slug'            => Str::slug($name . '-' . Str::random(5)),
 | 
				
			||||||
            'password'        => Str::random(10),
 | 
					            'password'        => Str::random(10),
 | 
				
			||||||
            'remember_token'  => Str::random(10),
 | 
					            'remember_token'  => Str::random(10),
 | 
				
			||||||
            'email_confirmed' => 1,
 | 
					            'email_confirmed' => 1,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Illuminate\Database\Migrations\Migration;
 | 
				
			||||||
 | 
					use Illuminate\Database\Schema\Blueprint;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Schema;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AddIndexForUserIp extends Migration
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Run the migrations.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return void
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function up()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Schema::table('activities', function (Blueprint $table) {
 | 
				
			||||||
 | 
					            $table->index('ip', 'activities_ip_index');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Reverse the migrations.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return void
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function down()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Schema::table('activities', function (Blueprint $table) {
 | 
				
			||||||
 | 
					            $table->dropIndex('activities_ip_index');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,48 @@
 | 
				
			||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Illuminate\Database\Migrations\Migration;
 | 
				
			||||||
 | 
					use Illuminate\Database\Schema\Blueprint;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Schema;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CreateWebhooksTable extends Migration
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Run the migrations.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return void
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function up()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Schema::create('webhooks', function (Blueprint $table) {
 | 
				
			||||||
 | 
					            $table->increments('id');
 | 
				
			||||||
 | 
					            $table->string('name', 150);
 | 
				
			||||||
 | 
					            $table->boolean('active');
 | 
				
			||||||
 | 
					            $table->string('endpoint', 500);
 | 
				
			||||||
 | 
					            $table->timestamps();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $table->index('name');
 | 
				
			||||||
 | 
					            $table->index('active');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Schema::create('webhook_tracked_events', function (Blueprint $table) {
 | 
				
			||||||
 | 
					            $table->increments('id');
 | 
				
			||||||
 | 
					            $table->integer('webhook_id');
 | 
				
			||||||
 | 
					            $table->string('event', 50);
 | 
				
			||||||
 | 
					            $table->timestamps();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $table->index('event');
 | 
				
			||||||
 | 
					            $table->index('webhook_id');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Reverse the migrations.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return void
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function down()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Schema::dropIfExists('webhooks');
 | 
				
			||||||
 | 
					        Schema::dropIfExists('webhook_tracked_events');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,36 @@
 | 
				
			||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Illuminate\Database\Migrations\Migration;
 | 
				
			||||||
 | 
					use Illuminate\Database\Schema\Blueprint;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Schema;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CreateJobsTable extends Migration
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Run the migrations.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return void
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function up()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Schema::create('jobs', function (Blueprint $table) {
 | 
				
			||||||
 | 
					            $table->bigIncrements('id');
 | 
				
			||||||
 | 
					            $table->string('queue')->index();
 | 
				
			||||||
 | 
					            $table->longText('payload');
 | 
				
			||||||
 | 
					            $table->unsignedTinyInteger('attempts');
 | 
				
			||||||
 | 
					            $table->unsignedInteger('reserved_at')->nullable();
 | 
				
			||||||
 | 
					            $table->unsignedInteger('available_at');
 | 
				
			||||||
 | 
					            $table->unsignedInteger('created_at');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Reverse the migrations.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return void
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function down()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Schema::dropIfExists('jobs');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,36 @@
 | 
				
			||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Illuminate\Database\Migrations\Migration;
 | 
				
			||||||
 | 
					use Illuminate\Database\Schema\Blueprint;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Schema;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CreateFailedJobsTable extends Migration
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Run the migrations.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return void
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function up()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Schema::create('failed_jobs', function (Blueprint $table) {
 | 
				
			||||||
 | 
					            $table->id();
 | 
				
			||||||
 | 
					            $table->string('uuid')->unique();
 | 
				
			||||||
 | 
					            $table->text('connection');
 | 
				
			||||||
 | 
					            $table->text('queue');
 | 
				
			||||||
 | 
					            $table->longText('payload');
 | 
				
			||||||
 | 
					            $table->longText('exception');
 | 
				
			||||||
 | 
					            $table->timestamp('failed_at')->useCurrent();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Reverse the migrations.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return void
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function down()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Schema::dropIfExists('failed_jobs');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10,15l5.88,0c0.27-0.31,0.67-0.5,1.12-0.5c0.83,0,1.5,0.67,1.5,1.5c0,0.83-0.67,1.5-1.5,1.5c-0.44,0-0.84-0.19-1.12-0.5 l-3.98,0c-0.46,2.28-2.48,4-4.9,4c-2.76,0-5-2.24-5-5c0-2.42,1.72-4.44,4-4.9l0,2.07C4.84,13.58,4,14.7,4,16c0,1.65,1.35,3,3,3 s3-1.35,3-3V15z M12.5,4c1.65,0,3,1.35,3,3h2c0-2.76-2.24-5-5-5l0,0c-2.76,0-5,2.24-5,5c0,1.43,0.6,2.71,1.55,3.62l-2.35,3.9 C6.02,14.66,5.5,15.27,5.5,16c0,0.83,0.67,1.5,1.5,1.5s1.5-0.67,1.5-1.5c0-0.16-0.02-0.31-0.07-0.45l3.38-5.63 C10.49,9.61,9.5,8.42,9.5,7C9.5,5.35,10.85,4,12.5,4z M17,13c-0.64,0-1.23,0.2-1.72,0.54l-3.05-5.07C11.53,8.35,11,7.74,11,7 c0-0.83,0.67-1.5,1.5-1.5S14,6.17,14,7c0,0.15-0.02,0.29-0.06,0.43l2.19,3.65C16.41,11.03,16.7,11,17,11l0,0c2.76,0,5,2.24,5,5 c0,2.76-2.24,5-5,5c-1.85,0-3.47-1.01-4.33-2.5l2.67,0C15.82,18.82,16.39,19,17,19c1.65,0,3-1.35,3-3S18.65,13,17,13z"/></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 903 B  | 
| 
						 | 
					@ -50,6 +50,7 @@ import templateManager from "./template-manager.js"
 | 
				
			||||||
import toggleSwitch from "./toggle-switch.js"
 | 
					import toggleSwitch from "./toggle-switch.js"
 | 
				
			||||||
import triLayout from "./tri-layout.js"
 | 
					import triLayout from "./tri-layout.js"
 | 
				
			||||||
import userSelect from "./user-select.js"
 | 
					import userSelect from "./user-select.js"
 | 
				
			||||||
 | 
					import webhookEvents from "./webhook-events";
 | 
				
			||||||
import wysiwygEditor from "./wysiwyg-editor.js"
 | 
					import wysiwygEditor from "./wysiwyg-editor.js"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const componentMapping = {
 | 
					const componentMapping = {
 | 
				
			||||||
| 
						 | 
					@ -105,6 +106,7 @@ const componentMapping = {
 | 
				
			||||||
    "toggle-switch": toggleSwitch,
 | 
					    "toggle-switch": toggleSwitch,
 | 
				
			||||||
    "tri-layout": triLayout,
 | 
					    "tri-layout": triLayout,
 | 
				
			||||||
    "user-select": userSelect,
 | 
					    "user-select": userSelect,
 | 
				
			||||||
 | 
					    "webhook-events": webhookEvents,
 | 
				
			||||||
    "wysiwyg-editor": wysiwygEditor,
 | 
					    "wysiwyg-editor": wysiwygEditor,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Webhook Events
 | 
				
			||||||
 | 
					 * Manages dynamic selection control in the webhook form interface.
 | 
				
			||||||
 | 
					 * @extends {Component}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class WebhookEvents {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setup() {
 | 
				
			||||||
 | 
					        this.checkboxes = this.$el.querySelectorAll('input[type="checkbox"]');
 | 
				
			||||||
 | 
					        this.allCheckbox = this.$el.querySelector('input[type="checkbox"][value="all"]');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.$el.addEventListener('change', event => {
 | 
				
			||||||
 | 
					            if (event.target.checked && event.target === this.allCheckbox) {
 | 
				
			||||||
 | 
					                this.deselectIndividualEvents();
 | 
				
			||||||
 | 
					            } else if (event.target.checked) {
 | 
				
			||||||
 | 
					                this.allCheckbox.checked = false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    deselectIndividualEvents() {
 | 
				
			||||||
 | 
					        for (const checkbox of this.checkboxes) {
 | 
				
			||||||
 | 
					            if (checkbox !== this.allCheckbox) {
 | 
				
			||||||
 | 
					                checkbox.checked = false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default WebhookEvents;
 | 
				
			||||||
| 
						 | 
					@ -7,41 +7,41 @@ return [
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Pages
 | 
					    // Pages
 | 
				
			||||||
    'page_create'                 => 'created page',
 | 
					    'page_create'                 => 'created page',
 | 
				
			||||||
    'page_create_notification'    => 'Page Successfully Created',
 | 
					    'page_create_notification'    => 'Page successfully created',
 | 
				
			||||||
    'page_update'                 => 'updated page',
 | 
					    'page_update'                 => 'updated page',
 | 
				
			||||||
    'page_update_notification'    => 'Page Successfully Updated',
 | 
					    'page_update_notification'    => 'Page successfully updated',
 | 
				
			||||||
    'page_delete'                 => 'deleted page',
 | 
					    'page_delete'                 => 'deleted page',
 | 
				
			||||||
    'page_delete_notification'    => 'Page Successfully Deleted',
 | 
					    'page_delete_notification'    => 'Page successfully deleted',
 | 
				
			||||||
    'page_restore'                => 'restored page',
 | 
					    'page_restore'                => 'restored page',
 | 
				
			||||||
    'page_restore_notification'   => 'Page Successfully Restored',
 | 
					    'page_restore_notification'   => 'Page successfully restored',
 | 
				
			||||||
    'page_move'                   => 'moved page',
 | 
					    'page_move'                   => 'moved page',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Chapters
 | 
					    // Chapters
 | 
				
			||||||
    'chapter_create'              => 'created chapter',
 | 
					    'chapter_create'              => 'created chapter',
 | 
				
			||||||
    'chapter_create_notification' => 'Chapter Successfully Created',
 | 
					    'chapter_create_notification' => 'Chapter successfully created',
 | 
				
			||||||
    'chapter_update'              => 'updated chapter',
 | 
					    'chapter_update'              => 'updated chapter',
 | 
				
			||||||
    'chapter_update_notification' => 'Chapter Successfully Updated',
 | 
					    'chapter_update_notification' => 'Chapter successfully updated',
 | 
				
			||||||
    'chapter_delete'              => 'deleted chapter',
 | 
					    'chapter_delete'              => 'deleted chapter',
 | 
				
			||||||
    'chapter_delete_notification' => 'Chapter Successfully Deleted',
 | 
					    'chapter_delete_notification' => 'Chapter successfully deleted',
 | 
				
			||||||
    'chapter_move'                => 'moved chapter',
 | 
					    'chapter_move'                => 'moved chapter',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Books
 | 
					    // Books
 | 
				
			||||||
    'book_create'                 => 'created book',
 | 
					    'book_create'                 => 'created book',
 | 
				
			||||||
    'book_create_notification'    => 'Book Successfully Created',
 | 
					    'book_create_notification'    => 'Book successfully created',
 | 
				
			||||||
    'book_update'                 => 'updated book',
 | 
					    'book_update'                 => 'updated book',
 | 
				
			||||||
    'book_update_notification'    => 'Book Successfully Updated',
 | 
					    'book_update_notification'    => 'Book successfully updated',
 | 
				
			||||||
    'book_delete'                 => 'deleted book',
 | 
					    'book_delete'                 => 'deleted book',
 | 
				
			||||||
    'book_delete_notification'    => 'Book Successfully Deleted',
 | 
					    'book_delete_notification'    => 'Book successfully deleted',
 | 
				
			||||||
    'book_sort'                   => 'sorted book',
 | 
					    'book_sort'                   => 'sorted book',
 | 
				
			||||||
    'book_sort_notification'      => 'Book Successfully Re-sorted',
 | 
					    'book_sort_notification'      => 'Book successfully re-sorted',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Bookshelves
 | 
					    // Bookshelves
 | 
				
			||||||
    'bookshelf_create'            => 'created Bookshelf',
 | 
					    'bookshelf_create'            => 'created bookshelf',
 | 
				
			||||||
    'bookshelf_create_notification'    => 'Bookshelf Successfully Created',
 | 
					    'bookshelf_create_notification'    => 'Bookshelf successfully created',
 | 
				
			||||||
    'bookshelf_update'                 => 'updated bookshelf',
 | 
					    'bookshelf_update'                 => 'updated bookshelf',
 | 
				
			||||||
    'bookshelf_update_notification'    => 'Bookshelf Successfully Updated',
 | 
					    'bookshelf_update_notification'    => 'Bookshelf successfully updated',
 | 
				
			||||||
    'bookshelf_delete'                 => 'deleted bookshelf',
 | 
					    'bookshelf_delete'                 => 'deleted bookshelf',
 | 
				
			||||||
    'bookshelf_delete_notification'    => 'Bookshelf Successfully Deleted',
 | 
					    'bookshelf_delete_notification'    => 'Bookshelf successfully deleted',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Favourites
 | 
					    // Favourites
 | 
				
			||||||
    'favourite_add_notification' => '":name" has been added to your favourites',
 | 
					    'favourite_add_notification' => '":name" has been added to your favourites',
 | 
				
			||||||
| 
						 | 
					@ -51,6 +51,14 @@ return [
 | 
				
			||||||
    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
 | 
					    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
 | 
				
			||||||
    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
 | 
					    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Webhooks
 | 
				
			||||||
 | 
					    'webhook_create' => 'created webhook',
 | 
				
			||||||
 | 
					    'webhook_create_notification' => 'Webhook successfully created',
 | 
				
			||||||
 | 
					    'webhook_update' => 'updated webhook',
 | 
				
			||||||
 | 
					    'webhook_update_notification' => 'Webhook successfully updated',
 | 
				
			||||||
 | 
					    'webhook_delete' => 'deleted webhook',
 | 
				
			||||||
 | 
					    'webhook_delete_notification' => 'Webhook successfully deleted',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Other
 | 
					    // Other
 | 
				
			||||||
    'commented_on'                => 'commented on',
 | 
					    'commented_on'                => 'commented on',
 | 
				
			||||||
    'permissions_update'          => 'updated permissions',
 | 
					    'permissions_update'          => 'updated permissions',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,7 @@ return [
 | 
				
			||||||
    'email' => 'Email',
 | 
					    'email' => 'Email',
 | 
				
			||||||
    'password' => 'Password',
 | 
					    'password' => 'Password',
 | 
				
			||||||
    'password_confirm' => 'Confirm Password',
 | 
					    'password_confirm' => 'Confirm Password',
 | 
				
			||||||
    'password_hint' => 'Must be over 7 characters',
 | 
					    'password_hint' => 'Must be at least 8 characters',
 | 
				
			||||||
    'forgot_password' => 'Forgot Password?',
 | 
					    'forgot_password' => 'Forgot Password?',
 | 
				
			||||||
    'remember_me' => 'Remember Me',
 | 
					    'remember_me' => 'Remember Me',
 | 
				
			||||||
    'ldap_email_hint' => 'Please enter an email to use for this account.',
 | 
					    'ldap_email_hint' => 'Please enter an email to use for this account.',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -71,6 +71,9 @@ return [
 | 
				
			||||||
    'list_view' => 'List View',
 | 
					    'list_view' => 'List View',
 | 
				
			||||||
    'default' => 'Default',
 | 
					    'default' => 'Default',
 | 
				
			||||||
    'breadcrumb' => 'Breadcrumb',
 | 
					    'breadcrumb' => 'Breadcrumb',
 | 
				
			||||||
 | 
					    'status' => 'Status',
 | 
				
			||||||
 | 
					    'status_active' => 'Active',
 | 
				
			||||||
 | 
					    'status_inactive' => 'Inactive',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Header
 | 
					    // Header
 | 
				
			||||||
    'header_menu_expand' => 'Expand Header Menu',
 | 
					    'header_menu_expand' => 'Expand Header Menu',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -143,6 +143,8 @@ return [
 | 
				
			||||||
    'books_sort_chapters_last' => 'Chapters Last',
 | 
					    'books_sort_chapters_last' => 'Chapters Last',
 | 
				
			||||||
    'books_sort_show_other' => 'Show Other Books',
 | 
					    'books_sort_show_other' => 'Show Other Books',
 | 
				
			||||||
    'books_sort_save' => 'Save New Order',
 | 
					    'books_sort_save' => 'Save New Order',
 | 
				
			||||||
 | 
					    'books_copy' => 'Copy Book',
 | 
				
			||||||
 | 
					    'books_copy_success' => 'Book successfully copied',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Chapters
 | 
					    // Chapters
 | 
				
			||||||
    'chapter' => 'Chapter',
 | 
					    'chapter' => 'Chapter',
 | 
				
			||||||
| 
						 | 
					@ -161,6 +163,8 @@ return [
 | 
				
			||||||
    'chapters_move' => 'Move Chapter',
 | 
					    'chapters_move' => 'Move Chapter',
 | 
				
			||||||
    'chapters_move_named' => 'Move Chapter :chapterName',
 | 
					    'chapters_move_named' => 'Move Chapter :chapterName',
 | 
				
			||||||
    'chapter_move_success' => 'Chapter moved to :bookName',
 | 
					    'chapter_move_success' => 'Chapter moved to :bookName',
 | 
				
			||||||
 | 
					    'chapters_copy' => 'Copy Chapter',
 | 
				
			||||||
 | 
					    'chapters_copy_success' => 'Chapter successfully copied',
 | 
				
			||||||
    'chapters_permissions' => 'Chapter Permissions',
 | 
					    'chapters_permissions' => 'Chapter Permissions',
 | 
				
			||||||
    'chapters_empty' => 'No pages are currently in this chapter.',
 | 
					    'chapters_empty' => 'No pages are currently in this chapter.',
 | 
				
			||||||
    'chapters_permissions_active' => 'Chapter Permissions Active',
 | 
					    'chapters_permissions_active' => 'Chapter Permissions Active',
 | 
				
			||||||
| 
						 | 
					@ -332,4 +336,12 @@ return [
 | 
				
			||||||
    'revision_restore_confirm' => 'Are you sure you want to restore this revision? The current page contents will be replaced.',
 | 
					    'revision_restore_confirm' => 'Are you sure you want to restore this revision? The current page contents will be replaced.',
 | 
				
			||||||
    'revision_delete_success' => 'Revision deleted',
 | 
					    'revision_delete_success' => 'Revision deleted',
 | 
				
			||||||
    'revision_cannot_delete_latest' => 'Cannot delete the latest revision.',
 | 
					    'revision_cannot_delete_latest' => 'Cannot delete the latest revision.',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Copy view
 | 
				
			||||||
 | 
					    'copy_consider' => 'Please consider the below when copying content.',
 | 
				
			||||||
 | 
					    'copy_consider_permissions' => 'Custom permission settings will not be copied.',
 | 
				
			||||||
 | 
					    'copy_consider_owner' => 'You will become the owner of all copied content.',
 | 
				
			||||||
 | 
					    'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',
 | 
				
			||||||
 | 
					    'copy_consider_attachments' => 'Page attachments will not be copied.',
 | 
				
			||||||
 | 
					    'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -174,7 +174,7 @@ return [
 | 
				
			||||||
    'users_role' => 'User Roles',
 | 
					    'users_role' => 'User Roles',
 | 
				
			||||||
    'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.',
 | 
					    'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.',
 | 
				
			||||||
    'users_password' => 'User Password',
 | 
					    'users_password' => 'User Password',
 | 
				
			||||||
    'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 6 characters long.',
 | 
					    'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',
 | 
				
			||||||
    'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.',
 | 
					    'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.',
 | 
				
			||||||
    'users_send_invite_option' => 'Send user invite email',
 | 
					    'users_send_invite_option' => 'Send user invite email',
 | 
				
			||||||
    'users_external_auth_id' => 'External Authentication ID',
 | 
					    'users_external_auth_id' => 'External Authentication ID',
 | 
				
			||||||
| 
						 | 
					@ -233,6 +233,28 @@ return [
 | 
				
			||||||
    'user_api_token_delete_confirm' => 'Are you sure you want to delete this API token?',
 | 
					    'user_api_token_delete_confirm' => 'Are you sure you want to delete this API token?',
 | 
				
			||||||
    'user_api_token_delete_success' => 'API token successfully deleted',
 | 
					    'user_api_token_delete_success' => 'API token successfully deleted',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Webhooks
 | 
				
			||||||
 | 
					    'webhooks' => 'Webhooks',
 | 
				
			||||||
 | 
					    'webhooks_create' => 'Create New Webhook',
 | 
				
			||||||
 | 
					    'webhooks_none_created' => 'No webhooks have yet been created.',
 | 
				
			||||||
 | 
					    'webhooks_edit' => 'Edit Webhook',
 | 
				
			||||||
 | 
					    'webhooks_save' => 'Save Webhook',
 | 
				
			||||||
 | 
					    'webhooks_details' => 'Webhook Details',
 | 
				
			||||||
 | 
					    'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',
 | 
				
			||||||
 | 
					    'webhooks_events' => 'Webhook Events',
 | 
				
			||||||
 | 
					    'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',
 | 
				
			||||||
 | 
					    'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\'t expose confidential content.',
 | 
				
			||||||
 | 
					    'webhooks_events_all' => 'All system events',
 | 
				
			||||||
 | 
					    'webhooks_name' => 'Webhook Name',
 | 
				
			||||||
 | 
					    'webhooks_endpoint' => 'Webhook Endpoint',
 | 
				
			||||||
 | 
					    'webhooks_active' => 'Webhook Active',
 | 
				
			||||||
 | 
					    'webhook_events_table_header' => 'Events',
 | 
				
			||||||
 | 
					    'webhooks_delete' => 'Delete Webhook',
 | 
				
			||||||
 | 
					    'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \':webhookName\', from the system.',
 | 
				
			||||||
 | 
					    'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?',
 | 
				
			||||||
 | 
					    'webhooks_format_example' => 'Webhook Format Example',
 | 
				
			||||||
 | 
					    'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The "related_item" and "url" properties are optional and will depend on the type of event triggered.',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    //! If editing translations files directly please ignore this in all
 | 
					    //! If editing translations files directly please ignore this in all
 | 
				
			||||||
    //! languages apart from en. Content will be auto-copied from en.
 | 
					    //! languages apart from en. Content will be auto-copied from en.
 | 
				
			||||||
    //!////////////////////////////////
 | 
					    //!////////////////////////////////
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,27 @@
 | 
				
			||||||
<h1 class="list-heading text-capitals mb-l">Getting Started</h1>
 | 
					<h1 class="list-heading text-capitals mb-l">Getting Started</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p class="mb-none">
 | 
				
			||||||
 | 
					    This documentation covers use of the REST API. <br>
 | 
				
			||||||
 | 
					    Some alternative options for extension and customization can be found below:
 | 
				
			||||||
 | 
					</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<ul>
 | 
				
			||||||
 | 
					    <li>
 | 
				
			||||||
 | 
					        <a href="{{ url('/settings/webhooks') }}" target="_blank" rel="noopener noreferrer">Webhooks</a> -
 | 
				
			||||||
 | 
					        HTTP POST calls upon events occurring in BookStack.
 | 
				
			||||||
 | 
					    </li>
 | 
				
			||||||
 | 
					    <li>
 | 
				
			||||||
 | 
					        <a href="https://github.com/BookStackApp/BookStack/blob/master/dev/docs/visual-theme-system.md" target="_blank" rel="noopener noreferrer">Visual Theme System</a> -
 | 
				
			||||||
 | 
					        Methods to override views, translations and icons within BookStack.
 | 
				
			||||||
 | 
					    </li>
 | 
				
			||||||
 | 
					    <li>
 | 
				
			||||||
 | 
					        <a href="https://github.com/BookStackApp/BookStack/blob/master/dev/docs/logical-theme-system.md" target="_blank" rel="noopener noreferrer">Logical Theme System</a> -
 | 
				
			||||||
 | 
					        Methods to extend back-end functionality within BookStack.
 | 
				
			||||||
 | 
					    </li>
 | 
				
			||||||
 | 
					</ul>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<hr>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<h5 id="authentication" class="text-mono mb-m">Authentication</h5>
 | 
					<h5 id="authentication" class="text-mono mb-m">Authentication</h5>
 | 
				
			||||||
<p>
 | 
					<p>
 | 
				
			||||||
    To access the API a user has to have the <em>"Access System API"</em> permission enabled on one of their assigned roles.
 | 
					    To access the API a user has to have the <em>"Access System API"</em> permission enabled on one of their assigned roles.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,40 @@
 | 
				
			||||||
 | 
					@extends('layouts.simple')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@section('body')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="container small">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="my-s">
 | 
				
			||||||
 | 
					            @include('entities.breadcrumbs', ['crumbs' => [
 | 
				
			||||||
 | 
					                $book,
 | 
				
			||||||
 | 
					                $book->getUrl('/copy') => [
 | 
				
			||||||
 | 
					                    'text' => trans('entities.books_copy'),
 | 
				
			||||||
 | 
					                    'icon' => 'copy',
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            ]])
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="card content-wrap auto-height">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <h1 class="list-heading">{{ trans('entities.books_copy') }}</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <form action="{{ $book->getUrl('/copy') }}" method="POST">
 | 
				
			||||||
 | 
					                {!! csrf_field() !!}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <div class="form-group title-input">
 | 
				
			||||||
 | 
					                    <label for="name">{{ trans('common.name') }}</label>
 | 
				
			||||||
 | 
					                    @include('form.text', ['name' => 'name'])
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                @include('entities.copy-considerations')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <div class="form-group text-right">
 | 
				
			||||||
 | 
					                    <a href="{{ $book->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
 | 
				
			||||||
 | 
					                    <button type="submit" class="button">{{ trans('entities.books_copy') }}</button>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@stop
 | 
				
			||||||
| 
						 | 
					@ -110,6 +110,12 @@
 | 
				
			||||||
                    <span>{{ trans('common.sort') }}</span>
 | 
					                    <span>{{ trans('common.sort') }}</span>
 | 
				
			||||||
                </a>
 | 
					                </a>
 | 
				
			||||||
            @endif
 | 
					            @endif
 | 
				
			||||||
 | 
					            @if(userCan('book-create-all'))
 | 
				
			||||||
 | 
					                <a href="{{ $book->getUrl('/copy') }}" class="icon-list-item">
 | 
				
			||||||
 | 
					                    <span>@icon('copy')</span>
 | 
				
			||||||
 | 
					                    <span>{{ trans('common.copy') }}</span>
 | 
				
			||||||
 | 
					                </a>
 | 
				
			||||||
 | 
					            @endif
 | 
				
			||||||
            @if(userCan('restrictions-manage', $book))
 | 
					            @if(userCan('restrictions-manage', $book))
 | 
				
			||||||
                <a href="{{ $book->getUrl('/permissions') }}" class="icon-list-item">
 | 
					                <a href="{{ $book->getUrl('/permissions') }}" class="icon-list-item">
 | 
				
			||||||
                    <span>@icon('lock')</span>
 | 
					                    <span>@icon('lock')</span>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,50 @@
 | 
				
			||||||
 | 
					@extends('layouts.simple')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@section('body')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="container small">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="my-s">
 | 
				
			||||||
 | 
					            @include('entities.breadcrumbs', ['crumbs' => [
 | 
				
			||||||
 | 
					                $chapter->book,
 | 
				
			||||||
 | 
					                $chapter,
 | 
				
			||||||
 | 
					                $chapter->getUrl('/copy') => [
 | 
				
			||||||
 | 
					                    'text' => trans('entities.chapters_copy'),
 | 
				
			||||||
 | 
					                    'icon' => 'copy',
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            ]])
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="card content-wrap auto-height">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <h1 class="list-heading">{{ trans('entities.chapters_copy') }}</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <form action="{{ $chapter->getUrl('/copy') }}" method="POST">
 | 
				
			||||||
 | 
					                {!! csrf_field() !!}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <div class="form-group title-input">
 | 
				
			||||||
 | 
					                    <label for="name">{{ trans('common.name') }}</label>
 | 
				
			||||||
 | 
					                    @include('form.text', ['name' => 'name'])
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <div class="form-group" collapsible>
 | 
				
			||||||
 | 
					                    <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
 | 
				
			||||||
 | 
					                        <label for="entity_selection">{{ trans('entities.pages_copy_desination') }}</label>
 | 
				
			||||||
 | 
					                    </button>
 | 
				
			||||||
 | 
					                    <div class="collapse-content" collapsible-content>
 | 
				
			||||||
 | 
					                        @include('entities.selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book', 'entityPermission' => 'chapter-create'])
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                @include('entities.copy-considerations')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <div class="form-group text-right">
 | 
				
			||||||
 | 
					                    <a href="{{ $chapter->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
 | 
				
			||||||
 | 
					                    <button type="submit" class="button">{{ trans('entities.chapters_copy') }}</button>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@stop
 | 
				
			||||||
| 
						 | 
					@ -108,6 +108,12 @@
 | 
				
			||||||
                    <span>{{ trans('common.edit') }}</span>
 | 
					                    <span>{{ trans('common.edit') }}</span>
 | 
				
			||||||
                </a>
 | 
					                </a>
 | 
				
			||||||
            @endif
 | 
					            @endif
 | 
				
			||||||
 | 
					            @if(userCanOnAny('chapter-create'))
 | 
				
			||||||
 | 
					                <a href="{{ $chapter->getUrl('/copy') }}" class="icon-list-item">
 | 
				
			||||||
 | 
					                    <span>@icon('copy')</span>
 | 
				
			||||||
 | 
					                    <span>{{ trans('common.copy') }}</span>
 | 
				
			||||||
 | 
					                </a>
 | 
				
			||||||
 | 
					            @endif
 | 
				
			||||||
            @if(userCan('chapter-update', $chapter) && userCan('chapter-delete', $chapter))
 | 
					            @if(userCan('chapter-update', $chapter) && userCan('chapter-delete', $chapter))
 | 
				
			||||||
                <a href="{{ $chapter->getUrl('/move') }}" class="icon-list-item">
 | 
					                <a href="{{ $chapter->getUrl('/move') }}" class="icon-list-item">
 | 
				
			||||||
                    <span>@icon('folder')</span>
 | 
					                    <span>@icon('folder')</span>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,8 +24,6 @@
 | 
				
			||||||
        "{{ $activity->entity->name }}"
 | 
					        "{{ $activity->entity->name }}"
 | 
				
			||||||
    @endif
 | 
					    @endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @if($activity->extra) "{{ $activity->extra }}" @endif
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <br>
 | 
					    <br>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <span class="text-muted"><small>@icon('time'){{ $activity->created_at->diffForHumans() }}</small></span>
 | 
					    <span class="text-muted"><small>@icon('time'){{ $activity->created_at->diffForHumans() }}</small></span>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,15 @@
 | 
				
			||||||
 | 
					<p class="text-warn mb-none mt-l">
 | 
				
			||||||
 | 
					    @icon('warning') <strong>{{ trans('entities.copy_consider') }}</strong>
 | 
				
			||||||
 | 
					</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="grid half no-gap no-row-gap text-warn mb-m">
 | 
				
			||||||
 | 
					    <ul class="pr-s mb-none">
 | 
				
			||||||
 | 
					        <li>{{ trans('entities.copy_consider_permissions') }}</li>
 | 
				
			||||||
 | 
					        <li>{{ trans('entities.copy_consider_owner') }}</li>
 | 
				
			||||||
 | 
					        <li>{{ trans('entities.copy_consider_images') }}</li>
 | 
				
			||||||
 | 
					    </ul>
 | 
				
			||||||
 | 
					    <ul class="pr-s mb-none">
 | 
				
			||||||
 | 
					        <li>{{ trans('entities.copy_consider_attachments') }}</li>
 | 
				
			||||||
 | 
					        <li>{{ trans('entities.copy_consider_access') }}</li>
 | 
				
			||||||
 | 
					    </ul>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					@if($errors->has($name))
 | 
				
			||||||
 | 
					    <div class="text-neg text-small">{{ $errors->first($name) }}</div>
 | 
				
			||||||
 | 
					@endif
 | 
				
			||||||
| 
						 | 
					@ -37,6 +37,8 @@
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                @include('entities.copy-considerations')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <div class="form-group text-right">
 | 
					                <div class="form-group text-right">
 | 
				
			||||||
                    <a href="{{ $page->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
 | 
					                    <a href="{{ $page->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
 | 
				
			||||||
                    <button type="submit" class="button">{{ trans('entities.pages_copy') }}</button>
 | 
					                    <button type="submit" class="button">{{ trans('entities.pages_copy') }}</button>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,7 @@
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="card content-wrap auto-height">
 | 
					    <div class="card content-wrap auto-height">
 | 
				
			||||||
        <h2 class="list-heading">{{ trans('settings.audit') }}</h2>
 | 
					        <h1 class="list-heading">{{ trans('settings.audit') }}</h1>
 | 
				
			||||||
        <p class="text-muted">{{ trans('settings.audit_desc') }}</p>
 | 
					        <p class="text-muted">{{ trans('settings.audit_desc') }}</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div class="flex-container-row">
 | 
					        <div class="flex-container-row">
 | 
				
			||||||
| 
						 | 
					@ -41,12 +41,19 @@
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                @endforeach
 | 
					                @endforeach
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <div class="form-group ml-auto"
 | 
					                <div class="form-group ml-auto mr-m"
 | 
				
			||||||
                     component="submit-on-change"
 | 
					                     component="submit-on-change"
 | 
				
			||||||
                     option:submit-on-change:filter='[name="user"]'>
 | 
					                     option:submit-on-change:filter='[name="user"]'>
 | 
				
			||||||
                    <label for="owner">{{ trans('settings.audit_table_user') }}</label>
 | 
					                    <label for="owner">{{ trans('settings.audit_table_user') }}</label>
 | 
				
			||||||
                    @include('form.user-select', ['user' => $listDetails['user'] ? \BookStack\Auth\User::query()->find($listDetails['user']) : null, 'name' => 'user', 'compact' =>  true])
 | 
					                    @include('form.user-select', ['user' => $listDetails['user'] ? \BookStack\Auth\User::query()->find($listDetails['user']) : null, 'name' => 'user', 'compact' =>  true])
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <div class="form-group ml-auto">
 | 
				
			||||||
 | 
					                    <label for="ip">{{ trans('settings.audit_table_ip') }}</label>
 | 
				
			||||||
 | 
					                    @include('form.text', ['name' => 'ip', 'model' => (object) $listDetails])
 | 
				
			||||||
 | 
					                    <input type="submit" style="display: none">
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
            </form>
 | 
					            </form>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,10 +6,12 @@ $version - Version of bookstack to display
 | 
				
			||||||
    <div class="py-m flex fit-content">
 | 
					    <div class="py-m flex fit-content">
 | 
				
			||||||
        @include('settings.parts.navbar', ['selected' => $selected])
 | 
					        @include('settings.parts.navbar', ['selected' => $selected])
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="flex"></div>
 | 
					</div>
 | 
				
			||||||
    <div class="text-right p-m flex fit-content">
 | 
					<div class="px-s">
 | 
				
			||||||
        <a target="_blank" rel="noopener noreferrer" href="https://github.com/BookStackApp/BookStack/releases">
 | 
					    <hr class="darker m-none">
 | 
				
			||||||
            BookStack @if(strpos($version, 'v') !== 0) version @endif {{ $version }}
 | 
					</div>
 | 
				
			||||||
        </a>
 | 
					<div class="py-l px-m flex fit-content">
 | 
				
			||||||
    </div>
 | 
					    <a target="_blank" rel="noopener noreferrer" href="https://github.com/BookStackApp/BookStack/releases">
 | 
				
			||||||
 | 
					        BookStack @if(strpos($version, 'v') !== 0) version @endif {{ $version }}
 | 
				
			||||||
 | 
					    </a>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -13,4 +13,7 @@
 | 
				
			||||||
    @if(userCan('user-roles-manage'))
 | 
					    @if(userCan('user-roles-manage'))
 | 
				
			||||||
        <a href="{{ url('/settings/roles') }}" @if($selected == 'roles') class="active" @endif>@icon('lock-open'){{ trans('settings.roles') }}</a>
 | 
					        <a href="{{ url('/settings/roles') }}" @if($selected == 'roles') class="active" @endif>@icon('lock-open'){{ trans('settings.roles') }}</a>
 | 
				
			||||||
    @endif
 | 
					    @endif
 | 
				
			||||||
 | 
					    @if(userCan('settings-manage'))
 | 
				
			||||||
 | 
					        <a href="{{ url('/settings/webhooks') }}" @if($selected == 'webhooks') class="active" @endif>@icon('webhooks'){{ trans('settings.webhooks') }}</a>
 | 
				
			||||||
 | 
					    @endif
 | 
				
			||||||
</nav>
 | 
					</nav>
 | 
				
			||||||
| 
						 | 
					@ -8,9 +8,21 @@
 | 
				
			||||||
            @include('settings.parts.navbar', ['selected' => 'roles'])
 | 
					            @include('settings.parts.navbar', ['selected' => 'roles'])
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <form action="{{ url("/settings/roles/new") }}" method="POST">
 | 
					        <div class="card content-wrap">
 | 
				
			||||||
            @include('settings.roles.parts.form', ['title' => trans('settings.role_create')])
 | 
					            <h1 class="list-heading">{{ trans('settings.role_create') }}</h1>
 | 
				
			||||||
        </form>
 | 
					
 | 
				
			||||||
 | 
					            <form action="{{ url("/settings/roles/new") }}" method="POST">
 | 
				
			||||||
 | 
					                {{ csrf_field() }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                @include('settings.roles.parts.form', ['role' => $role ?? null])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <div class="form-group text-right">
 | 
				
			||||||
 | 
					                    <a href="{{ url("/settings/roles") }}" class="button outline">{{ trans('common.cancel') }}</a>
 | 
				
			||||||
 | 
					                    <button type="submit" class="button">{{ trans('settings.role_save') }}</button>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@stop
 | 
					@stop
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,10 +7,53 @@
 | 
				
			||||||
            @include('settings.parts.navbar', ['selected' => 'roles'])
 | 
					            @include('settings.parts.navbar', ['selected' => 'roles'])
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <form action="{{ url("/settings/roles/{$role->id}") }}" method="POST">
 | 
					        <div class="card content-wrap">
 | 
				
			||||||
            <input type="hidden" name="_method" value="PUT">
 | 
					            <h1 class="list-heading">{{ trans('settings.role_edit') }}</h1>
 | 
				
			||||||
            @include('settings.roles.parts.form', ['model' => $role, 'title' => trans('settings.role_edit'), 'icon' => 'edit'])
 | 
					
 | 
				
			||||||
        </form>
 | 
					            <form action="{{ url("/settings/roles/{$role->id}") }}" method="POST">
 | 
				
			||||||
 | 
					                {{ csrf_field() }}
 | 
				
			||||||
 | 
					                {{ method_field('PUT') }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                @include('settings.roles.parts.form', ['role' => $role])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <div class="form-group text-right">
 | 
				
			||||||
 | 
					                    <a href="{{ url("/settings/roles") }}" class="button outline">{{ trans('common.cancel') }}</a>
 | 
				
			||||||
 | 
					                    <a href="{{ url("/settings/roles/new?copy_from={$role->id}") }}" class="button outline">{{ trans('common.copy') }}</a>
 | 
				
			||||||
 | 
					                    <a href="{{ url("/settings/roles/delete/{$role->id}") }}" class="button outline">{{ trans('settings.role_delete') }}</a>
 | 
				
			||||||
 | 
					                    <button type="submit" class="button">{{ trans('settings.role_save') }}</button>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="card content-wrap auto-height">
 | 
				
			||||||
 | 
					            <h2 class="list-heading">{{ trans('settings.role_users') }}</h2>
 | 
				
			||||||
 | 
					            @if(count($role->users ?? []) > 0)
 | 
				
			||||||
 | 
					                <div class="grid third">
 | 
				
			||||||
 | 
					                    @foreach($role->users as $user)
 | 
				
			||||||
 | 
					                        <div class="user-list-item">
 | 
				
			||||||
 | 
					                            <div>
 | 
				
			||||||
 | 
					                                <img class="avatar small" src="{{ $user->getAvatar(40) }}" alt="{{ $user->name }}">
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                            <div>
 | 
				
			||||||
 | 
					                                @if(userCan('users-manage') || user()->id == $user->id)
 | 
				
			||||||
 | 
					                                    <a href="{{ url("/settings/users/{$user->id}") }}">
 | 
				
			||||||
 | 
					                                        @endif
 | 
				
			||||||
 | 
					                                        {{ $user->name }}
 | 
				
			||||||
 | 
					                                        @if(userCan('users-manage') || user()->id == $user->id)
 | 
				
			||||||
 | 
					                                    </a>
 | 
				
			||||||
 | 
					                                @endif
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    @endforeach
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            @else
 | 
				
			||||||
 | 
					                <p class="text-muted">
 | 
				
			||||||
 | 
					                    {{ trans('settings.role_users_none') }}
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
 | 
					            @endif
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@stop
 | 
					@stop
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,267 +1,224 @@
 | 
				
			||||||
{!! csrf_field() !!}
 | 
					<div class="setting-list">
 | 
				
			||||||
 | 
					 | 
				
			||||||
<div class="card content-wrap">
 | 
					 | 
				
			||||||
    <h1 class="list-heading">{{ $title }}</h1>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="setting-list">
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div class="grid half">
 | 
					 | 
				
			||||||
            <div>
 | 
					 | 
				
			||||||
                <label class="setting-list-label">{{ trans('settings.role_details') }}</label>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            <div>
 | 
					 | 
				
			||||||
                <div class="form-group">
 | 
					 | 
				
			||||||
                    <label for="display_name">{{ trans('settings.role_name') }}</label>
 | 
					 | 
				
			||||||
                    @include('form.text', ['name' => 'display_name'])
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
                <div class="form-group">
 | 
					 | 
				
			||||||
                    <label for="description">{{ trans('settings.role_desc') }}</label>
 | 
					 | 
				
			||||||
                    @include('form.text', ['name' => 'description'])
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
                <div class="form-group">
 | 
					 | 
				
			||||||
                    @include('form.checkbox', ['name' => 'mfa_enforced', 'label' => trans('settings.role_mfa_enforced') ])
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                @if(in_array(config('auth.method'), ['ldap', 'saml2', 'oidc']))
 | 
					 | 
				
			||||||
                    <div class="form-group">
 | 
					 | 
				
			||||||
                        <label for="name">{{ trans('settings.role_external_auth_id') }}</label>
 | 
					 | 
				
			||||||
                        @include('form.text', ['name' => 'external_auth_id'])
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                @endif
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div permissions-table>
 | 
					 | 
				
			||||||
            <label class="setting-list-label">{{ trans('settings.role_system') }}</label>
 | 
					 | 
				
			||||||
            <a href="#" permissions-table-toggle-all class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <div class="toggle-switch-list grid half mt-m">
 | 
					 | 
				
			||||||
                <div>
 | 
					 | 
				
			||||||
                    <div>@include('settings.roles.parts.checkbox', ['permission' => 'restrictions-manage-all', 'label' => trans('settings.role_manage_entity_permissions')])</div>
 | 
					 | 
				
			||||||
                    <div>@include('settings.roles.parts.checkbox', ['permission' => 'restrictions-manage-own', 'label' => trans('settings.role_manage_own_entity_permissions')])</div>
 | 
					 | 
				
			||||||
                    <div>@include('settings.roles.parts.checkbox', ['permission' => 'templates-manage', 'label' => trans('settings.role_manage_page_templates')])</div>
 | 
					 | 
				
			||||||
                    <div>@include('settings.roles.parts.checkbox', ['permission' => 'access-api', 'label' => trans('settings.role_access_api')])</div>
 | 
					 | 
				
			||||||
                    <div>@include('settings.roles.parts.checkbox', ['permission' => 'content-export', 'label' => trans('settings.role_export_content')])</div>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
                <div>
 | 
					 | 
				
			||||||
                    <div>@include('settings.roles.parts.checkbox', ['permission' => 'settings-manage', 'label' => trans('settings.role_manage_settings')])</div>
 | 
					 | 
				
			||||||
                    <div>@include('settings.roles.parts.checkbox', ['permission' => 'users-manage', 'label' => trans('settings.role_manage_users')])</div>
 | 
					 | 
				
			||||||
                    <div>@include('settings.roles.parts.checkbox', ['permission' => 'user-roles-manage', 'label' => trans('settings.role_manage_roles')])</div>
 | 
					 | 
				
			||||||
                    <p class="text-warn text-small mt-s mb-none">{{ trans('settings.roles_system_warning') }}</p>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="grid half">
 | 
				
			||||||
        <div>
 | 
					        <div>
 | 
				
			||||||
            <label class="setting-list-label">{{ trans('settings.role_asset') }}</label>
 | 
					            <label class="setting-list-label">{{ trans('settings.role_details') }}</label>
 | 
				
			||||||
            <p>{{ trans('settings.role_asset_desc') }}</p>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            @if (isset($role) && $role->system_name === 'admin')
 | 
					 | 
				
			||||||
                <p class="text-warn">{{ trans('settings.role_asset_admins') }}</p>
 | 
					 | 
				
			||||||
            @endif
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <table permissions-table class="table toggle-switch-list compact permissions-table">
 | 
					 | 
				
			||||||
                <tr>
 | 
					 | 
				
			||||||
                    <th width="20%">
 | 
					 | 
				
			||||||
                        <a href="#" permissions-table-toggle-all class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
					 | 
				
			||||||
                    </th>
 | 
					 | 
				
			||||||
                    <th width="20%" permissions-table-toggle-all-in-column>{{ trans('common.create') }}</th>
 | 
					 | 
				
			||||||
                    <th width="20%" permissions-table-toggle-all-in-column>{{ trans('common.view') }}</th>
 | 
					 | 
				
			||||||
                    <th width="20%" permissions-table-toggle-all-in-column>{{ trans('common.edit') }}</th>
 | 
					 | 
				
			||||||
                    <th width="20%" permissions-table-toggle-all-in-column>{{ trans('common.delete') }}</th>
 | 
					 | 
				
			||||||
                </tr>
 | 
					 | 
				
			||||||
                <tr>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        <div>{{ trans('entities.shelves_long') }}</div>
 | 
					 | 
				
			||||||
                        <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-create-all', 'label' => trans('settings.role_all')])
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-view-own', 'label' => trans('settings.role_own')])
 | 
					 | 
				
			||||||
                        <br>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-view-all', 'label' => trans('settings.role_all')])
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-update-own', 'label' => trans('settings.role_own')])
 | 
					 | 
				
			||||||
                        <br>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-update-all', 'label' => trans('settings.role_all')])
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-delete-own', 'label' => trans('settings.role_own')])
 | 
					 | 
				
			||||||
                        <br>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-delete-all', 'label' => trans('settings.role_all')])
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                </tr>
 | 
					 | 
				
			||||||
                <tr>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        <div>{{ trans('entities.books') }}</div>
 | 
					 | 
				
			||||||
                        <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'book-create-all', 'label' => trans('settings.role_all')])
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'book-view-own', 'label' => trans('settings.role_own')])
 | 
					 | 
				
			||||||
                        <br>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'book-view-all', 'label' => trans('settings.role_all')])
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'book-update-own', 'label' => trans('settings.role_own')])
 | 
					 | 
				
			||||||
                        <br>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'book-update-all', 'label' => trans('settings.role_all')])
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'book-delete-own', 'label' => trans('settings.role_own')])
 | 
					 | 
				
			||||||
                        <br>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'book-delete-all', 'label' => trans('settings.role_all')])
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                </tr>
 | 
					 | 
				
			||||||
                <tr>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        <div>{{ trans('entities.chapters') }}</div>
 | 
					 | 
				
			||||||
                        <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'chapter-create-own', 'label' => trans('settings.role_own')])
 | 
					 | 
				
			||||||
                        <br>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'chapter-create-all', 'label' => trans('settings.role_all')])
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'chapter-view-own', 'label' => trans('settings.role_own')])
 | 
					 | 
				
			||||||
                        <br>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'chapter-view-all', 'label' => trans('settings.role_all')])
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'chapter-update-own', 'label' => trans('settings.role_own')])
 | 
					 | 
				
			||||||
                        <br>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'chapter-update-all', 'label' => trans('settings.role_all')])
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'chapter-delete-own', 'label' => trans('settings.role_own')])
 | 
					 | 
				
			||||||
                        <br>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'chapter-delete-all', 'label' => trans('settings.role_all')])
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                </tr>
 | 
					 | 
				
			||||||
                <tr>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        <div>{{ trans('entities.pages') }}</div>
 | 
					 | 
				
			||||||
                        <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'page-create-own', 'label' => trans('settings.role_own')])
 | 
					 | 
				
			||||||
                        <br>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'page-create-all', 'label' => trans('settings.role_all')])
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'page-view-own', 'label' => trans('settings.role_own')])
 | 
					 | 
				
			||||||
                        <br>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'page-view-all', 'label' => trans('settings.role_all')])
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'page-update-own', 'label' => trans('settings.role_own')])
 | 
					 | 
				
			||||||
                        <br>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'page-update-all', 'label' => trans('settings.role_all')])
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'page-delete-own', 'label' => trans('settings.role_own')])
 | 
					 | 
				
			||||||
                        <br>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'page-delete-all', 'label' => trans('settings.role_all')])
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                </tr>
 | 
					 | 
				
			||||||
                <tr>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        <div>{{ trans('entities.images') }}</div>
 | 
					 | 
				
			||||||
                        <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                    <td>@include('settings.roles.parts.checkbox', ['permission' => 'image-create-all', 'label' => ''])</td>
 | 
					 | 
				
			||||||
                    <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'image-update-own', 'label' => trans('settings.role_own')])
 | 
					 | 
				
			||||||
                        <br>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'image-update-all', 'label' => trans('settings.role_all')])
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'image-delete-own', 'label' => trans('settings.role_own')])
 | 
					 | 
				
			||||||
                        <br>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'image-delete-all', 'label' => trans('settings.role_all')])
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                </tr>
 | 
					 | 
				
			||||||
                <tr>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        <div>{{ trans('entities.attachments') }}</div>
 | 
					 | 
				
			||||||
                        <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                    <td>@include('settings.roles.parts.checkbox', ['permission' => 'attachment-create-all', 'label' => ''])</td>
 | 
					 | 
				
			||||||
                    <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'attachment-update-own', 'label' => trans('settings.role_own')])
 | 
					 | 
				
			||||||
                        <br>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'attachment-update-all', 'label' => trans('settings.role_all')])
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'attachment-delete-own', 'label' => trans('settings.role_own')])
 | 
					 | 
				
			||||||
                        <br>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'attachment-delete-all', 'label' => trans('settings.role_all')])
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                </tr>
 | 
					 | 
				
			||||||
                <tr>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        <div>{{ trans('entities.comments') }}</div>
 | 
					 | 
				
			||||||
                        <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                    <td>@include('settings.roles.parts.checkbox', ['permission' => 'comment-create-all', 'label' => ''])</td>
 | 
					 | 
				
			||||||
                    <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'comment-update-own', 'label' => trans('settings.role_own')])
 | 
					 | 
				
			||||||
                        <br>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'comment-update-all', 'label' => trans('settings.role_all')])
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                    <td>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'comment-delete-own', 'label' => trans('settings.role_own')])
 | 
					 | 
				
			||||||
                        <br>
 | 
					 | 
				
			||||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'comment-delete-all', 'label' => trans('settings.role_all')])
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                </tr>
 | 
					 | 
				
			||||||
            </table>
 | 
					 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </div>
 | 
					        <div>
 | 
				
			||||||
 | 
					            <div class="form-group">
 | 
				
			||||||
 | 
					                <label for="display_name">{{ trans('settings.role_name') }}</label>
 | 
				
			||||||
 | 
					                @include('form.text', ['name' => 'display_name', 'model' => $role])
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="form-group">
 | 
				
			||||||
 | 
					                <label for="description">{{ trans('settings.role_desc') }}</label>
 | 
				
			||||||
 | 
					                @include('form.text', ['name' => 'description', 'model' => $role])
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="form-group">
 | 
				
			||||||
 | 
					                @include('form.checkbox', ['name' => 'mfa_enforced', 'label' => trans('settings.role_mfa_enforced'), 'model' => $role ])
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="form-group text-right">
 | 
					            @if(in_array(config('auth.method'), ['ldap', 'saml2', 'oidc']))
 | 
				
			||||||
        <a href="{{ url("/settings/roles") }}" class="button outline">{{ trans('common.cancel') }}</a>
 | 
					                <div class="form-group">
 | 
				
			||||||
        @if (isset($role) && $role->id)
 | 
					                    <label for="name">{{ trans('settings.role_external_auth_id') }}</label>
 | 
				
			||||||
            <a href="{{ url("/settings/roles/delete/{$role->id}") }}" class="button outline">{{ trans('settings.role_delete') }}</a>
 | 
					                    @include('form.text', ['name' => 'external_auth_id', 'model' => $role])
 | 
				
			||||||
        @endif
 | 
					 | 
				
			||||||
        <button type="submit" class="button">{{ trans('settings.role_save') }}</button>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div class="card content-wrap auto-height">
 | 
					 | 
				
			||||||
    <h2 class="list-heading">{{ trans('settings.role_users') }}</h2>
 | 
					 | 
				
			||||||
    @if(count($role->users ?? []) > 0)
 | 
					 | 
				
			||||||
        <div class="grid third">
 | 
					 | 
				
			||||||
            @foreach($role->users as $user)
 | 
					 | 
				
			||||||
                <div class="user-list-item">
 | 
					 | 
				
			||||||
                    <div>
 | 
					 | 
				
			||||||
                        <img class="avatar small" src="{{ $user->getAvatar(40) }}" alt="{{ $user->name }}">
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                    <div>
 | 
					 | 
				
			||||||
                        @if(userCan('users-manage') || user()->id == $user->id)
 | 
					 | 
				
			||||||
                            <a href="{{ url("/settings/users/{$user->id}") }}">
 | 
					 | 
				
			||||||
                                @endif
 | 
					 | 
				
			||||||
                                {{ $user->name }}
 | 
					 | 
				
			||||||
                                @if(userCan('users-manage') || user()->id == $user->id)
 | 
					 | 
				
			||||||
                            </a>
 | 
					 | 
				
			||||||
                        @endif
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            @endforeach
 | 
					            @endif
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    @else
 | 
					    </div>
 | 
				
			||||||
        <p class="text-muted">
 | 
					
 | 
				
			||||||
            {{ trans('settings.role_users_none') }}
 | 
					    <div permissions-table>
 | 
				
			||||||
        </p>
 | 
					        <label class="setting-list-label">{{ trans('settings.role_system') }}</label>
 | 
				
			||||||
    @endif
 | 
					        <a href="#" permissions-table-toggle-all class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
				
			||||||
</div>
 | 
					
 | 
				
			||||||
 | 
					        <div class="toggle-switch-list grid half mt-m">
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					                <div>@include('settings.roles.parts.checkbox', ['permission' => 'restrictions-manage-all', 'label' => trans('settings.role_manage_entity_permissions')])</div>
 | 
				
			||||||
 | 
					                <div>@include('settings.roles.parts.checkbox', ['permission' => 'restrictions-manage-own', 'label' => trans('settings.role_manage_own_entity_permissions')])</div>
 | 
				
			||||||
 | 
					                <div>@include('settings.roles.parts.checkbox', ['permission' => 'templates-manage', 'label' => trans('settings.role_manage_page_templates')])</div>
 | 
				
			||||||
 | 
					                <div>@include('settings.roles.parts.checkbox', ['permission' => 'access-api', 'label' => trans('settings.role_access_api')])</div>
 | 
				
			||||||
 | 
					                <div>@include('settings.roles.parts.checkbox', ['permission' => 'content-export', 'label' => trans('settings.role_export_content')])</div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					                <div>@include('settings.roles.parts.checkbox', ['permission' => 'settings-manage', 'label' => trans('settings.role_manage_settings')])</div>
 | 
				
			||||||
 | 
					                <div>@include('settings.roles.parts.checkbox', ['permission' => 'users-manage', 'label' => trans('settings.role_manage_users')])</div>
 | 
				
			||||||
 | 
					                <div>@include('settings.roles.parts.checkbox', ['permission' => 'user-roles-manage', 'label' => trans('settings.role_manage_roles')])</div>
 | 
				
			||||||
 | 
					                <p class="text-warn text-small mt-s mb-none">{{ trans('settings.roles_system_warning') }}</p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div>
 | 
				
			||||||
 | 
					        <label class="setting-list-label">{{ trans('settings.role_asset') }}</label>
 | 
				
			||||||
 | 
					        <p>{{ trans('settings.role_asset_desc') }}</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @if (isset($role) && $role->system_name === 'admin')
 | 
				
			||||||
 | 
					            <p class="text-warn">{{ trans('settings.role_asset_admins') }}</p>
 | 
				
			||||||
 | 
					        @endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <table permissions-table class="table toggle-switch-list compact permissions-table">
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					                <th width="20%">
 | 
				
			||||||
 | 
					                    <a href="#" permissions-table-toggle-all class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
				
			||||||
 | 
					                </th>
 | 
				
			||||||
 | 
					                <th width="20%" permissions-table-toggle-all-in-column>{{ trans('common.create') }}</th>
 | 
				
			||||||
 | 
					                <th width="20%" permissions-table-toggle-all-in-column>{{ trans('common.view') }}</th>
 | 
				
			||||||
 | 
					                <th width="20%" permissions-table-toggle-all-in-column>{{ trans('common.edit') }}</th>
 | 
				
			||||||
 | 
					                <th width="20%" permissions-table-toggle-all-in-column>{{ trans('common.delete') }}</th>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    <div>{{ trans('entities.shelves_long') }}</div>
 | 
				
			||||||
 | 
					                    <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-create-all', 'label' => trans('settings.role_all')])
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-view-own', 'label' => trans('settings.role_own')])
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-view-all', 'label' => trans('settings.role_all')])
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-update-own', 'label' => trans('settings.role_own')])
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-update-all', 'label' => trans('settings.role_all')])
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-delete-own', 'label' => trans('settings.role_own')])
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-delete-all', 'label' => trans('settings.role_all')])
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    <div>{{ trans('entities.books') }}</div>
 | 
				
			||||||
 | 
					                    <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'book-create-all', 'label' => trans('settings.role_all')])
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'book-view-own', 'label' => trans('settings.role_own')])
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'book-view-all', 'label' => trans('settings.role_all')])
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'book-update-own', 'label' => trans('settings.role_own')])
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'book-update-all', 'label' => trans('settings.role_all')])
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'book-delete-own', 'label' => trans('settings.role_own')])
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'book-delete-all', 'label' => trans('settings.role_all')])
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    <div>{{ trans('entities.chapters') }}</div>
 | 
				
			||||||
 | 
					                    <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-create-own', 'label' => trans('settings.role_own')])
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-create-all', 'label' => trans('settings.role_all')])
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-view-own', 'label' => trans('settings.role_own')])
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-view-all', 'label' => trans('settings.role_all')])
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-update-own', 'label' => trans('settings.role_own')])
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-update-all', 'label' => trans('settings.role_all')])
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-delete-own', 'label' => trans('settings.role_own')])
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-delete-all', 'label' => trans('settings.role_all')])
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    <div>{{ trans('entities.pages') }}</div>
 | 
				
			||||||
 | 
					                    <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'page-create-own', 'label' => trans('settings.role_own')])
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'page-create-all', 'label' => trans('settings.role_all')])
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'page-view-own', 'label' => trans('settings.role_own')])
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'page-view-all', 'label' => trans('settings.role_all')])
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'page-update-own', 'label' => trans('settings.role_own')])
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'page-update-all', 'label' => trans('settings.role_all')])
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'page-delete-own', 'label' => trans('settings.role_own')])
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'page-delete-all', 'label' => trans('settings.role_all')])
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    <div>{{ trans('entities.images') }}</div>
 | 
				
			||||||
 | 
					                    <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>@include('settings.roles.parts.checkbox', ['permission' => 'image-create-all', 'label' => ''])</td>
 | 
				
			||||||
 | 
					                <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'image-update-own', 'label' => trans('settings.role_own')])
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'image-update-all', 'label' => trans('settings.role_all')])
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'image-delete-own', 'label' => trans('settings.role_own')])
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'image-delete-all', 'label' => trans('settings.role_all')])
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    <div>{{ trans('entities.attachments') }}</div>
 | 
				
			||||||
 | 
					                    <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>@include('settings.roles.parts.checkbox', ['permission' => 'attachment-create-all', 'label' => ''])</td>
 | 
				
			||||||
 | 
					                <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'attachment-update-own', 'label' => trans('settings.role_own')])
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'attachment-update-all', 'label' => trans('settings.role_all')])
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'attachment-delete-own', 'label' => trans('settings.role_own')])
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'attachment-delete-all', 'label' => trans('settings.role_all')])
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    <div>{{ trans('entities.comments') }}</div>
 | 
				
			||||||
 | 
					                    <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>@include('settings.roles.parts.checkbox', ['permission' => 'comment-create-all', 'label' => ''])</td>
 | 
				
			||||||
 | 
					                <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'comment-update-own', 'label' => trans('settings.role_own')])
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'comment-update-all', 'label' => trans('settings.role_all')])
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'comment-delete-own', 'label' => trans('settings.role_own')])
 | 
				
			||||||
 | 
					                    <br>
 | 
				
			||||||
 | 
					                    @include('settings.roles.parts.checkbox', ['permission' => 'comment-delete-all', 'label' => trans('settings.role_all')])
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					        </table>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,18 @@
 | 
				
			||||||
 | 
					@extends('layouts.simple')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@section('body')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="container small">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="py-m">
 | 
				
			||||||
 | 
					            @include('settings.parts.navbar', ['selected' => 'webhooks'])
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <form action="{{ url("/settings/webhooks/create") }}" method="POST">
 | 
				
			||||||
 | 
					            @include('settings.webhooks.parts.form', ['title' => trans('settings.webhooks_create')])
 | 
				
			||||||
 | 
					        </form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @include('settings.webhooks.parts.format-example')
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@stop
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,39 @@
 | 
				
			||||||
 | 
					@extends('layouts.simple')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@section('body')
 | 
				
			||||||
 | 
					    <div class="container small">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="py-m">
 | 
				
			||||||
 | 
					            @include('settings.parts.navbar', ['selected' => 'webhooks'])
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="card content-wrap auto-height">
 | 
				
			||||||
 | 
					            <h1 class="list-heading"> {{ trans('settings.webhooks_delete') }}</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <p>{{ trans('settings.webhooks_delete_warning', ['webhookName' => $webhook->name]) }}</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <form action="{{ $webhook->getUrl() }}" method="POST">
 | 
				
			||||||
 | 
					                {!! csrf_field() !!}
 | 
				
			||||||
 | 
					                {!! method_field('DELETE') !!}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <div class="grid half v-center">
 | 
				
			||||||
 | 
					                    <div>
 | 
				
			||||||
 | 
					                        <p class="text-neg">
 | 
				
			||||||
 | 
					                            <strong>{{ trans('settings.webhooks_delete_confirm') }}</strong>
 | 
				
			||||||
 | 
					                        </p>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    <div>
 | 
				
			||||||
 | 
					                        <div class="form-group text-right">
 | 
				
			||||||
 | 
					                            <a href="{{ $webhook->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
 | 
				
			||||||
 | 
					                            <button type="submit" class="button">{{ trans('common.confirm') }}</button>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            </form>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					@stop
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,18 @@
 | 
				
			||||||
 | 
					@extends('layouts.simple')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@section('body')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="container small">
 | 
				
			||||||
 | 
					        <div class="py-m">
 | 
				
			||||||
 | 
					            @include('settings.parts.navbar', ['selected' => 'webhooks'])
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <form action="{{ $webhook->getUrl() }}" method="POST">
 | 
				
			||||||
 | 
					            {!! method_field('PUT') !!}
 | 
				
			||||||
 | 
					            @include('settings.webhooks.parts.form', ['model' => $webhook, 'title' => trans('settings.webhooks_edit')])
 | 
				
			||||||
 | 
					        </form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @include('settings.webhooks.parts.format-example')
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@stop
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,59 @@
 | 
				
			||||||
 | 
					@extends('layouts.simple')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@section('body')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="container small">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="py-m">
 | 
				
			||||||
 | 
					            @include('settings.parts.navbar', ['selected' => 'webhooks'])
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="card content-wrap auto-height">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <div class="grid half v-center">
 | 
				
			||||||
 | 
					                <h1 class="list-heading">{{ trans('settings.webhooks') }}</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <div class="text-right">
 | 
				
			||||||
 | 
					                    <a href="{{ url("/settings/webhooks/create") }}"
 | 
				
			||||||
 | 
					                       class="button outline">{{ trans('settings.webhooks_create') }}</a>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            @if(count($webhooks) > 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <table class="table">
 | 
				
			||||||
 | 
					                    <tr>
 | 
				
			||||||
 | 
					                        <th>{{ trans('common.name') }}</th>
 | 
				
			||||||
 | 
					                        <th>{{ trans('settings.webhook_events_table_header') }}</th>
 | 
				
			||||||
 | 
					                        <th>{{ trans('common.status') }}</th>
 | 
				
			||||||
 | 
					                    </tr>
 | 
				
			||||||
 | 
					                    @foreach($webhooks as $webhook)
 | 
				
			||||||
 | 
					                        <tr>
 | 
				
			||||||
 | 
					                            <td>
 | 
				
			||||||
 | 
					                                <a href="{{ $webhook->getUrl() }}">{{ $webhook->name }}</a> <br>
 | 
				
			||||||
 | 
					                                <span class="small text-muted italic">{{ $webhook->endpoint }}</span>
 | 
				
			||||||
 | 
					                            </td>
 | 
				
			||||||
 | 
					                            <td>
 | 
				
			||||||
 | 
					                                @if($webhook->tracksEvent('all'))
 | 
				
			||||||
 | 
					                                    {{ trans('settings.webhooks_events_all') }}
 | 
				
			||||||
 | 
					                                @else
 | 
				
			||||||
 | 
					                                    {{ $webhook->trackedEvents->count() }}
 | 
				
			||||||
 | 
					                                @endif
 | 
				
			||||||
 | 
					                            </td>
 | 
				
			||||||
 | 
					                            <td>
 | 
				
			||||||
 | 
					                                {{ trans('common.status_' . ($webhook->active ? 'active' : 'inactive')) }}
 | 
				
			||||||
 | 
					                            </td>
 | 
				
			||||||
 | 
					                        </tr>
 | 
				
			||||||
 | 
					                    @endforeach
 | 
				
			||||||
 | 
					                </table>
 | 
				
			||||||
 | 
					            @else
 | 
				
			||||||
 | 
					                <p class="text-muted empty-text px-none">
 | 
				
			||||||
 | 
					                    {{ trans('settings.webhooks_none_created') }}
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
 | 
					            @endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@stop
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,75 @@
 | 
				
			||||||
 | 
					{!! csrf_field() !!}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="card content-wrap auto-height">
 | 
				
			||||||
 | 
					    <h1 class="list-heading">{{ $title }}</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="setting-list">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="grid half">
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					                <label class="setting-list-label">{{ trans('settings.webhooks_details') }}</label>
 | 
				
			||||||
 | 
					                <p class="small">{{ trans('settings.webhooks_details_desc') }}</p>
 | 
				
			||||||
 | 
					                <div>
 | 
				
			||||||
 | 
					                    @include('form.toggle-switch', [
 | 
				
			||||||
 | 
					                        'name' => 'active',
 | 
				
			||||||
 | 
					                        'value' => old('active') ?? $model->active ?? true,
 | 
				
			||||||
 | 
					                        'label' => trans('settings.webhooks_active'),
 | 
				
			||||||
 | 
					                    ])
 | 
				
			||||||
 | 
					                    @include('form.errors', ['name' => 'active'])
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					                <div class="form-group">
 | 
				
			||||||
 | 
					                    <label for="name">{{ trans('settings.webhooks_name') }}</label>
 | 
				
			||||||
 | 
					                    @include('form.text', ['name' => 'name'])
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div class="form-group">
 | 
				
			||||||
 | 
					                    <label for="endpoint">{{ trans('settings.webhooks_endpoint') }}</label>
 | 
				
			||||||
 | 
					                    @include('form.text', ['name' => 'endpoint'])
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div component="webhook-events">
 | 
				
			||||||
 | 
					            <label class="setting-list-label">{{ trans('settings.webhooks_events') }}</label>
 | 
				
			||||||
 | 
					            @include('form.errors', ['name' => 'events'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <p class="small">{{ trans('settings.webhooks_events_desc') }}</p>
 | 
				
			||||||
 | 
					            <p class="text-warn small">{{ trans('settings.webhooks_events_warning') }}</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <div class="toggle-switch-list">
 | 
				
			||||||
 | 
					                @include('form.custom-checkbox', [
 | 
				
			||||||
 | 
					                    'name' => 'events[]',
 | 
				
			||||||
 | 
					                    'value' => 'all',
 | 
				
			||||||
 | 
					                    'label' => trans('settings.webhooks_events_all'),
 | 
				
			||||||
 | 
					                    'checked' => old('events') ? in_array('all', old('events')) : (isset($webhook) ? $webhook->tracksEvent('all') : false),
 | 
				
			||||||
 | 
					                ])
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <hr class="my-s">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <div class="dual-column-content toggle-switch-list">
 | 
				
			||||||
 | 
					                @foreach(\BookStack\Actions\ActivityType::all() as $activityType)
 | 
				
			||||||
 | 
					                    <div>
 | 
				
			||||||
 | 
					                        @include('form.custom-checkbox', [
 | 
				
			||||||
 | 
					                           'name' => 'events[]',
 | 
				
			||||||
 | 
					                           'value' => $activityType,
 | 
				
			||||||
 | 
					                           'label' => $activityType,
 | 
				
			||||||
 | 
					                           'checked' => old('events') ? in_array($activityType, old('events')) : (isset($webhook) ? $webhook->tracksEvent($activityType) : false),
 | 
				
			||||||
 | 
					                       ])
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                @endforeach
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="form-group text-right">
 | 
				
			||||||
 | 
					        <a href="{{ url("/settings/webhooks") }}" class="button outline">{{ trans('common.cancel') }}</a>
 | 
				
			||||||
 | 
					        @if ($webhook->id ?? false)
 | 
				
			||||||
 | 
					            <a href="{{ $webhook->getUrl('/delete') }}" class="button outline">{{ trans('settings.webhooks_delete') }}</a>
 | 
				
			||||||
 | 
					        @endif
 | 
				
			||||||
 | 
					        <button type="submit" class="button">{{ trans('settings.webhooks_save') }}</button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,34 @@
 | 
				
			||||||
 | 
					<div component="code-highlighter" class="card content-wrap auto-height">
 | 
				
			||||||
 | 
					    <h2 class="list-heading">{{ trans('settings.webhooks_format_example') }}</h2>
 | 
				
			||||||
 | 
					    <p>{{ trans('settings.webhooks_format_example_desc') }}</p>
 | 
				
			||||||
 | 
					    <pre><code class="language-json">{
 | 
				
			||||||
 | 
					    "event": "page_update",
 | 
				
			||||||
 | 
					    "text": "Benny updated page \"My wonderful updated page\"",
 | 
				
			||||||
 | 
					    "triggered_at": "2021-12-11T22:25:10.000000Z",
 | 
				
			||||||
 | 
					    "triggered_by": {
 | 
				
			||||||
 | 
					        "id": 1,
 | 
				
			||||||
 | 
					        "name": "Benny",
 | 
				
			||||||
 | 
					        "slug": "benny"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "triggered_by_profile_url": "https://bookstack.local/user/benny",
 | 
				
			||||||
 | 
					    "webhook_id": 2,
 | 
				
			||||||
 | 
					    "webhook_name": "My page update webhook",
 | 
				
			||||||
 | 
					    "url": "https://bookstack.local/books/my-awesome-book/page/my-wonderful-updated-page",
 | 
				
			||||||
 | 
					    "related_item": {
 | 
				
			||||||
 | 
					        "id": 2432,
 | 
				
			||||||
 | 
					        "book_id": 13,
 | 
				
			||||||
 | 
					        "chapter_id": 554,
 | 
				
			||||||
 | 
					        "name": "My wonderful updated page",
 | 
				
			||||||
 | 
					        "slug": "my-wonderful-updated-page",
 | 
				
			||||||
 | 
					        "priority": 2,
 | 
				
			||||||
 | 
					        "created_at": "2021-12-11T21:53:24.000000Z",
 | 
				
			||||||
 | 
					        "updated_at": "2021-12-11T22:25:10.000000Z",
 | 
				
			||||||
 | 
					        "created_by": 1,
 | 
				
			||||||
 | 
					        "updated_by": 1,
 | 
				
			||||||
 | 
					        "draft": false,
 | 
				
			||||||
 | 
					        "revision_count": 9,
 | 
				
			||||||
 | 
					        "template": false,
 | 
				
			||||||
 | 
					        "owned_by": 1
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}</code></pre>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -29,7 +29,11 @@ use BookStack\Http\Controllers\UserApiTokenController;
 | 
				
			||||||
use BookStack\Http\Controllers\UserController;
 | 
					use BookStack\Http\Controllers\UserController;
 | 
				
			||||||
use BookStack\Http\Controllers\UserProfileController;
 | 
					use BookStack\Http\Controllers\UserProfileController;
 | 
				
			||||||
use BookStack\Http\Controllers\UserSearchController;
 | 
					use BookStack\Http\Controllers\UserSearchController;
 | 
				
			||||||
 | 
					use BookStack\Http\Controllers\WebhookController;
 | 
				
			||||||
 | 
					use BookStack\Http\Middleware\VerifyCsrfToken;
 | 
				
			||||||
 | 
					use Illuminate\Session\Middleware\StartSession;
 | 
				
			||||||
use Illuminate\Support\Facades\Route;
 | 
					use Illuminate\Support\Facades\Route;
 | 
				
			||||||
 | 
					use Illuminate\View\Middleware\ShareErrorsFromSession;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Route::get('/status', [StatusController::class, 'show']);
 | 
					Route::get('/status', [StatusController::class, 'show']);
 | 
				
			||||||
Route::get('/robots.txt', [HomeController::class, 'robots']);
 | 
					Route::get('/robots.txt', [HomeController::class, 'robots']);
 | 
				
			||||||
| 
						 | 
					@ -76,6 +80,8 @@ Route::middleware('auth')->group(function () {
 | 
				
			||||||
    Route::get('/books/{bookSlug}/permissions', [BookController::class, 'showPermissions']);
 | 
					    Route::get('/books/{bookSlug}/permissions', [BookController::class, 'showPermissions']);
 | 
				
			||||||
    Route::put('/books/{bookSlug}/permissions', [BookController::class, 'permissions']);
 | 
					    Route::put('/books/{bookSlug}/permissions', [BookController::class, 'permissions']);
 | 
				
			||||||
    Route::get('/books/{slug}/delete', [BookController::class, 'showDelete']);
 | 
					    Route::get('/books/{slug}/delete', [BookController::class, 'showDelete']);
 | 
				
			||||||
 | 
					    Route::get('/books/{bookSlug}/copy', [BookController::class, 'showCopy']);
 | 
				
			||||||
 | 
					    Route::post('/books/{bookSlug}/copy', [BookController::class, 'copy']);
 | 
				
			||||||
    Route::get('/books/{bookSlug}/sort', [BookSortController::class, 'show']);
 | 
					    Route::get('/books/{bookSlug}/sort', [BookSortController::class, 'show']);
 | 
				
			||||||
    Route::put('/books/{bookSlug}/sort', [BookSortController::class, 'update']);
 | 
					    Route::put('/books/{bookSlug}/sort', [BookSortController::class, 'update']);
 | 
				
			||||||
    Route::get('/books/{bookSlug}/export/html', [BookExportController::class, 'html']);
 | 
					    Route::get('/books/{bookSlug}/export/html', [BookExportController::class, 'html']);
 | 
				
			||||||
| 
						 | 
					@ -123,6 +129,8 @@ Route::middleware('auth')->group(function () {
 | 
				
			||||||
    Route::put('/books/{bookSlug}/chapter/{chapterSlug}', [ChapterController::class, 'update']);
 | 
					    Route::put('/books/{bookSlug}/chapter/{chapterSlug}', [ChapterController::class, 'update']);
 | 
				
			||||||
    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/move', [ChapterController::class, 'showMove']);
 | 
					    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/move', [ChapterController::class, 'showMove']);
 | 
				
			||||||
    Route::put('/books/{bookSlug}/chapter/{chapterSlug}/move', [ChapterController::class, 'move']);
 | 
					    Route::put('/books/{bookSlug}/chapter/{chapterSlug}/move', [ChapterController::class, 'move']);
 | 
				
			||||||
 | 
					    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/copy', [ChapterController::class, 'showCopy']);
 | 
				
			||||||
 | 
					    Route::post('/books/{bookSlug}/chapter/{chapterSlug}/copy', [ChapterController::class, 'copy']);
 | 
				
			||||||
    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/edit', [ChapterController::class, 'edit']);
 | 
					    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/edit', [ChapterController::class, 'edit']);
 | 
				
			||||||
    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/permissions', [ChapterController::class, 'showPermissions']);
 | 
					    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/permissions', [ChapterController::class, 'showPermissions']);
 | 
				
			||||||
    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/pdf', [ChapterExportController::class, 'pdf']);
 | 
					    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/pdf', [ChapterExportController::class, 'pdf']);
 | 
				
			||||||
| 
						 | 
					@ -244,13 +252,22 @@ Route::middleware('auth')->group(function () {
 | 
				
			||||||
    Route::delete('/settings/users/{userId}/api-tokens/{tokenId}', [UserApiTokenController::class, 'destroy']);
 | 
					    Route::delete('/settings/users/{userId}/api-tokens/{tokenId}', [UserApiTokenController::class, 'destroy']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Roles
 | 
					    // Roles
 | 
				
			||||||
    Route::get('/settings/roles', [RoleController::class, 'list']);
 | 
					    Route::get('/settings/roles', [RoleController::class, 'index']);
 | 
				
			||||||
    Route::get('/settings/roles/new', [RoleController::class, 'create']);
 | 
					    Route::get('/settings/roles/new', [RoleController::class, 'create']);
 | 
				
			||||||
    Route::post('/settings/roles/new', [RoleController::class, 'store']);
 | 
					    Route::post('/settings/roles/new', [RoleController::class, 'store']);
 | 
				
			||||||
    Route::get('/settings/roles/delete/{id}', [RoleController::class, 'showDelete']);
 | 
					    Route::get('/settings/roles/delete/{id}', [RoleController::class, 'showDelete']);
 | 
				
			||||||
    Route::delete('/settings/roles/delete/{id}', [RoleController::class, 'delete']);
 | 
					    Route::delete('/settings/roles/delete/{id}', [RoleController::class, 'delete']);
 | 
				
			||||||
    Route::get('/settings/roles/{id}', [RoleController::class, 'edit']);
 | 
					    Route::get('/settings/roles/{id}', [RoleController::class, 'edit']);
 | 
				
			||||||
    Route::put('/settings/roles/{id}', [RoleController::class, 'update']);
 | 
					    Route::put('/settings/roles/{id}', [RoleController::class, 'update']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Webhooks
 | 
				
			||||||
 | 
					    Route::get('/settings/webhooks', [WebhookController::class, 'index']);
 | 
				
			||||||
 | 
					    Route::get('/settings/webhooks/create', [WebhookController::class, 'create']);
 | 
				
			||||||
 | 
					    Route::post('/settings/webhooks/create', [WebhookController::class, 'store']);
 | 
				
			||||||
 | 
					    Route::get('/settings/webhooks/{id}', [WebhookController::class, 'edit']);
 | 
				
			||||||
 | 
					    Route::put('/settings/webhooks/{id}', [WebhookController::class, 'update']);
 | 
				
			||||||
 | 
					    Route::get('/settings/webhooks/{id}/delete', [WebhookController::class, 'delete']);
 | 
				
			||||||
 | 
					    Route::delete('/settings/webhooks/{id}', [WebhookController::class, 'destroy']);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// MFA routes
 | 
					// MFA routes
 | 
				
			||||||
| 
						 | 
					@ -291,9 +308,9 @@ Route::post('/saml2/logout', [Auth\Saml2Controller::class, 'logout']);
 | 
				
			||||||
Route::get('/saml2/metadata', [Auth\Saml2Controller::class, 'metadata']);
 | 
					Route::get('/saml2/metadata', [Auth\Saml2Controller::class, 'metadata']);
 | 
				
			||||||
Route::get('/saml2/sls', [Auth\Saml2Controller::class, 'sls']);
 | 
					Route::get('/saml2/sls', [Auth\Saml2Controller::class, 'sls']);
 | 
				
			||||||
Route::post('/saml2/acs', [Auth\Saml2Controller::class, 'startAcs'])->withoutMiddleware([
 | 
					Route::post('/saml2/acs', [Auth\Saml2Controller::class, 'startAcs'])->withoutMiddleware([
 | 
				
			||||||
    \Illuminate\Session\Middleware\StartSession::class,
 | 
					    StartSession::class,
 | 
				
			||||||
    \Illuminate\View\Middleware\ShareErrorsFromSession::class,
 | 
					    ShareErrorsFromSession::class,
 | 
				
			||||||
    \BookStack\Http\Middleware\VerifyCsrfToken::class,
 | 
					    VerifyCsrfToken::class,
 | 
				
			||||||
]);
 | 
					]);
 | 
				
			||||||
Route::get('/saml2/acs', [Auth\Saml2Controller::class, 'processAcs']);
 | 
					Route::get('/saml2/acs', [Auth\Saml2Controller::class, 'processAcs']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,10 @@
 | 
				
			||||||
<?php
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Tests;
 | 
					namespace Tests\Actions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use function app;
 | 
				
			||||||
use BookStack\Actions\Activity;
 | 
					use BookStack\Actions\Activity;
 | 
				
			||||||
use BookStack\Actions\ActivityService;
 | 
					use BookStack\Actions\ActivityLogger;
 | 
				
			||||||
use BookStack\Actions\ActivityType;
 | 
					use BookStack\Actions\ActivityType;
 | 
				
			||||||
use BookStack\Auth\UserRepo;
 | 
					use BookStack\Auth\UserRepo;
 | 
				
			||||||
use BookStack\Entities\Models\Chapter;
 | 
					use BookStack\Entities\Models\Chapter;
 | 
				
			||||||
| 
						 | 
					@ -11,16 +12,18 @@ use BookStack\Entities\Models\Page;
 | 
				
			||||||
use BookStack\Entities\Repos\PageRepo;
 | 
					use BookStack\Entities\Repos\PageRepo;
 | 
				
			||||||
use BookStack\Entities\Tools\TrashCan;
 | 
					use BookStack\Entities\Tools\TrashCan;
 | 
				
			||||||
use Carbon\Carbon;
 | 
					use Carbon\Carbon;
 | 
				
			||||||
 | 
					use function config;
 | 
				
			||||||
 | 
					use Tests\TestCase;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AuditLogTest extends TestCase
 | 
					class AuditLogTest extends TestCase
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    /** @var ActivityService */
 | 
					    /** @var ActivityLogger */
 | 
				
			||||||
    protected $activityService;
 | 
					    protected $activityService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected function setUp(): void
 | 
					    protected function setUp(): void
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        parent::setUp();
 | 
					        parent::setUp();
 | 
				
			||||||
        $this->activityService = app(ActivityService::class);
 | 
					        $this->activityService = app(ActivityLogger::class);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function test_only_accessible_with_right_permissions()
 | 
					    public function test_only_accessible_with_right_permissions()
 | 
				
			||||||
| 
						 | 
					@ -46,7 +49,7 @@ class AuditLogTest extends TestCase
 | 
				
			||||||
        $admin = $this->getAdmin();
 | 
					        $admin = $this->getAdmin();
 | 
				
			||||||
        $this->actingAs($admin);
 | 
					        $this->actingAs($admin);
 | 
				
			||||||
        $page = Page::query()->first();
 | 
					        $page = Page::query()->first();
 | 
				
			||||||
        $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE);
 | 
					        $this->activityService->add(ActivityType::PAGE_CREATE, $page);
 | 
				
			||||||
        $activity = Activity::query()->orderBy('id', 'desc')->first();
 | 
					        $activity = Activity::query()->orderBy('id', 'desc')->first();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $resp = $this->get('settings/audit');
 | 
					        $resp = $this->get('settings/audit');
 | 
				
			||||||
| 
						 | 
					@ -61,7 +64,7 @@ class AuditLogTest extends TestCase
 | 
				
			||||||
        $this->actingAs($this->getAdmin());
 | 
					        $this->actingAs($this->getAdmin());
 | 
				
			||||||
        $page = Page::query()->first();
 | 
					        $page = Page::query()->first();
 | 
				
			||||||
        $pageName = $page->name;
 | 
					        $pageName = $page->name;
 | 
				
			||||||
        $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE);
 | 
					        $this->activityService->add(ActivityType::PAGE_CREATE, $page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        app(PageRepo::class)->destroy($page);
 | 
					        app(PageRepo::class)->destroy($page);
 | 
				
			||||||
        app(TrashCan::class)->empty();
 | 
					        app(TrashCan::class)->empty();
 | 
				
			||||||
| 
						 | 
					@ -76,7 +79,7 @@ class AuditLogTest extends TestCase
 | 
				
			||||||
        $viewer = $this->getViewer();
 | 
					        $viewer = $this->getViewer();
 | 
				
			||||||
        $this->actingAs($viewer);
 | 
					        $this->actingAs($viewer);
 | 
				
			||||||
        $page = Page::query()->first();
 | 
					        $page = Page::query()->first();
 | 
				
			||||||
        $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE);
 | 
					        $this->activityService->add(ActivityType::PAGE_CREATE, $page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $this->actingAs($this->getAdmin());
 | 
					        $this->actingAs($this->getAdmin());
 | 
				
			||||||
        app(UserRepo::class)->destroy($viewer);
 | 
					        app(UserRepo::class)->destroy($viewer);
 | 
				
			||||||
| 
						 | 
					@ -89,7 +92,7 @@ class AuditLogTest extends TestCase
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->actingAs($this->getAdmin());
 | 
					        $this->actingAs($this->getAdmin());
 | 
				
			||||||
        $page = Page::query()->first();
 | 
					        $page = Page::query()->first();
 | 
				
			||||||
        $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE);
 | 
					        $this->activityService->add(ActivityType::PAGE_CREATE, $page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $resp = $this->get('settings/audit');
 | 
					        $resp = $this->get('settings/audit');
 | 
				
			||||||
        $resp->assertSeeText($page->name);
 | 
					        $resp->assertSeeText($page->name);
 | 
				
			||||||
| 
						 | 
					@ -102,7 +105,7 @@ class AuditLogTest extends TestCase
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->actingAs($this->getAdmin());
 | 
					        $this->actingAs($this->getAdmin());
 | 
				
			||||||
        $page = Page::query()->first();
 | 
					        $page = Page::query()->first();
 | 
				
			||||||
        $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE);
 | 
					        $this->activityService->add(ActivityType::PAGE_CREATE, $page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $yesterday = (Carbon::now()->subDay()->format('Y-m-d'));
 | 
					        $yesterday = (Carbon::now()->subDay()->format('Y-m-d'));
 | 
				
			||||||
        $tomorrow = (Carbon::now()->addDay()->format('Y-m-d'));
 | 
					        $tomorrow = (Carbon::now()->addDay()->format('Y-m-d'));
 | 
				
			||||||
| 
						 | 
					@ -126,11 +129,11 @@ class AuditLogTest extends TestCase
 | 
				
			||||||
        $editor = $this->getEditor();
 | 
					        $editor = $this->getEditor();
 | 
				
			||||||
        $this->actingAs($admin);
 | 
					        $this->actingAs($admin);
 | 
				
			||||||
        $page = Page::query()->first();
 | 
					        $page = Page::query()->first();
 | 
				
			||||||
        $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE);
 | 
					        $this->activityService->add(ActivityType::PAGE_CREATE, $page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $this->actingAs($editor);
 | 
					        $this->actingAs($editor);
 | 
				
			||||||
        $chapter = Chapter::query()->first();
 | 
					        $chapter = Chapter::query()->first();
 | 
				
			||||||
        $this->activityService->addForEntity($chapter, ActivityType::CHAPTER_UPDATE);
 | 
					        $this->activityService->add(ActivityType::CHAPTER_UPDATE, $chapter);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $resp = $this->actingAs($admin)->get('settings/audit?user=' . $admin->id);
 | 
					        $resp = $this->actingAs($admin)->get('settings/audit?user=' . $admin->id);
 | 
				
			||||||
        $resp->assertSeeText($page->name);
 | 
					        $resp->assertSeeText($page->name);
 | 
				
			||||||
| 
						 | 
					@ -166,6 +169,32 @@ class AuditLogTest extends TestCase
 | 
				
			||||||
        $resp->assertSee('192.123.45.1');
 | 
					        $resp->assertSee('192.123.45.1');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_ip_address_is_searchable()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        config()->set('app.proxies', '*');
 | 
				
			||||||
 | 
					        $editor = $this->getEditor();
 | 
				
			||||||
 | 
					        /** @var Page $page */
 | 
				
			||||||
 | 
					        $page = Page::query()->first();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->actingAs($editor)->put($page->getUrl(), [
 | 
				
			||||||
 | 
					            'name' => 'Updated page',
 | 
				
			||||||
 | 
					            'html' => '<p>Updated content</p>',
 | 
				
			||||||
 | 
					        ], [
 | 
				
			||||||
 | 
					            'X-Forwarded-For' => '192.123.45.1',
 | 
				
			||||||
 | 
					        ])->assertRedirect($page->refresh()->getUrl());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->actingAs($editor)->put($page->getUrl(), [
 | 
				
			||||||
 | 
					            'name' => 'Updated page',
 | 
				
			||||||
 | 
					            'html' => '<p>Updated content</p>',
 | 
				
			||||||
 | 
					        ], [
 | 
				
			||||||
 | 
					            'X-Forwarded-For' => '192.122.45.1',
 | 
				
			||||||
 | 
					        ])->assertRedirect($page->refresh()->getUrl());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $resp = $this->asAdmin()->get('/settings/audit?&ip=192.123');
 | 
				
			||||||
 | 
					        $resp->assertSee('192.123.45.1');
 | 
				
			||||||
 | 
					        $resp->assertDontSee('192.122.45.1');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function test_ip_address_not_logged_in_demo_mode()
 | 
					    public function test_ip_address_not_logged_in_demo_mode()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        config()->set('app.proxies', '*');
 | 
					        config()->set('app.proxies', '*');
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,114 @@
 | 
				
			||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Tests\Actions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use BookStack\Actions\ActivityLogger;
 | 
				
			||||||
 | 
					use BookStack\Actions\ActivityType;
 | 
				
			||||||
 | 
					use BookStack\Actions\DispatchWebhookJob;
 | 
				
			||||||
 | 
					use BookStack\Actions\Webhook;
 | 
				
			||||||
 | 
					use BookStack\Auth\User;
 | 
				
			||||||
 | 
					use BookStack\Entities\Models\Page;
 | 
				
			||||||
 | 
					use Illuminate\Http\Client\Request;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Bus;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Http;
 | 
				
			||||||
 | 
					use Tests\TestCase;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class WebhookCallTest extends TestCase
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public function test_webhook_listening_to_all_called_on_event()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->newWebhook([], ['all']);
 | 
				
			||||||
 | 
					        Bus::fake();
 | 
				
			||||||
 | 
					        $this->runEvent(ActivityType::ROLE_CREATE);
 | 
				
			||||||
 | 
					        Bus::assertDispatched(DispatchWebhookJob::class);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_webhook_listening_to_specific_event_called_on_event()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->newWebhook([], [ActivityType::ROLE_UPDATE]);
 | 
				
			||||||
 | 
					        Bus::fake();
 | 
				
			||||||
 | 
					        $this->runEvent(ActivityType::ROLE_UPDATE);
 | 
				
			||||||
 | 
					        Bus::assertDispatched(DispatchWebhookJob::class);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_webhook_listening_to_specific_event_not_called_on_other_event()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->newWebhook([], [ActivityType::ROLE_UPDATE]);
 | 
				
			||||||
 | 
					        Bus::fake();
 | 
				
			||||||
 | 
					        $this->runEvent(ActivityType::ROLE_CREATE);
 | 
				
			||||||
 | 
					        Bus::assertNotDispatched(DispatchWebhookJob::class);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_inactive_webhook_not_called_on_event()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->newWebhook(['active' => false], ['all']);
 | 
				
			||||||
 | 
					        Bus::fake();
 | 
				
			||||||
 | 
					        $this->runEvent(ActivityType::ROLE_CREATE);
 | 
				
			||||||
 | 
					        Bus::assertNotDispatched(DispatchWebhookJob::class);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_failed_webhook_call_logs_error()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $logger = $this->withTestLogger();
 | 
				
			||||||
 | 
					        Http::fake([
 | 
				
			||||||
 | 
					            '*' => Http::response('', 500),
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					        $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->runEvent(ActivityType::ROLE_CREATE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->assertTrue($logger->hasError('Webhook call to endpoint https://wh.example.com failed with status 500'));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_webhook_call_data_format()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Http::fake([
 | 
				
			||||||
 | 
					            '*' => Http::response('', 200),
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					        $webhook = $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']);
 | 
				
			||||||
 | 
					        /** @var Page $page */
 | 
				
			||||||
 | 
					        $page = Page::query()->first();
 | 
				
			||||||
 | 
					        $editor = $this->getEditor();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->runEvent(ActivityType::PAGE_UPDATE, $page, $editor);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Http::assertSent(function (Request $request) use ($editor, $page, $webhook) {
 | 
				
			||||||
 | 
					            $reqData = $request->data();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return $request->isJson()
 | 
				
			||||||
 | 
					                && $reqData['event'] === 'page_update'
 | 
				
			||||||
 | 
					                && $reqData['text'] === ($editor->name . ' updated page "' . $page->name . '"')
 | 
				
			||||||
 | 
					                && is_string($reqData['triggered_at'])
 | 
				
			||||||
 | 
					                && $reqData['triggered_by']['name'] === $editor->name
 | 
				
			||||||
 | 
					                && $reqData['triggered_by_profile_url'] === $editor->getProfileUrl()
 | 
				
			||||||
 | 
					                && $reqData['webhook_id'] === $webhook->id
 | 
				
			||||||
 | 
					                && $reqData['webhook_name'] === $webhook->name
 | 
				
			||||||
 | 
					                && $reqData['url'] === $page->getUrl()
 | 
				
			||||||
 | 
					                && $reqData['related_item']['name'] === $page->name;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected function runEvent(string $event, $detail = '', ?User $user = null)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if (is_null($user)) {
 | 
				
			||||||
 | 
					            $user = $this->getEditor();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->actingAs($user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $activityLogger = $this->app->make(ActivityLogger::class);
 | 
				
			||||||
 | 
					        $activityLogger->add($event, $detail);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected function newWebhook(array $attrs = [], array $events = ['all']): Webhook
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /** @var Webhook $webhook */
 | 
				
			||||||
 | 
					        $webhook = Webhook::factory()->create($attrs);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        foreach ($events as $event) {
 | 
				
			||||||
 | 
					            $webhook->trackedEvents()->create(['event' => $event]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $webhook;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,171 @@
 | 
				
			||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Tests\Actions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use BookStack\Actions\ActivityType;
 | 
				
			||||||
 | 
					use BookStack\Actions\Webhook;
 | 
				
			||||||
 | 
					use Tests\TestCase;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class WebhookManagementTest extends TestCase
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public function test_index_view()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $webhook = $this->newWebhook([
 | 
				
			||||||
 | 
					            'name'     => 'My awesome webhook',
 | 
				
			||||||
 | 
					            'endpoint' => 'https://example.com/donkey/webhook',
 | 
				
			||||||
 | 
					        ], ['all']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $resp = $this->asAdmin()->get('/settings/webhooks');
 | 
				
			||||||
 | 
					        $resp->assertOk();
 | 
				
			||||||
 | 
					        $resp->assertElementContains('a[href$="/settings/webhooks/create"]', 'Create New Webhook');
 | 
				
			||||||
 | 
					        $resp->assertElementExists('a[href="' . $webhook->getUrl() . '"]', $webhook->name);
 | 
				
			||||||
 | 
					        $resp->assertSee($webhook->endpoint);
 | 
				
			||||||
 | 
					        $resp->assertSee('All system events');
 | 
				
			||||||
 | 
					        $resp->assertSee('Active');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_create_view()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $resp = $this->asAdmin()->get('/settings/webhooks/create');
 | 
				
			||||||
 | 
					        $resp->assertOk();
 | 
				
			||||||
 | 
					        $resp->assertSee('Create New Webhook');
 | 
				
			||||||
 | 
					        $resp->assertElementContains('form[action$="/settings/webhooks/create"] button', 'Save Webhook');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_store()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $resp = $this->asAdmin()->post('/settings/webhooks/create', [
 | 
				
			||||||
 | 
					            'name'     => 'My first webhook',
 | 
				
			||||||
 | 
					            'endpoint' => 'https://example.com/webhook',
 | 
				
			||||||
 | 
					            'events'   => ['all'],
 | 
				
			||||||
 | 
					            'active'   => 'true',
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $resp->assertRedirect('/settings/webhooks');
 | 
				
			||||||
 | 
					        $this->assertActivityExists(ActivityType::WEBHOOK_CREATE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $resp = $this->followRedirects($resp);
 | 
				
			||||||
 | 
					        $resp->assertSee('Webhook successfully created');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->assertDatabaseHas('webhooks', [
 | 
				
			||||||
 | 
					            'name'     => 'My first webhook',
 | 
				
			||||||
 | 
					            'endpoint' => 'https://example.com/webhook',
 | 
				
			||||||
 | 
					            'active'   => true,
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /** @var Webhook $webhook */
 | 
				
			||||||
 | 
					        $webhook = Webhook::query()->where('name', '=', 'My first webhook')->first();
 | 
				
			||||||
 | 
					        $this->assertDatabaseHas('webhook_tracked_events', [
 | 
				
			||||||
 | 
					            'webhook_id' => $webhook->id,
 | 
				
			||||||
 | 
					            'event'      => 'all',
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_edit_view()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $webhook = $this->newWebhook();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $resp = $this->asAdmin()->get('/settings/webhooks/' . $webhook->id);
 | 
				
			||||||
 | 
					        $resp->assertOk();
 | 
				
			||||||
 | 
					        $resp->assertSee('Edit Webhook');
 | 
				
			||||||
 | 
					        $resp->assertElementContains('form[action="' . $webhook->getUrl() . '"] button', 'Save Webhook');
 | 
				
			||||||
 | 
					        $resp->assertElementContains('a[href="' . $webhook->getUrl('/delete') . '"]', 'Delete Webhook');
 | 
				
			||||||
 | 
					        $resp->assertElementExists('input[type="checkbox"][value="all"][name="events[]"]');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_update()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $webhook = $this->newWebhook();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $resp = $this->asAdmin()->put('/settings/webhooks/' . $webhook->id, [
 | 
				
			||||||
 | 
					            'name'     => 'My updated webhook',
 | 
				
			||||||
 | 
					            'endpoint' => 'https://example.com/updated-webhook',
 | 
				
			||||||
 | 
					            'events'   => [ActivityType::PAGE_CREATE, ActivityType::PAGE_UPDATE],
 | 
				
			||||||
 | 
					            'active'   => 'true',
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					        $resp->assertRedirect('/settings/webhooks');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $resp = $this->followRedirects($resp);
 | 
				
			||||||
 | 
					        $resp->assertSee('Webhook successfully updated');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->assertDatabaseHas('webhooks', [
 | 
				
			||||||
 | 
					            'id'       => $webhook->id,
 | 
				
			||||||
 | 
					            'name'     => 'My updated webhook',
 | 
				
			||||||
 | 
					            'endpoint' => 'https://example.com/updated-webhook',
 | 
				
			||||||
 | 
					            'active'   => true,
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $trackedEvents = $webhook->trackedEvents()->get();
 | 
				
			||||||
 | 
					        $this->assertCount(2, $trackedEvents);
 | 
				
			||||||
 | 
					        $this->assertEquals(['page_create', 'page_update'], $trackedEvents->pluck('event')->values()->all());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->assertActivityExists(ActivityType::WEBHOOK_UPDATE);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_delete_view()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $webhook = $this->newWebhook(['name' => 'Webhook to delete']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $resp = $this->asAdmin()->get('/settings/webhooks/' . $webhook->id . '/delete');
 | 
				
			||||||
 | 
					        $resp->assertOk();
 | 
				
			||||||
 | 
					        $resp->assertSee('Delete Webhook');
 | 
				
			||||||
 | 
					        $resp->assertSee('This will fully delete this webhook, with the name \'Webhook to delete\', from the system.');
 | 
				
			||||||
 | 
					        $resp->assertElementContains('form[action$="/settings/webhooks/' . $webhook->id . '"]', 'Delete');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_destroy()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $webhook = $this->newWebhook();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $resp = $this->asAdmin()->delete('/settings/webhooks/' . $webhook->id);
 | 
				
			||||||
 | 
					        $resp->assertRedirect('/settings/webhooks');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $resp = $this->followRedirects($resp);
 | 
				
			||||||
 | 
					        $resp->assertSee('Webhook successfully deleted');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->assertDatabaseMissing('webhooks', ['id' => $webhook->id]);
 | 
				
			||||||
 | 
					        $this->assertDatabaseMissing('webhook_tracked_events', ['webhook_id' => $webhook->id]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->assertActivityExists(ActivityType::WEBHOOK_DELETE);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_settings_manage_permission_required_for_webhook_routes()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $editor = $this->getEditor();
 | 
				
			||||||
 | 
					        $this->actingAs($editor);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $routes = [
 | 
				
			||||||
 | 
					            ['GET', '/settings/webhooks'],
 | 
				
			||||||
 | 
					            ['GET', '/settings/webhooks/create'],
 | 
				
			||||||
 | 
					            ['POST', '/settings/webhooks/create'],
 | 
				
			||||||
 | 
					            ['GET', '/settings/webhooks/1'],
 | 
				
			||||||
 | 
					            ['PUT', '/settings/webhooks/1'],
 | 
				
			||||||
 | 
					            ['DELETE', '/settings/webhooks/1'],
 | 
				
			||||||
 | 
					            ['GET', '/settings/webhooks/1/delete'],
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        foreach ($routes as [$method, $endpoint]) {
 | 
				
			||||||
 | 
					            $resp = $this->call($method, $endpoint);
 | 
				
			||||||
 | 
					            $this->assertPermissionError($resp);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->giveUserPermissions($editor, ['settings-manage']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        foreach ($routes as [$method, $endpoint]) {
 | 
				
			||||||
 | 
					            $resp = $this->call($method, $endpoint);
 | 
				
			||||||
 | 
					            $this->assertNotPermissionError($resp);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected function newWebhook(array $attrs = [], array $events = ['all']): Webhook
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /** @var Webhook $webhook */
 | 
				
			||||||
 | 
					        $webhook = Webhook::factory()->create($attrs);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        foreach ($events as $event) {
 | 
				
			||||||
 | 
					            $webhook->trackedEvents()->create(['event' => $event]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $webhook;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,8 @@ namespace Tests\Commands;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use BookStack\Actions\ActivityType;
 | 
					use BookStack\Actions\ActivityType;
 | 
				
			||||||
use BookStack\Entities\Models\Page;
 | 
					use BookStack\Entities\Models\Page;
 | 
				
			||||||
 | 
					use BookStack\Facades\Activity;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Artisan;
 | 
				
			||||||
use Illuminate\Support\Facades\DB;
 | 
					use Illuminate\Support\Facades\DB;
 | 
				
			||||||
use Tests\TestCase;
 | 
					use Tests\TestCase;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,8 +14,9 @@ class ClearActivityCommandTest extends TestCase
 | 
				
			||||||
    public function test_clear_activity_command()
 | 
					    public function test_clear_activity_command()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->asEditor();
 | 
					        $this->asEditor();
 | 
				
			||||||
        $page = Page::first();
 | 
					        /** @var Page $page */
 | 
				
			||||||
        \Activity::addForEntity($page, ActivityType::PAGE_UPDATE);
 | 
					        $page = Page::query()->first();
 | 
				
			||||||
 | 
					        Activity::add(ActivityType::PAGE_UPDATE, $page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $this->assertDatabaseHas('activities', [
 | 
					        $this->assertDatabaseHas('activities', [
 | 
				
			||||||
            'type'      => 'page_update',
 | 
					            'type'      => 'page_update',
 | 
				
			||||||
| 
						 | 
					@ -22,7 +25,7 @@ class ClearActivityCommandTest extends TestCase
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        DB::rollBack();
 | 
					        DB::rollBack();
 | 
				
			||||||
        $exitCode = \Artisan::call('bookstack:clear-activity');
 | 
					        $exitCode = Artisan::call('bookstack:clear-activity');
 | 
				
			||||||
        DB::beginTransaction();
 | 
					        DB::beginTransaction();
 | 
				
			||||||
        $this->assertTrue($exitCode === 0, 'Command executed successfully');
 | 
					        $this->assertTrue($exitCode === 0, 'Command executed successfully');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,10 +3,15 @@
 | 
				
			||||||
namespace Tests\Entity;
 | 
					namespace Tests\Entity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use BookStack\Entities\Models\Book;
 | 
					use BookStack\Entities\Models\Book;
 | 
				
			||||||
 | 
					use BookStack\Entities\Models\BookChild;
 | 
				
			||||||
 | 
					use BookStack\Entities\Repos\BookRepo;
 | 
				
			||||||
use Tests\TestCase;
 | 
					use Tests\TestCase;
 | 
				
			||||||
 | 
					use Tests\Uploads\UsesImages;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BookTest extends TestCase
 | 
					class BookTest extends TestCase
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
					    use UsesImages;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function test_create()
 | 
					    public function test_create()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $book = Book::factory()->make([
 | 
					        $book = Book::factory()->make([
 | 
				
			||||||
| 
						 | 
					@ -204,4 +209,88 @@ class BookTest extends TestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $this->assertEquals('parta-partb-partc', $book->slug);
 | 
					        $this->assertEquals('parta-partb-partc', $book->slug);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_show_view_has_copy_button()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /** @var Book $book */
 | 
				
			||||||
 | 
					        $book = Book::query()->first();
 | 
				
			||||||
 | 
					        $resp = $this->asEditor()->get($book->getUrl());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $resp->assertElementContains("a[href=\"{$book->getUrl('/copy')}\"]", 'Copy');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_copy_view()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /** @var Book $book */
 | 
				
			||||||
 | 
					        $book = Book::query()->first();
 | 
				
			||||||
 | 
					        $resp = $this->asEditor()->get($book->getUrl('/copy'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $resp->assertOk();
 | 
				
			||||||
 | 
					        $resp->assertSee('Copy Book');
 | 
				
			||||||
 | 
					        $resp->assertElementExists("input[name=\"name\"][value=\"{$book->name}\"]");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_copy()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /** @var Book $book */
 | 
				
			||||||
 | 
					        $book = Book::query()->whereHas('chapters')->whereHas('pages')->first();
 | 
				
			||||||
 | 
					        $resp = $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /** @var Book $copy */
 | 
				
			||||||
 | 
					        $copy = Book::query()->where('name', '=', 'My copy book')->first();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $resp->assertRedirect($copy->getUrl());
 | 
				
			||||||
 | 
					        $this->assertEquals($book->getDirectChildren()->count(), $copy->getDirectChildren()->count());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_copy_does_not_copy_non_visible_content()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /** @var Book $book */
 | 
				
			||||||
 | 
					        $book = Book::query()->whereHas('chapters')->whereHas('pages')->first();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Hide child content
 | 
				
			||||||
 | 
					        /** @var BookChild $page */
 | 
				
			||||||
 | 
					        foreach ($book->getDirectChildren() as $child) {
 | 
				
			||||||
 | 
					            $child->restricted = true;
 | 
				
			||||||
 | 
					            $child->save();
 | 
				
			||||||
 | 
					            $this->regenEntityPermissions($child);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);
 | 
				
			||||||
 | 
					        /** @var Book $copy */
 | 
				
			||||||
 | 
					        $copy = Book::query()->where('name', '=', 'My copy book')->first();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->assertEquals(0, $copy->getDirectChildren()->count());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_copy_does_not_copy_pages_or_chapters_if_user_cant_create()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /** @var Book $book */
 | 
				
			||||||
 | 
					        $book = Book::query()->whereHas('chapters')->whereHas('directPages')->whereHas('chapters')->first();
 | 
				
			||||||
 | 
					        $viewer = $this->getViewer();
 | 
				
			||||||
 | 
					        $this->giveUserPermissions($viewer, ['book-create-all']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->actingAs($viewer)->post($book->getUrl('/copy'), ['name' => 'My copy book']);
 | 
				
			||||||
 | 
					        /** @var Book $copy */
 | 
				
			||||||
 | 
					        $copy = Book::query()->where('name', '=', 'My copy book')->first();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->assertEquals(0, $copy->pages()->count());
 | 
				
			||||||
 | 
					        $this->assertEquals(0, $copy->chapters()->count());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_copy_clones_cover_image_if_existing()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /** @var Book $book */
 | 
				
			||||||
 | 
					        $book = Book::query()->first();
 | 
				
			||||||
 | 
					        $bookRepo = $this->app->make(BookRepo::class);
 | 
				
			||||||
 | 
					        $coverImageFile = $this->getTestImage('cover.png');
 | 
				
			||||||
 | 
					        $bookRepo->updateCoverImage($book, $coverImageFile);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /** @var Book $copy */
 | 
				
			||||||
 | 
					        $copy = Book::query()->where('name', '=', 'My copy book')->first();
 | 
				
			||||||
 | 
					        $this->assertNotNull($copy->cover);
 | 
				
			||||||
 | 
					        $this->assertNotEquals($book->cover->id, $copy->cover->id);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@ namespace Tests\Entity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use BookStack\Entities\Models\Book;
 | 
					use BookStack\Entities\Models\Book;
 | 
				
			||||||
use BookStack\Entities\Models\Chapter;
 | 
					use BookStack\Entities\Models\Chapter;
 | 
				
			||||||
 | 
					use BookStack\Entities\Models\Page;
 | 
				
			||||||
use Tests\TestCase;
 | 
					use Tests\TestCase;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ChapterTest extends TestCase
 | 
					class ChapterTest extends TestCase
 | 
				
			||||||
| 
						 | 
					@ -54,4 +55,95 @@ class ChapterTest extends TestCase
 | 
				
			||||||
        $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));
 | 
					        $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));
 | 
				
			||||||
        $redirectReq->assertNotificationContains('Chapter Successfully Deleted');
 | 
					        $redirectReq->assertNotificationContains('Chapter Successfully Deleted');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_show_view_has_copy_button()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /** @var Chapter $chapter */
 | 
				
			||||||
 | 
					        $chapter = Chapter::query()->first();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $resp = $this->asEditor()->get($chapter->getUrl());
 | 
				
			||||||
 | 
					        $resp->assertElementContains("a[href$=\"{$chapter->getUrl('/copy')}\"]", 'Copy');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_copy_view()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /** @var Chapter $chapter */
 | 
				
			||||||
 | 
					        $chapter = Chapter::query()->first();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $resp = $this->asEditor()->get($chapter->getUrl('/copy'));
 | 
				
			||||||
 | 
					        $resp->assertOk();
 | 
				
			||||||
 | 
					        $resp->assertSee('Copy Chapter');
 | 
				
			||||||
 | 
					        $resp->assertElementExists("input[name=\"name\"][value=\"{$chapter->name}\"]");
 | 
				
			||||||
 | 
					        $resp->assertElementExists('input[name="entity_selection"]');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_copy()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /** @var Chapter $chapter */
 | 
				
			||||||
 | 
					        $chapter = Chapter::query()->whereHas('pages')->first();
 | 
				
			||||||
 | 
					        /** @var Book $otherBook */
 | 
				
			||||||
 | 
					        $otherBook = Book::query()->where('id', '!=', $chapter->book_id)->first();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $resp = $this->asEditor()->post($chapter->getUrl('/copy'), [
 | 
				
			||||||
 | 
					            'name'             => 'My copied chapter',
 | 
				
			||||||
 | 
					            'entity_selection' => 'book:' . $otherBook->id,
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /** @var Chapter $newChapter */
 | 
				
			||||||
 | 
					        $newChapter = Chapter::query()->where('name', '=', 'My copied chapter')->first();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $resp->assertRedirect($newChapter->getUrl());
 | 
				
			||||||
 | 
					        $this->assertEquals($otherBook->id, $newChapter->book_id);
 | 
				
			||||||
 | 
					        $this->assertEquals($chapter->pages->count(), $newChapter->pages->count());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_copy_does_not_copy_non_visible_pages()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /** @var Chapter $chapter */
 | 
				
			||||||
 | 
					        $chapter = Chapter::query()->whereHas('pages')->first();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Hide pages to all non-admin roles
 | 
				
			||||||
 | 
					        /** @var Page $page */
 | 
				
			||||||
 | 
					        foreach ($chapter->pages as $page) {
 | 
				
			||||||
 | 
					            $page->restricted = true;
 | 
				
			||||||
 | 
					            $page->save();
 | 
				
			||||||
 | 
					            $this->regenEntityPermissions($page);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->asEditor()->post($chapter->getUrl('/copy'), [
 | 
				
			||||||
 | 
					            'name' => 'My copied chapter',
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /** @var Chapter $newChapter */
 | 
				
			||||||
 | 
					        $newChapter = Chapter::query()->where('name', '=', 'My copied chapter')->first();
 | 
				
			||||||
 | 
					        $this->assertEquals(0, $newChapter->pages()->count());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_copy_does_not_copy_pages_if_user_cant_page_create()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /** @var Chapter $chapter */
 | 
				
			||||||
 | 
					        $chapter = Chapter::query()->whereHas('pages')->first();
 | 
				
			||||||
 | 
					        $viewer = $this->getViewer();
 | 
				
			||||||
 | 
					        $this->giveUserPermissions($viewer, ['chapter-create-all']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Lacking permission results in no copied pages
 | 
				
			||||||
 | 
					        $this->actingAs($viewer)->post($chapter->getUrl('/copy'), [
 | 
				
			||||||
 | 
					            'name' => 'My copied chapter',
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /** @var Chapter $newChapter */
 | 
				
			||||||
 | 
					        $newChapter = Chapter::query()->where('name', '=', 'My copied chapter')->first();
 | 
				
			||||||
 | 
					        $this->assertEquals(0, $newChapter->pages()->count());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->giveUserPermissions($viewer, ['page-create-all']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Having permission rules in copied pages
 | 
				
			||||||
 | 
					        $this->actingAs($viewer)->post($chapter->getUrl('/copy'), [
 | 
				
			||||||
 | 
					            'name' => 'My copied again chapter',
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /** @var Chapter $newChapter2 */
 | 
				
			||||||
 | 
					        $newChapter2 = Chapter::query()->where('name', '=', 'My copied again chapter')->first();
 | 
				
			||||||
 | 
					        $this->assertEquals($chapter->pages()->count(), $newChapter2->pages()->count());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -403,6 +403,17 @@ class EntitySearchTest extends TestCase
 | 
				
			||||||
        $search->assertElementContains('.tag-value.highlight', 'MeowieCat');
 | 
					        $search->assertElementContains('.tag-value.highlight', 'MeowieCat');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_match_highlighting_works_with_multibyte_content()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->newPage([
 | 
				
			||||||
 | 
					            'name' => 'Test Page',
 | 
				
			||||||
 | 
					            'html' => '<p>На мен ми трябва нещо добро test</p>',
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $search = $this->asEditor()->get('/search?term=' . urlencode('На мен ми трябва нещо добро'));
 | 
				
			||||||
 | 
					        $search->assertSee('<strong>На</strong> <strong>мен</strong> <strong>ми</strong> <strong>трябва</strong> <strong>нещо</strong> <strong>добро</strong> test', false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function test_html_entities_in_item_details_remains_escaped_in_search_results()
 | 
					    public function test_html_entities_in_item_details_remains_escaped_in_search_results()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->newPage(['name' => 'My <cool> TestPageContent', 'html' => '<p>My supercool <great> TestPageContent page</p>']);
 | 
					        $this->newPage(['name' => 'My <cool> TestPageContent', 'html' => '<p>My supercool <great> TestPageContent page</p>']);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -163,6 +163,23 @@ class RolesTest extends TestCase
 | 
				
			||||||
        $this->assertEquals($this->user->id, $roleA->users()->first()->id);
 | 
					        $this->assertEquals($this->user->id, $roleA->users()->first()->id);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_copy_role_button_shown()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /** @var Role $role */
 | 
				
			||||||
 | 
					        $role = Role::query()->first();
 | 
				
			||||||
 | 
					        $resp = $this->asAdmin()->get("/settings/roles/{$role->id}");
 | 
				
			||||||
 | 
					        $resp->assertElementContains('a[href$="/roles/new?copy_from=' . $role->id . '"]', 'Copy');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function test_copy_from_param_on_create_prefills_with_other_role_data()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /** @var Role $role */
 | 
				
			||||||
 | 
					        $role = Role::query()->first();
 | 
				
			||||||
 | 
					        $resp = $this->asAdmin()->get("/settings/roles/new?copy_from={$role->id}");
 | 
				
			||||||
 | 
					        $resp->assertOk();
 | 
				
			||||||
 | 
					        $resp->assertElementExists('input[name="display_name"][value="' . ($role->display_name . ' (Copy)') . '"]');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function test_manage_user_permission()
 | 
					    public function test_manage_user_permission()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->actingAs($this->user)->get('/settings/users')->assertRedirect('/');
 | 
					        $this->actingAs($this->user)->get('/settings/users')->assertRedirect('/');
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,13 @@
 | 
				
			||||||
<?php
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Tests;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Exception;
 | 
				
			||||||
use Illuminate\Cache\ArrayStore;
 | 
					use Illuminate\Cache\ArrayStore;
 | 
				
			||||||
use Illuminate\Support\Facades\Cache;
 | 
					use Illuminate\Support\Facades\Cache;
 | 
				
			||||||
use Illuminate\Support\Facades\DB;
 | 
					use Illuminate\Support\Facades\DB;
 | 
				
			||||||
use Illuminate\Support\Facades\Session;
 | 
					use Illuminate\Support\Facades\Session;
 | 
				
			||||||
use Tests\TestCase;
 | 
					use Mockery;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StatusTest extends TestCase
 | 
					class StatusTest extends TestCase
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,7 +34,7 @@ class TestEmailTest extends TestCase
 | 
				
			||||||
        $this->app[Dispatcher::class] = $mockDispatcher;
 | 
					        $this->app[Dispatcher::class] = $mockDispatcher;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $exception = new \Exception('A random error occurred when testing an email');
 | 
					        $exception = new \Exception('A random error occurred when testing an email');
 | 
				
			||||||
        $mockDispatcher->shouldReceive('send')->andThrow($exception);
 | 
					        $mockDispatcher->shouldReceive('sendNow')->andThrow($exception);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $admin = $this->getAdmin();
 | 
					        $admin = $this->getAdmin();
 | 
				
			||||||
        $sendReq = $this->actingAs($admin)->post('/settings/maintenance/send-test-email');
 | 
					        $sendReq = $this->actingAs($admin)->post('/settings/maintenance/send-test-email');
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,16 +11,16 @@ class AvatarTest extends TestCase
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    use UsesImages;
 | 
					    use UsesImages;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected function createUserRequest($user)
 | 
					    protected function createUserRequest($user): User
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->asAdmin()->post('/settings/users/create', [
 | 
					        $this->asAdmin()->post('/settings/users/create', [
 | 
				
			||||||
            'name'             => $user->name,
 | 
					            'name'             => $user->name,
 | 
				
			||||||
            'email'            => $user->email,
 | 
					            'email'            => $user->email,
 | 
				
			||||||
            'password'         => 'testing',
 | 
					            'password'         => 'testing101',
 | 
				
			||||||
            'password-confirm' => 'testing',
 | 
					            'password-confirm' => 'testing101',
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return User::where('email', '=', $user->email)->first();
 | 
					        return User::query()->where('email', '=', $user->email)->first();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected function assertImageFetchFrom(string $url)
 | 
					    protected function assertImageFetchFrom(string $url)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -64,8 +64,8 @@ class UserProfileTest extends TestCase
 | 
				
			||||||
        $newUser = User::factory()->create();
 | 
					        $newUser = User::factory()->create();
 | 
				
			||||||
        $this->actingAs($newUser);
 | 
					        $this->actingAs($newUser);
 | 
				
			||||||
        $entities = $this->createEntityChainBelongingToUser($newUser, $newUser);
 | 
					        $entities = $this->createEntityChainBelongingToUser($newUser, $newUser);
 | 
				
			||||||
        Activity::addForEntity($entities['book'], ActivityType::BOOK_UPDATE);
 | 
					        Activity::add(ActivityType::BOOK_UPDATE, $entities['book']);
 | 
				
			||||||
        Activity::addForEntity($entities['page'], ActivityType::PAGE_CREATE);
 | 
					        Activity::add(ActivityType::PAGE_CREATE, $entities['page']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $this->asAdmin()->get('/user/' . $newUser->slug)
 | 
					        $this->asAdmin()->get('/user/' . $newUser->slug)
 | 
				
			||||||
            ->assertElementContains('#recent-user-activity', 'updated book')
 | 
					            ->assertElementContains('#recent-user-activity', 'updated book')
 | 
				
			||||||
| 
						 | 
					@ -78,8 +78,8 @@ class UserProfileTest extends TestCase
 | 
				
			||||||
        $newUser = User::factory()->create();
 | 
					        $newUser = User::factory()->create();
 | 
				
			||||||
        $this->actingAs($newUser);
 | 
					        $this->actingAs($newUser);
 | 
				
			||||||
        $entities = $this->createEntityChainBelongingToUser($newUser, $newUser);
 | 
					        $entities = $this->createEntityChainBelongingToUser($newUser, $newUser);
 | 
				
			||||||
        Activity::addForEntity($entities['book'], ActivityType::BOOK_UPDATE);
 | 
					        Activity::add(ActivityType::BOOK_UPDATE, $entities['book']);
 | 
				
			||||||
        Activity::addForEntity($entities['page'], ActivityType::PAGE_CREATE);
 | 
					        Activity::add(ActivityType::PAGE_CREATE, $entities['page']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $linkSelector = '#recent-activity a[href$="/user/' . $newUser->slug . '"]';
 | 
					        $linkSelector = '#recent-activity a[href$="/user/' . $newUser->slug . '"]';
 | 
				
			||||||
        $this->asAdmin()->get('/')
 | 
					        $this->asAdmin()->get('/')
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue