Entity Repo & Controller Refactor (#1690)
* Started mass-refactoring of the current entity repos * Rewrote book tree logic - Now does two simple queries instead of one really complex one. - Extracted logic into its own class. - Remove model-level akward union field listing. - Logic now more readable than being large separate query and compilation functions. * Extracted and split book sort logic * Finished up Book controller/repo organisation * Refactored bookshelves controllers and repo parts * Fixed issues found via phpunit * Refactored Chapter controller * Updated Chapter export controller * Started Page controller/repo refactor * Refactored another chunk of PageController * Completed initial pagecontroller refactor pass * Fixed tests and continued reduction of old repos * Removed old page remove and further reduced entity repo * Removed old entity repo, split out page controller * Ran phpcbf and split out some page content methods * Tidied up some EntityProvider elements * Fixed issued caused by viewservice change
This commit is contained in:
		
							parent
							
								
									7cd956b24b
								
							
						
					
					
						commit
						31f5786e01
					
				| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
<?php namespace BookStack\Actions;
 | 
			
		||||
 | 
			
		||||
use BookStack\Auth\Permissions\PermissionService;
 | 
			
		||||
use BookStack\Entities\Book;
 | 
			
		||||
use BookStack\Entities\Entity;
 | 
			
		||||
 | 
			
		||||
class ActivityService
 | 
			
		||||
| 
						 | 
				
			
			@ -68,7 +69,7 @@ class ActivityService
 | 
			
		|||
     * 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.
 | 
			
		||||
     * @param Entity $entity
 | 
			
		||||
     * @param \BookStack\Entities\Entity $entity
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     */
 | 
			
		||||
    public function removeEntity(Entity $entity)
 | 
			
		||||
| 
						 | 
				
			
			@ -106,7 +107,7 @@ class ActivityService
 | 
			
		|||
    /**
 | 
			
		||||
     * Gets the latest activity for an entity, Filtering out similar
 | 
			
		||||
     * items to prevent a message activity list.
 | 
			
		||||
     * @param Entity $entity
 | 
			
		||||
     * @param \BookStack\Entities\Entity $entity
 | 
			
		||||
     * @param int $count
 | 
			
		||||
     * @param int $page
 | 
			
		||||
     * @return array
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
<?php namespace BookStack\Actions;
 | 
			
		||||
 | 
			
		||||
use BookStack\Auth\Permissions\PermissionService;
 | 
			
		||||
use BookStack\Entities\Book;
 | 
			
		||||
use BookStack\Entities\Entity;
 | 
			
		||||
use BookStack\Entities\EntityProvider;
 | 
			
		||||
use DB;
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +45,7 @@ class ViewService
 | 
			
		|||
        }
 | 
			
		||||
 | 
			
		||||
        // Otherwise create new view count
 | 
			
		||||
        $entity->views()->save($this->view->create([
 | 
			
		||||
        $entity->views()->save($this->view->newInstance([
 | 
			
		||||
            'user_id' => $user->id,
 | 
			
		||||
            'views' => 1
 | 
			
		||||
        ]));
 | 
			
		||||
| 
						 | 
				
			
			@ -60,7 +61,7 @@ class ViewService
 | 
			
		|||
     * @param string $action - used for permission checking
 | 
			
		||||
     * @return Collection
 | 
			
		||||
     */
 | 
			
		||||
    public function getPopular(int $count = 10, int $page = 0, $filterModels = null, string $action = 'view')
 | 
			
		||||
    public function getPopular(int $count = 10, int $page = 0, array $filterModels = null, string $action = 'view')
 | 
			
		||||
    {
 | 
			
		||||
        $skipCount = $count * $page;
 | 
			
		||||
        $query = $this->permissionService
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -633,42 +633,40 @@ class PermissionService
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the children of a book in an efficient single query, Filtered by the permission system.
 | 
			
		||||
     * @param integer $book_id
 | 
			
		||||
     * @param bool $filterDrafts
 | 
			
		||||
     * @param bool $fetchPageContent
 | 
			
		||||
     * @return QueryBuilder
 | 
			
		||||
     * Limited the given entity query so that the query will only
 | 
			
		||||
     * return items that the user has permission for the given ability.
 | 
			
		||||
     */
 | 
			
		||||
    public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false)
 | 
			
		||||
    public function restrictEntityQuery(Builder $query, string $ability = 'view'): Builder
 | 
			
		||||
    {
 | 
			
		||||
        $entities = $this->entityProvider;
 | 
			
		||||
        $pageSelect = $this->db->table('pages')->selectRaw($entities->page->entityRawQuery($fetchPageContent))
 | 
			
		||||
            ->where('book_id', '=', $book_id)->where(function ($query) use ($filterDrafts) {
 | 
			
		||||
                $query->where('draft', '=', 0);
 | 
			
		||||
                if (!$filterDrafts) {
 | 
			
		||||
                    $query->orWhere(function ($query) {
 | 
			
		||||
                        $query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        $chapterSelect = $this->db->table('chapters')->selectRaw($entities->chapter->entityRawQuery())->where('book_id', '=', $book_id);
 | 
			
		||||
        $query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
 | 
			
		||||
            ->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
 | 
			
		||||
 | 
			
		||||
        // Add joint permission filter
 | 
			
		||||
        $whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
 | 
			
		||||
            ->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
 | 
			
		||||
            ->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
 | 
			
		||||
            ->where(function ($query) {
 | 
			
		||||
                $query->where('jp.has_permission', '=', 1)->orWhere(function ($query) {
 | 
			
		||||
                    $query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        $query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
 | 
			
		||||
 | 
			
		||||
        $query->orderBy('draft', 'desc')->orderBy('priority', 'asc');
 | 
			
		||||
        $this->clean();
 | 
			
		||||
        return  $query;
 | 
			
		||||
        return $query->where(function (Builder $parentQuery) use ($ability) {
 | 
			
		||||
            $parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) use ($ability) {
 | 
			
		||||
                $permissionQuery->whereIn('role_id', $this->getRoles())
 | 
			
		||||
                    ->where('action', '=', $ability)
 | 
			
		||||
                    ->where(function (Builder $query) {
 | 
			
		||||
                        $query->where('has_permission', '=', true)
 | 
			
		||||
                            ->orWhere(function (Builder $query) {
 | 
			
		||||
                                $query->where('has_permission_own', '=', true)
 | 
			
		||||
                                    ->where('created_by', '=', $this->currentUser()->id);
 | 
			
		||||
                            });
 | 
			
		||||
                    });
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Extend the given page query to ensure draft items are not visible
 | 
			
		||||
     * unless created by the given user.
 | 
			
		||||
     */
 | 
			
		||||
    public function enforceDraftVisiblityOnQuery(Builder $query): Builder
 | 
			
		||||
    {
 | 
			
		||||
        return $query->where(function (Builder $query) {
 | 
			
		||||
            $query->where('draft', '=', false)
 | 
			
		||||
                ->orWhere(function (Builder $query) {
 | 
			
		||||
                    $query->where('draft', '=', true)
 | 
			
		||||
                        ->where('created_by', '=', $this->currentUser()->id);
 | 
			
		||||
                });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -75,7 +75,7 @@ class Role extends Model
 | 
			
		|||
     */
 | 
			
		||||
    public static function getRole($roleName)
 | 
			
		||||
    {
 | 
			
		||||
        return static::where('name', '=', $roleName)->first();
 | 
			
		||||
        return static::query()->where('name', '=', $roleName)->first();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			@ -85,7 +85,7 @@ class Role extends Model
 | 
			
		|||
     */
 | 
			
		||||
    public static function getSystemRole($roleName)
 | 
			
		||||
    {
 | 
			
		||||
        return static::where('system_name', '=', $roleName)->first();
 | 
			
		||||
        return static::query()->where('system_name', '=', $roleName)->first();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			@ -94,6 +94,15 @@ class Role extends Model
 | 
			
		|||
     */
 | 
			
		||||
    public static function visible()
 | 
			
		||||
    {
 | 
			
		||||
        return static::where('hidden', '=', false)->orderBy('name')->get();
 | 
			
		||||
        return static::query()->where('hidden', '=', false)->orderBy('name')->get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the roles that can be restricted.
 | 
			
		||||
     * @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection
 | 
			
		||||
     */
 | 
			
		||||
    public static function restrictable()
 | 
			
		||||
    {
 | 
			
		||||
        return static::query()->where('system_name', '!=', 'admin')->get();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -53,13 +53,24 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
 | 
			
		|||
     */
 | 
			
		||||
    protected $permissions;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * This holds the default user when loaded.
 | 
			
		||||
     * @var null|User
 | 
			
		||||
     */
 | 
			
		||||
    protected static $defaultUser = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the default public user.
 | 
			
		||||
     * @return User
 | 
			
		||||
     */
 | 
			
		||||
    public static function getDefault()
 | 
			
		||||
    {
 | 
			
		||||
        return static::where('system_name', '=', 'public')->first();
 | 
			
		||||
        if (!is_null(static::$defaultUser)) {
 | 
			
		||||
            return static::$defaultUser;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        static::$defaultUser = static::where('system_name', '=', 'public')->first();
 | 
			
		||||
        return static::$defaultUser;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,32 +1,31 @@
 | 
			
		|||
<?php namespace BookStack\Auth;
 | 
			
		||||
 | 
			
		||||
use Activity;
 | 
			
		||||
use BookStack\Entities\Repos\EntityRepo;
 | 
			
		||||
use BookStack\Entities\Book;
 | 
			
		||||
use BookStack\Entities\Bookshelf;
 | 
			
		||||
use BookStack\Entities\Chapter;
 | 
			
		||||
use BookStack\Entities\Page;
 | 
			
		||||
use BookStack\Exceptions\NotFoundException;
 | 
			
		||||
use BookStack\Exceptions\UserUpdateException;
 | 
			
		||||
use BookStack\Uploads\Image;
 | 
			
		||||
use Exception;
 | 
			
		||||
use Illuminate\Database\Eloquent\Builder;
 | 
			
		||||
use Images;
 | 
			
		||||
use Log;
 | 
			
		||||
 | 
			
		||||
class UserRepo
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    protected $user;
 | 
			
		||||
    protected $role;
 | 
			
		||||
    protected $entityRepo;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * UserRepo constructor.
 | 
			
		||||
     * @param User $user
 | 
			
		||||
     * @param Role $role
 | 
			
		||||
     * @param EntityRepo $entityRepo
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(User $user, Role $role, EntityRepo $entityRepo)
 | 
			
		||||
    public function __construct(User $user, Role $role)
 | 
			
		||||
    {
 | 
			
		||||
        $this->user = $user;
 | 
			
		||||
        $this->role = $role;
 | 
			
		||||
        $this->entityRepo = $entityRepo;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			@ -81,7 +80,7 @@ class UserRepo
 | 
			
		|||
     * Creates a new user and attaches a role to them.
 | 
			
		||||
     * @param array $data
 | 
			
		||||
     * @param boolean $verifyEmail
 | 
			
		||||
     * @return \BookStack\Auth\User
 | 
			
		||||
     * @return User
 | 
			
		||||
     */
 | 
			
		||||
    public function registerNew(array $data, $verifyEmail = false)
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -121,7 +120,7 @@ class UserRepo
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks if the give user is the only admin.
 | 
			
		||||
     * @param \BookStack\Auth\User $user
 | 
			
		||||
     * @param User $user
 | 
			
		||||
     * @return bool
 | 
			
		||||
     */
 | 
			
		||||
    public function isOnlyAdmin(User $user)
 | 
			
		||||
| 
						 | 
				
			
			@ -175,7 +174,7 @@ class UserRepo
 | 
			
		|||
     * Create a new basic instance of user.
 | 
			
		||||
     * @param array $data
 | 
			
		||||
     * @param boolean $verifyEmail
 | 
			
		||||
     * @return \BookStack\Auth\User
 | 
			
		||||
     * @return User
 | 
			
		||||
     */
 | 
			
		||||
    public function create(array $data, $verifyEmail = false)
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -189,7 +188,7 @@ class UserRepo
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove the given user from storage, Delete all related content.
 | 
			
		||||
     * @param \BookStack\Auth\User $user
 | 
			
		||||
     * @param User $user
 | 
			
		||||
     * @throws Exception
 | 
			
		||||
     */
 | 
			
		||||
    public function destroy(User $user)
 | 
			
		||||
| 
						 | 
				
			
			@ -206,7 +205,7 @@ class UserRepo
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the latest activity for a user.
 | 
			
		||||
     * @param \BookStack\Auth\User $user
 | 
			
		||||
     * @param User $user
 | 
			
		||||
     * @param int $count
 | 
			
		||||
     * @param int $page
 | 
			
		||||
     * @return array
 | 
			
		||||
| 
						 | 
				
			
			@ -218,36 +217,35 @@ class UserRepo
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the recently created content for this given user.
 | 
			
		||||
     * @param \BookStack\Auth\User $user
 | 
			
		||||
     * @param int $count
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     */
 | 
			
		||||
    public function getRecentlyCreated(User $user, $count = 20)
 | 
			
		||||
    public function getRecentlyCreated(User $user, int $count = 20): array
 | 
			
		||||
    {
 | 
			
		||||
        $createdByUserQuery = function (Builder $query) use ($user) {
 | 
			
		||||
            $query->where('created_by', '=', $user->id);
 | 
			
		||||
        $query = function (Builder $query) use ($user, $count) {
 | 
			
		||||
            return $query->orderBy('created_at', 'desc')
 | 
			
		||||
                ->where('created_by', '=', $user->id)
 | 
			
		||||
                ->take($count)
 | 
			
		||||
                ->get();
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            'pages'    => $this->entityRepo->getRecentlyCreated('page', $count, 0, $createdByUserQuery),
 | 
			
		||||
            'chapters' => $this->entityRepo->getRecentlyCreated('chapter', $count, 0, $createdByUserQuery),
 | 
			
		||||
            'books'    => $this->entityRepo->getRecentlyCreated('book', $count, 0, $createdByUserQuery),
 | 
			
		||||
            'shelves'  => $this->entityRepo->getRecentlyCreated('bookshelf', $count, 0, $createdByUserQuery)
 | 
			
		||||
            'pages'    => $query(Page::visible()->where('draft', '=', false)),
 | 
			
		||||
            'chapters' => $query(Chapter::visible()),
 | 
			
		||||
            'books'    => $query(Book::visible()),
 | 
			
		||||
            'shelves'  => $query(Bookshelf::visible()),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get asset created counts for the give user.
 | 
			
		||||
     * @param \BookStack\Auth\User $user
 | 
			
		||||
     * @return array
 | 
			
		||||
     */
 | 
			
		||||
    public function getAssetCounts(User $user)
 | 
			
		||||
    public function getAssetCounts(User $user): array
 | 
			
		||||
    {
 | 
			
		||||
        $createdBy = ['created_by' => $user->id];
 | 
			
		||||
        return [
 | 
			
		||||
            'pages'    => $this->entityRepo->getUserTotalCreated('page', $user),
 | 
			
		||||
            'chapters' => $this->entityRepo->getUserTotalCreated('chapter', $user),
 | 
			
		||||
            'books'    => $this->entityRepo->getUserTotalCreated('book', $user),
 | 
			
		||||
            'shelves'    => $this->entityRepo->getUserTotalCreated('bookshelf', $user),
 | 
			
		||||
            'pages'    =>  Page::visible()->where($createdBy)->count(),
 | 
			
		||||
            'chapters'    =>  Chapter::visible()->where($createdBy)->count(),
 | 
			
		||||
            'books'    =>  Book::visible()->where($createdBy)->count(),
 | 
			
		||||
            'shelves'    =>  Bookshelf::visible()->where($createdBy)->count(),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -260,16 +258,6 @@ class UserRepo
 | 
			
		|||
        return $this->role->newQuery()->orderBy('name', 'asc')->get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all the roles which can be given restricted access to
 | 
			
		||||
     * other entities in the system.
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     */
 | 
			
		||||
    public function getRestrictableRoles()
 | 
			
		||||
    {
 | 
			
		||||
        return $this->role->where('system_name', '!=', 'admin')->get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an avatar image for a user and set it as their avatar.
 | 
			
		||||
     * Returns early if avatars disabled or not set in config.
 | 
			
		||||
| 
						 | 
				
			
			@ -288,7 +276,7 @@ class UserRepo
 | 
			
		|||
            $user->save();
 | 
			
		||||
            return true;
 | 
			
		||||
        } catch (Exception $e) {
 | 
			
		||||
            \Log::error('Failed to save user avatar image');
 | 
			
		||||
            Log::error('Failed to save user avatar image');
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,11 @@
 | 
			
		|||
<?php namespace BookStack\Entities;
 | 
			
		||||
 | 
			
		||||
use BookStack\Uploads\Image;
 | 
			
		||||
use Exception;
 | 
			
		||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 | 
			
		||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 | 
			
		||||
use Illuminate\Database\Eloquent\Relations\HasMany;
 | 
			
		||||
use Illuminate\Support\Collection;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class Book
 | 
			
		||||
| 
						 | 
				
			
			@ -9,21 +14,12 @@ use BookStack\Uploads\Image;
 | 
			
		|||
 * @property Image|null $cover
 | 
			
		||||
 * @package BookStack\Entities
 | 
			
		||||
 */
 | 
			
		||||
class Book extends Entity
 | 
			
		||||
class Book extends Entity implements HasCoverImage
 | 
			
		||||
{
 | 
			
		||||
    public $searchFactor = 2;
 | 
			
		||||
 | 
			
		||||
    protected $fillable = ['name', 'description', 'image_id'];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the morph class for this model.
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    public function getMorphClass()
 | 
			
		||||
    {
 | 
			
		||||
        return 'BookStack\\Book';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the url for this book.
 | 
			
		||||
     * @param string|bool $path
 | 
			
		||||
| 
						 | 
				
			
			@ -52,7 +48,7 @@ class Book extends Entity
 | 
			
		|||
 | 
			
		||||
        try {
 | 
			
		||||
            $cover = $this->cover ? url($this->cover->getThumb($width, $height, false)) : $default;
 | 
			
		||||
        } catch (\Exception $err) {
 | 
			
		||||
        } catch (Exception $err) {
 | 
			
		||||
            $cover = $default;
 | 
			
		||||
        }
 | 
			
		||||
        return $cover;
 | 
			
		||||
| 
						 | 
				
			
			@ -60,16 +56,23 @@ class Book extends Entity
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the cover image of the book
 | 
			
		||||
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
 | 
			
		||||
     */
 | 
			
		||||
    public function cover()
 | 
			
		||||
    public function cover(): BelongsTo
 | 
			
		||||
    {
 | 
			
		||||
        return $this->belongsTo(Image::class, 'image_id');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the type of the image model that is used when storing a cover image.
 | 
			
		||||
     */
 | 
			
		||||
    public function coverImageTypeKey(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'cover_book';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all pages within this book.
 | 
			
		||||
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
 | 
			
		||||
     * @return HasMany
 | 
			
		||||
     */
 | 
			
		||||
    public function pages()
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -78,7 +81,7 @@ class Book extends Entity
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the direct child pages of this book.
 | 
			
		||||
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
 | 
			
		||||
     * @return HasMany
 | 
			
		||||
     */
 | 
			
		||||
    public function directPages()
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -87,7 +90,7 @@ class Book extends Entity
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all chapters within this book.
 | 
			
		||||
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
 | 
			
		||||
     * @return HasMany
 | 
			
		||||
     */
 | 
			
		||||
    public function chapters()
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -96,13 +99,24 @@ class Book extends Entity
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the shelves this book is contained within.
 | 
			
		||||
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
 | 
			
		||||
     * @return BelongsToMany
 | 
			
		||||
     */
 | 
			
		||||
    public function shelves()
 | 
			
		||||
    {
 | 
			
		||||
        return $this->belongsToMany(Bookshelf::class, 'bookshelves_books', 'book_id', 'bookshelf_id');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the direct child items within this book.
 | 
			
		||||
     * @return Collection
 | 
			
		||||
     */
 | 
			
		||||
    public function getDirectChildren(): Collection
 | 
			
		||||
    {
 | 
			
		||||
        $pages = $this->directPages()->visible()->get();
 | 
			
		||||
        $chapters = $this->chapters()->visible()->get();
 | 
			
		||||
        return $pages->contact($chapters)->sortBy('priority')->sortByDesc('draft');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an excerpt of this book's description to the specified length or less.
 | 
			
		||||
     * @param int $length
 | 
			
		||||
| 
						 | 
				
			
			@ -113,13 +127,4 @@ class Book extends Entity
 | 
			
		|||
        $description = $this->description;
 | 
			
		||||
        return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return a generalised, common raw query that can be 'unioned' across entities.
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    public function entityRawQuery()
 | 
			
		||||
    {
 | 
			
		||||
        return "'BookStack\\\\Book' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,14 +1,31 @@
 | 
			
		|||
<?php namespace BookStack\Entities;
 | 
			
		||||
 | 
			
		||||
use Illuminate\Database\Eloquent\Builder;
 | 
			
		||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class BookChild
 | 
			
		||||
 * @property int $book_id
 | 
			
		||||
 * @property int $priority
 | 
			
		||||
 * @property Book $book
 | 
			
		||||
 * @method Builder whereSlugs(string $bookSlug, string $childSlug)
 | 
			
		||||
 */
 | 
			
		||||
class BookChild extends Entity
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Scope a query to find items where the the child has the given childSlug
 | 
			
		||||
     * where its parent has the bookSlug.
 | 
			
		||||
     */
 | 
			
		||||
    public function scopeWhereSlugs(Builder $query, string $bookSlug, string $childSlug)
 | 
			
		||||
    {
 | 
			
		||||
        return $query->with('book')
 | 
			
		||||
            ->whereHas('book', function (Builder $query) use ($bookSlug) {
 | 
			
		||||
                $query->where('slug', '=', $bookSlug);
 | 
			
		||||
            })
 | 
			
		||||
            ->where('slug', '=', $childSlug);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the book this page sits in.
 | 
			
		||||
     * @return BelongsTo
 | 
			
		||||
| 
						 | 
				
			
			@ -18,4 +35,26 @@ class BookChild extends Entity
 | 
			
		|||
        return $this->belongsTo(Book::class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
    /**
 | 
			
		||||
     * Change the book that this entity belongs to.
 | 
			
		||||
     */
 | 
			
		||||
    public function changeBook(int $newBookId): Entity
 | 
			
		||||
    {
 | 
			
		||||
        $this->book_id = $newBookId;
 | 
			
		||||
        $this->refreshSlug();
 | 
			
		||||
        $this->save();
 | 
			
		||||
        $this->refresh();
 | 
			
		||||
 | 
			
		||||
        // Update related activity
 | 
			
		||||
        $this->activity()->update(['book_id' => $newBookId]);
 | 
			
		||||
 | 
			
		||||
        // Update all child pages if a chapter
 | 
			
		||||
        if ($this instanceof Chapter) {
 | 
			
		||||
            foreach ($this->pages as $page) {
 | 
			
		||||
                $page->changeBook($newBookId);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,10 @@
 | 
			
		|||
<?php namespace BookStack\Entities;
 | 
			
		||||
 | 
			
		||||
use BookStack\Uploads\Image;
 | 
			
		||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 | 
			
		||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 | 
			
		||||
 | 
			
		||||
class Bookshelf extends Entity
 | 
			
		||||
class Bookshelf extends Entity implements HasCoverImage
 | 
			
		||||
{
 | 
			
		||||
    protected $table = 'bookshelves';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -10,15 +12,6 @@ class Bookshelf extends Entity
 | 
			
		|||
 | 
			
		||||
    protected $fillable = ['name', 'description', 'image_id'];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the morph class for this model.
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    public function getMorphClass()
 | 
			
		||||
    {
 | 
			
		||||
        return 'BookStack\\Bookshelf';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the books in this shelf.
 | 
			
		||||
     * Should not be used directly since does not take into account permissions.
 | 
			
		||||
| 
						 | 
				
			
			@ -31,6 +24,14 @@ class Bookshelf extends Entity
 | 
			
		|||
            ->orderBy('order', 'asc');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Related books that are visible to the current user.
 | 
			
		||||
     */
 | 
			
		||||
    public function visibleBooks(): BelongsToMany
 | 
			
		||||
    {
 | 
			
		||||
        return $this->books()->visible();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the url for this bookshelf.
 | 
			
		||||
     * @param string|bool $path
 | 
			
		||||
| 
						 | 
				
			
			@ -68,13 +69,20 @@ class Bookshelf extends Entity
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the cover image of the shelf
 | 
			
		||||
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
 | 
			
		||||
     */
 | 
			
		||||
    public function cover()
 | 
			
		||||
    public function cover(): BelongsTo
 | 
			
		||||
    {
 | 
			
		||||
        return $this->belongsTo(Image::class, 'image_id');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the type of the image model that is used when storing a cover image.
 | 
			
		||||
     */
 | 
			
		||||
    public function coverImageTypeKey(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'cover_shelf';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an excerpt of this book's description to the specified length or less.
 | 
			
		||||
     * @param int $length
 | 
			
		||||
| 
						 | 
				
			
			@ -86,21 +94,12 @@ class Bookshelf extends Entity
 | 
			
		|||
        return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return a generalised, common raw query that can be 'unioned' across entities.
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    public function entityRawQuery()
 | 
			
		||||
    {
 | 
			
		||||
        return "'BookStack\\\\BookShelf' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if this shelf contains the given book.
 | 
			
		||||
     * @param Book $book
 | 
			
		||||
     * @return bool
 | 
			
		||||
     */
 | 
			
		||||
    public function contains(Book $book): bool 
 | 
			
		||||
    public function contains(Book $book): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->books()->where('id', '=', $book->id)->count() > 0;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -111,11 +110,11 @@ class Bookshelf extends Entity
 | 
			
		|||
     */
 | 
			
		||||
    public function appendBook(Book $book)
 | 
			
		||||
    {
 | 
			
		||||
       if ($this->contains($book)) {
 | 
			
		||||
           return;
 | 
			
		||||
       }
 | 
			
		||||
        if ($this->contains($book)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
       $maxOrder = $this->books()->max('order');
 | 
			
		||||
       $this->books()->attach($book->id, ['order' => $maxOrder + 1]);
 | 
			
		||||
        $maxOrder = $this->books()->max('order');
 | 
			
		||||
        $this->books()->attach($book->id, ['order' => $maxOrder + 1]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
<?php namespace BookStack\Entities;
 | 
			
		||||
 | 
			
		||||
use BookStack\Entities\Managers\EntityContext;
 | 
			
		||||
use Illuminate\View\View;
 | 
			
		||||
 | 
			
		||||
class BreadcrumbsViewComposer
 | 
			
		||||
| 
						 | 
				
			
			@ -9,9 +10,9 @@ class BreadcrumbsViewComposer
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * BreadcrumbsViewComposer constructor.
 | 
			
		||||
     * @param EntityContextManager $entityContextManager
 | 
			
		||||
     * @param EntityContext $entityContextManager
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(EntityContextManager $entityContextManager)
 | 
			
		||||
    public function __construct(EntityContext $entityContextManager)
 | 
			
		||||
    {
 | 
			
		||||
        $this->entityContextManager = $entityContextManager;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,22 +1,18 @@
 | 
			
		|||
<?php namespace BookStack\Entities;
 | 
			
		||||
 | 
			
		||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 | 
			
		||||
use Illuminate\Support\Collection;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class Chapter
 | 
			
		||||
 * @property Collection<Page> $pages
 | 
			
		||||
 * @package BookStack\Entities
 | 
			
		||||
 */
 | 
			
		||||
class Chapter extends BookChild
 | 
			
		||||
{
 | 
			
		||||
    public $searchFactor = 1.3;
 | 
			
		||||
 | 
			
		||||
    protected $fillable = ['name', 'description', 'priority', 'book_id'];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the morph class for this model.
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    public function getMorphClass()
 | 
			
		||||
    {
 | 
			
		||||
        return 'BookStack\\Chapter';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the pages that this chapter contains.
 | 
			
		||||
     * @param string $dir
 | 
			
		||||
| 
						 | 
				
			
			@ -55,15 +51,6 @@ class Chapter extends BookChild
 | 
			
		|||
        return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return a generalised, common raw query that can be 'unioned' across entities.
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    public function entityRawQuery()
 | 
			
		||||
    {
 | 
			
		||||
        return "'BookStack\\\\Chapter' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, '' as html, book_id, priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if this chapter has any child pages.
 | 
			
		||||
     * @return bool
 | 
			
		||||
| 
						 | 
				
			
			@ -72,4 +59,15 @@ class Chapter extends BookChild
 | 
			
		|||
    {
 | 
			
		||||
        return count($this->pages) > 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the visible pages in this chapter.
 | 
			
		||||
     */
 | 
			
		||||
    public function getVisiblePages(): Collection
 | 
			
		||||
    {
 | 
			
		||||
        return $this->pages()->visible()
 | 
			
		||||
        ->orderBy('draft', 'desc')
 | 
			
		||||
        ->orderBy('priority', 'asc')
 | 
			
		||||
        ->get();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,8 @@ use BookStack\Auth\Permissions\JointPermission;
 | 
			
		|||
use BookStack\Facades\Permissions;
 | 
			
		||||
use BookStack\Ownable;
 | 
			
		||||
use Carbon\Carbon;
 | 
			
		||||
use Illuminate\Database\Eloquent\Builder;
 | 
			
		||||
use Illuminate\Database\Eloquent\Collection;
 | 
			
		||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			@ -24,6 +26,11 @@ use Illuminate\Database\Eloquent\Relations\MorphMany;
 | 
			
		|||
 * @property int $created_by
 | 
			
		||||
 * @property int $updated_by
 | 
			
		||||
 * @property boolean $restricted
 | 
			
		||||
 * @property Collection $tags
 | 
			
		||||
 * @method static Entity|Builder visible()
 | 
			
		||||
 * @method static Entity|Builder hasPermission(string $permission)
 | 
			
		||||
 * @method static Builder withLastView()
 | 
			
		||||
 * @method static Builder withViewCount()
 | 
			
		||||
 *
 | 
			
		||||
 * @package BookStack\Entities
 | 
			
		||||
 */
 | 
			
		||||
| 
						 | 
				
			
			@ -41,14 +48,45 @@ class Entity extends Ownable
 | 
			
		|||
    public $searchFactor = 1.0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the morph class for this model.
 | 
			
		||||
     * Set here since, due to folder changes, the namespace used
 | 
			
		||||
     * in the database no longer matches the class namespace.
 | 
			
		||||
     * @return string
 | 
			
		||||
     * Get the entities that are visible to the current user.
 | 
			
		||||
     */
 | 
			
		||||
    public function getMorphClass()
 | 
			
		||||
    public function scopeVisible(Builder $query)
 | 
			
		||||
    {
 | 
			
		||||
        return 'BookStack\\Entity';
 | 
			
		||||
        return $this->scopeHasPermission($query, 'view');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Scope the query to those entities that the current user has the given permission for.
 | 
			
		||||
     */
 | 
			
		||||
    public function scopeHasPermission(Builder $query, string $permission)
 | 
			
		||||
    {
 | 
			
		||||
        return Permissions::restrictEntityQuery($query, $permission);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Query scope to get the last view from the current user.
 | 
			
		||||
     */
 | 
			
		||||
    public function scopeWithLastView(Builder $query)
 | 
			
		||||
    {
 | 
			
		||||
        $viewedAtQuery = View::query()->select('updated_at')
 | 
			
		||||
            ->whereColumn('viewable_id', '=', $this->getTable() . '.id')
 | 
			
		||||
            ->where('viewable_type', '=', $this->getMorphClass())
 | 
			
		||||
            ->where('user_id', '=', user()->id)
 | 
			
		||||
            ->take(1);
 | 
			
		||||
 | 
			
		||||
        return $query->addSelect(['last_viewed_at' => $viewedAtQuery]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Query scope to get the total view count of the entities.
 | 
			
		||||
     */
 | 
			
		||||
    public function scopeWithViewCount(Builder $query)
 | 
			
		||||
    {
 | 
			
		||||
        $viewCountQuery = View::query()->selectRaw('SUM(views) as view_count')
 | 
			
		||||
            ->whereColumn('viewable_id', '=', $this->getTable() . '.id')
 | 
			
		||||
            ->where('viewable_type', '=', $this->getMorphClass())->take(1);
 | 
			
		||||
 | 
			
		||||
        $query->addSelect(['view_count' => $viewCountQuery]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			@ -88,7 +126,7 @@ class Entity extends Ownable
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the activity objects for this entity.
 | 
			
		||||
     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
 | 
			
		||||
     * @return MorphMany
 | 
			
		||||
     */
 | 
			
		||||
    public function activity()
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -106,7 +144,7 @@ class Entity extends Ownable
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the Tag models that have been user assigned to this entity.
 | 
			
		||||
     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
 | 
			
		||||
     * @return MorphMany
 | 
			
		||||
     */
 | 
			
		||||
    public function tags()
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -126,7 +164,7 @@ class Entity extends Ownable
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the related search terms.
 | 
			
		||||
     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
 | 
			
		||||
     * @return MorphMany
 | 
			
		||||
     */
 | 
			
		||||
    public function searchTerms()
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -155,7 +193,7 @@ class Entity extends Ownable
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the entity jointPermissions this is connected to.
 | 
			
		||||
     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
 | 
			
		||||
     * @return MorphMany
 | 
			
		||||
     */
 | 
			
		||||
    public function jointPermissions()
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -182,14 +220,6 @@ class Entity extends Ownable
 | 
			
		|||
        return strtolower(static::getClassName());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the type of this entity.
 | 
			
		||||
     */
 | 
			
		||||
    public function type(): string
 | 
			
		||||
    {
 | 
			
		||||
        return static::getType();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an instance of an entity of the given type.
 | 
			
		||||
     * @param $type
 | 
			
		||||
| 
						 | 
				
			
			@ -242,15 +272,6 @@ class Entity extends Ownable
 | 
			
		|||
        return trim($text);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return a generalised, common raw query that can be 'unioned' across entities.
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    public function entityRawQuery()
 | 
			
		||||
    {
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the url of this entity
 | 
			
		||||
     * @param $path
 | 
			
		||||
| 
						 | 
				
			
			@ -270,6 +291,15 @@ class Entity extends Ownable
 | 
			
		|||
        Permissions::buildJointPermissionsForEntity($this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Index the current entity for search
 | 
			
		||||
     */
 | 
			
		||||
    public function indexForSearch()
 | 
			
		||||
    {
 | 
			
		||||
        $searchService = app()->make(SearchService::class);
 | 
			
		||||
        $searchService->indexEntity($this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Generate and set a new URL slug for this model.
 | 
			
		||||
     */
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -39,11 +39,6 @@ class EntityProvider
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * EntityProvider constructor.
 | 
			
		||||
     * @param Bookshelf $bookshelf
 | 
			
		||||
     * @param Book $book
 | 
			
		||||
     * @param Chapter $chapter
 | 
			
		||||
     * @param Page $page
 | 
			
		||||
     * @param PageRevision $pageRevision
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        Bookshelf $bookshelf,
 | 
			
		||||
| 
						 | 
				
			
			@ -62,9 +57,8 @@ class EntityProvider
 | 
			
		|||
    /**
 | 
			
		||||
     * Fetch all core entity types as an associated array
 | 
			
		||||
     * with their basic names as the keys.
 | 
			
		||||
     * @return Entity[]
 | 
			
		||||
     */
 | 
			
		||||
    public function all()
 | 
			
		||||
    public function all(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            'bookshelf' => $this->bookshelf,
 | 
			
		||||
| 
						 | 
				
			
			@ -76,10 +70,8 @@ class EntityProvider
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an entity instance by it's basic name.
 | 
			
		||||
     * @param string $type
 | 
			
		||||
     * @return Entity
 | 
			
		||||
     */
 | 
			
		||||
    public function get(string $type)
 | 
			
		||||
    public function get(string $type): Entity
 | 
			
		||||
    {
 | 
			
		||||
        $type = strtolower($type);
 | 
			
		||||
        return $this->all()[$type];
 | 
			
		||||
| 
						 | 
				
			
			@ -87,15 +79,9 @@ class EntityProvider
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the morph classes, as an array, for a single or multiple types.
 | 
			
		||||
     * @param string|array $types
 | 
			
		||||
     * @return array<string>
 | 
			
		||||
     */
 | 
			
		||||
    public function getMorphClasses($types)
 | 
			
		||||
    public function getMorphClasses(array $types): array
 | 
			
		||||
    {
 | 
			
		||||
        if (is_string($types)) {
 | 
			
		||||
            $types = [$types];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $morphClasses = [];
 | 
			
		||||
        foreach ($types as $type) {
 | 
			
		||||
            $model = $this->get($type);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,35 +1,34 @@
 | 
			
		|||
<?php namespace BookStack\Entities;
 | 
			
		||||
 | 
			
		||||
use BookStack\Entities\Repos\EntityRepo;
 | 
			
		||||
use BookStack\Entities\Managers\BookContents;
 | 
			
		||||
use BookStack\Entities\Managers\PageContent;
 | 
			
		||||
use BookStack\Uploads\ImageService;
 | 
			
		||||
use DomPDF;
 | 
			
		||||
use Exception;
 | 
			
		||||
use SnappyPDF;
 | 
			
		||||
use Throwable;
 | 
			
		||||
 | 
			
		||||
class ExportService
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    protected $entityRepo;
 | 
			
		||||
    protected $imageService;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * ExportService constructor.
 | 
			
		||||
     * @param EntityRepo $entityRepo
 | 
			
		||||
     * @param ImageService $imageService
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(EntityRepo $entityRepo, ImageService $imageService)
 | 
			
		||||
    public function __construct(ImageService $imageService)
 | 
			
		||||
    {
 | 
			
		||||
        $this->entityRepo = $entityRepo;
 | 
			
		||||
        $this->imageService = $imageService;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convert a page to a self-contained HTML file.
 | 
			
		||||
     * Includes required CSS & image content. Images are base64 encoded into the HTML.
 | 
			
		||||
     * @param \BookStack\Entities\Page $page
 | 
			
		||||
     * @return mixed|string
 | 
			
		||||
     * @throws \Throwable
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function pageToContainedHtml(Page $page)
 | 
			
		||||
    {
 | 
			
		||||
        $this->entityRepo->renderPage($page);
 | 
			
		||||
        $page->html = (new PageContent($page))->render();
 | 
			
		||||
        $pageHtml = view('pages/export', [
 | 
			
		||||
            'page' => $page
 | 
			
		||||
        ])->render();
 | 
			
		||||
| 
						 | 
				
			
			@ -38,15 +37,13 @@ class ExportService
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convert a chapter to a self-contained HTML file.
 | 
			
		||||
     * @param \BookStack\Entities\Chapter $chapter
 | 
			
		||||
     * @return mixed|string
 | 
			
		||||
     * @throws \Throwable
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function chapterToContainedHtml(Chapter $chapter)
 | 
			
		||||
    {
 | 
			
		||||
        $pages = $this->entityRepo->getChapterChildren($chapter);
 | 
			
		||||
        $pages = $chapter->getVisiblePages();
 | 
			
		||||
        $pages->each(function ($page) {
 | 
			
		||||
            $page->html = $this->entityRepo->renderPage($page);
 | 
			
		||||
            $page->html = (new PageContent($page))->render();
 | 
			
		||||
        });
 | 
			
		||||
        $html = view('chapters/export', [
 | 
			
		||||
            'chapter' => $chapter,
 | 
			
		||||
| 
						 | 
				
			
			@ -57,13 +54,11 @@ class ExportService
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convert a book to a self-contained HTML file.
 | 
			
		||||
     * @param Book $book
 | 
			
		||||
     * @return mixed|string
 | 
			
		||||
     * @throws \Throwable
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function bookToContainedHtml(Book $book)
 | 
			
		||||
    {
 | 
			
		||||
        $bookTree = $this->entityRepo->getBookChildren($book, true, true);
 | 
			
		||||
        $bookTree = (new BookContents($book))->getTree(false, true);
 | 
			
		||||
        $html = view('books/export', [
 | 
			
		||||
            'book' => $book,
 | 
			
		||||
            'bookChildren' => $bookTree
 | 
			
		||||
| 
						 | 
				
			
			@ -73,13 +68,11 @@ class ExportService
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convert a page to a PDF file.
 | 
			
		||||
     * @param Page $page
 | 
			
		||||
     * @return mixed|string
 | 
			
		||||
     * @throws \Throwable
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function pageToPdf(Page $page)
 | 
			
		||||
    {
 | 
			
		||||
        $this->entityRepo->renderPage($page);
 | 
			
		||||
        $page->html = (new PageContent($page))->render();
 | 
			
		||||
        $html = view('pages/pdf', [
 | 
			
		||||
            'page' => $page
 | 
			
		||||
        ])->render();
 | 
			
		||||
| 
						 | 
				
			
			@ -88,32 +81,30 @@ class ExportService
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convert a chapter to a PDF file.
 | 
			
		||||
     * @param \BookStack\Entities\Chapter $chapter
 | 
			
		||||
     * @return mixed|string
 | 
			
		||||
     * @throws \Throwable
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function chapterToPdf(Chapter $chapter)
 | 
			
		||||
    {
 | 
			
		||||
        $pages = $this->entityRepo->getChapterChildren($chapter);
 | 
			
		||||
        $pages = $chapter->getVisiblePages();
 | 
			
		||||
        $pages->each(function ($page) {
 | 
			
		||||
            $page->html = $this->entityRepo->renderPage($page);
 | 
			
		||||
            $page->html = (new PageContent($page))->render();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        $html = view('chapters/export', [
 | 
			
		||||
            'chapter' => $chapter,
 | 
			
		||||
            'pages' => $pages
 | 
			
		||||
        ])->render();
 | 
			
		||||
 | 
			
		||||
        return $this->htmlToPdf($html);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convert a book to a PDF file
 | 
			
		||||
     * @param \BookStack\Entities\Book $book
 | 
			
		||||
     * @return string
 | 
			
		||||
     * @throws \Throwable
 | 
			
		||||
     * Convert a book to a PDF file.
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function bookToPdf(Book $book)
 | 
			
		||||
    {
 | 
			
		||||
        $bookTree = $this->entityRepo->getBookChildren($book, true, true);
 | 
			
		||||
        $bookTree = (new BookContents($book))->getTree(false, true);
 | 
			
		||||
        $html = view('books/export', [
 | 
			
		||||
            'book' => $book,
 | 
			
		||||
            'bookChildren' => $bookTree
 | 
			
		||||
| 
						 | 
				
			
			@ -122,31 +113,27 @@ class ExportService
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convert normal webpage HTML to a PDF.
 | 
			
		||||
     * @param $html
 | 
			
		||||
     * @return string
 | 
			
		||||
     * @throws \Exception
 | 
			
		||||
     * Convert normal web-page HTML to a PDF.
 | 
			
		||||
     * @throws Exception
 | 
			
		||||
     */
 | 
			
		||||
    protected function htmlToPdf($html)
 | 
			
		||||
    protected function htmlToPdf(string $html): string
 | 
			
		||||
    {
 | 
			
		||||
        $containedHtml = $this->containHtml($html);
 | 
			
		||||
        $useWKHTML = config('snappy.pdf.binary') !== false;
 | 
			
		||||
        if ($useWKHTML) {
 | 
			
		||||
            $pdf = \SnappyPDF::loadHTML($containedHtml);
 | 
			
		||||
            $pdf = SnappyPDF::loadHTML($containedHtml);
 | 
			
		||||
            $pdf->setOption('print-media-type', true);
 | 
			
		||||
        } else {
 | 
			
		||||
            $pdf = \DomPDF::loadHTML($containedHtml);
 | 
			
		||||
            $pdf = DomPDF::loadHTML($containedHtml);
 | 
			
		||||
        }
 | 
			
		||||
        return $pdf->output();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Bundle of the contents of a html file to be self-contained.
 | 
			
		||||
     * @param $htmlContent
 | 
			
		||||
     * @return mixed|string
 | 
			
		||||
     * @throws \Exception
 | 
			
		||||
     * @throws Exception
 | 
			
		||||
     */
 | 
			
		||||
    protected function containHtml($htmlContent)
 | 
			
		||||
    protected function containHtml(string $htmlContent): string
 | 
			
		||||
    {
 | 
			
		||||
        $imageTagsOutput = [];
 | 
			
		||||
        preg_match_all("/\<img.*src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
 | 
			
		||||
| 
						 | 
				
			
			@ -188,12 +175,10 @@ class ExportService
 | 
			
		|||
    /**
 | 
			
		||||
     * Converts the page contents into simple plain text.
 | 
			
		||||
     * This method filters any bad looking content to provide a nice final output.
 | 
			
		||||
     * @param Page $page
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     */
 | 
			
		||||
    public function pageToPlainText(Page $page)
 | 
			
		||||
    public function pageToPlainText(Page $page): string
 | 
			
		||||
    {
 | 
			
		||||
        $html = $this->entityRepo->renderPage($page);
 | 
			
		||||
        $html = (new PageContent($page))->render();
 | 
			
		||||
        $text = strip_tags($html);
 | 
			
		||||
        // Replace multiple spaces with single spaces
 | 
			
		||||
        $text = preg_replace('/\ {2,}/', ' ', $text);
 | 
			
		||||
| 
						 | 
				
			
			@ -207,10 +192,8 @@ class ExportService
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convert a chapter into a plain text string.
 | 
			
		||||
     * @param \BookStack\Entities\Chapter $chapter
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    public function chapterToPlainText(Chapter $chapter)
 | 
			
		||||
    public function chapterToPlainText(Chapter $chapter): string
 | 
			
		||||
    {
 | 
			
		||||
        $text = $chapter->name . "\n\n";
 | 
			
		||||
        $text .= $chapter->description . "\n\n";
 | 
			
		||||
| 
						 | 
				
			
			@ -222,12 +205,10 @@ class ExportService
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convert a book into a plain text string.
 | 
			
		||||
     * @param Book $book
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    public function bookToPlainText(Book $book)
 | 
			
		||||
    public function bookToPlainText(Book $book): string
 | 
			
		||||
    {
 | 
			
		||||
        $bookTree = $this->entityRepo->getBookChildren($book, true, true);
 | 
			
		||||
        $bookTree = (new BookContents($book))->getTree(false, true);
 | 
			
		||||
        $text = $book->name . "\n\n";
 | 
			
		||||
        foreach ($bookTree as $bookChild) {
 | 
			
		||||
            if ($bookChild->isA('chapter')) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
namespace BookStack\Entities;
 | 
			
		||||
 | 
			
		||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 | 
			
		||||
 | 
			
		||||
interface HasCoverImage
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the cover image for this item.
 | 
			
		||||
     */
 | 
			
		||||
    public function cover(): BelongsTo;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the type of the image model that is used when storing a cover image.
 | 
			
		||||
     */
 | 
			
		||||
    public function coverImageTypeKey(): string;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,204 @@
 | 
			
		|||
<?php namespace BookStack\Entities\Managers;
 | 
			
		||||
 | 
			
		||||
use BookStack\Entities\Book;
 | 
			
		||||
use BookStack\Entities\BookChild;
 | 
			
		||||
use BookStack\Entities\Chapter;
 | 
			
		||||
use BookStack\Entities\Entity;
 | 
			
		||||
use BookStack\Entities\Page;
 | 
			
		||||
use BookStack\Exceptions\SortOperationException;
 | 
			
		||||
use Illuminate\Support\Collection;
 | 
			
		||||
 | 
			
		||||
class BookContents
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Book
 | 
			
		||||
     */
 | 
			
		||||
    protected $book;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * BookContents constructor.
 | 
			
		||||
     * @param $book
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(Book $book)
 | 
			
		||||
    {
 | 
			
		||||
        $this->book = $book;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the current priority of the last item
 | 
			
		||||
     * at the top-level of the book.
 | 
			
		||||
     */
 | 
			
		||||
    public function getLastPriority(): int
 | 
			
		||||
    {
 | 
			
		||||
        $maxPage = Page::visible()->where('book_id', '=', $this->book->id)
 | 
			
		||||
            ->where('draft', '=', false)
 | 
			
		||||
            ->where('chapter_id', '=', 0)->max('priority');
 | 
			
		||||
        $maxChapter = Chapter::visible()->where('book_id', '=', $this->book->id)
 | 
			
		||||
            ->max('priority');
 | 
			
		||||
        return max($maxChapter, $maxPage, 1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the contents as a sorted collection tree.
 | 
			
		||||
     * TODO - Support $renderPages option
 | 
			
		||||
     */
 | 
			
		||||
    public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection
 | 
			
		||||
    {
 | 
			
		||||
        $pages = $this->getPages($showDrafts);
 | 
			
		||||
        $chapters = Chapter::visible()->where('book_id', '=', $this->book->id)->get();
 | 
			
		||||
        $all = collect()->concat($pages)->concat($chapters);
 | 
			
		||||
        $chapterMap = $chapters->keyBy('id');
 | 
			
		||||
        $lonePages = collect();
 | 
			
		||||
 | 
			
		||||
        $pages->groupBy('chapter_id')->each(function ($pages, $chapter_id) use ($chapterMap, &$lonePages) {
 | 
			
		||||
            $chapter = $chapterMap->get($chapter_id);
 | 
			
		||||
            if ($chapter) {
 | 
			
		||||
                $chapter->setAttribute('pages', collect($pages)->sortBy($this->bookChildSortFunc()));
 | 
			
		||||
            } else {
 | 
			
		||||
                $lonePages = $lonePages->concat($pages);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        $all->each(function (Entity $entity) {
 | 
			
		||||
            $entity->setRelation('book', $this->book);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return collect($chapters)->concat($lonePages)->sortBy($this->bookChildSortFunc());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Function for providing a sorting score for an entity in relation to the
 | 
			
		||||
     * other items within the book.
 | 
			
		||||
     */
 | 
			
		||||
    protected function bookChildSortFunc(): callable
 | 
			
		||||
    {
 | 
			
		||||
        return function (Entity $entity) {
 | 
			
		||||
            if (isset($entity['draft']) && $entity['draft']) {
 | 
			
		||||
                return -100;
 | 
			
		||||
            }
 | 
			
		||||
            return $entity['priority'] ?? 0;
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the visible pages within this book.
 | 
			
		||||
     */
 | 
			
		||||
    protected function getPages(bool $showDrafts = false): Collection
 | 
			
		||||
    {
 | 
			
		||||
        $query = Page::visible()->where('book_id', '=', $this->book->id);
 | 
			
		||||
 | 
			
		||||
        if (!$showDrafts) {
 | 
			
		||||
            $query->where('draft', '=', false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $query->get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sort the books content using the given map.
 | 
			
		||||
     * The map is a single-dimension collection of objects in the following format:
 | 
			
		||||
     *   {
 | 
			
		||||
     *     +"id": "294" (ID of item)
 | 
			
		||||
     *     +"sort": 1 (Sort order index)
 | 
			
		||||
     *     +"parentChapter": false (ID of parent chapter, as string, or false)
 | 
			
		||||
     *     +"type": "page" (Entity type of item)
 | 
			
		||||
     *     +"book": "1" (Id of book to place item in)
 | 
			
		||||
     *   }
 | 
			
		||||
     *
 | 
			
		||||
     * Returns a list of books that were involved in the operation.
 | 
			
		||||
     * @throws SortOperationException
 | 
			
		||||
     */
 | 
			
		||||
    public function sortUsingMap(Collection $sortMap): Collection
 | 
			
		||||
    {
 | 
			
		||||
        // Load models into map
 | 
			
		||||
        $this->loadModelsIntoSortMap($sortMap);
 | 
			
		||||
        $booksInvolved = $this->getBooksInvolvedInSort($sortMap);
 | 
			
		||||
 | 
			
		||||
        // Perform the sort
 | 
			
		||||
        $sortMap->each(function ($mapItem) {
 | 
			
		||||
            $this->applySortUpdates($mapItem);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Update permissions and activity.
 | 
			
		||||
        $booksInvolved->each(function (Book $book) {
 | 
			
		||||
            $book->rebuildPermissions();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return $booksInvolved;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Using the given sort map item, detect changes for the related model
 | 
			
		||||
     * and update it if required.
 | 
			
		||||
     */
 | 
			
		||||
    protected function applySortUpdates(\stdClass $sortMapItem)
 | 
			
		||||
    {
 | 
			
		||||
        /** @var BookChild $model */
 | 
			
		||||
        $model = $sortMapItem->model;
 | 
			
		||||
 | 
			
		||||
        $priorityChanged = intval($model->priority) !== intval($sortMapItem->sort);
 | 
			
		||||
        $bookChanged = intval($model->book_id) !== intval($sortMapItem->book);
 | 
			
		||||
        $chapterChanged = ($sortMapItem->type === 'page') && intval($model->chapter_id) !== $sortMapItem->parentChapter;
 | 
			
		||||
 | 
			
		||||
        if ($bookChanged) {
 | 
			
		||||
            $model->changeBook($sortMapItem->book);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($chapterChanged) {
 | 
			
		||||
            $model->chapter_id = intval($sortMapItem->parentChapter);
 | 
			
		||||
            $model->save();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($priorityChanged) {
 | 
			
		||||
            $model->priority = intval($sortMapItem->sort);
 | 
			
		||||
            $model->save();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Load models from the database into the given sort map.
 | 
			
		||||
     */
 | 
			
		||||
    protected function loadModelsIntoSortMap(Collection $sortMap): void
 | 
			
		||||
    {
 | 
			
		||||
        $keyMap = $sortMap->keyBy(function (\stdClass $sortMapItem) {
 | 
			
		||||
            return  $sortMapItem->type . ':' . $sortMapItem->id;
 | 
			
		||||
        });
 | 
			
		||||
        $pageIds = $sortMap->where('type', '=', 'page')->pluck('id');
 | 
			
		||||
        $chapterIds = $sortMap->where('type', '=', 'chapter')->pluck('id');
 | 
			
		||||
 | 
			
		||||
        $pages = Page::visible()->whereIn('id', $pageIds)->get();
 | 
			
		||||
        $chapters = Chapter::visible()->whereIn('id', $chapterIds)->get();
 | 
			
		||||
 | 
			
		||||
        foreach ($pages as $page) {
 | 
			
		||||
            $sortItem = $keyMap->get('page:' . $page->id);
 | 
			
		||||
            $sortItem->model = $page;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach ($chapters as $chapter) {
 | 
			
		||||
            $sortItem = $keyMap->get('chapter:' . $chapter->id);
 | 
			
		||||
            $sortItem->model = $chapter;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the books involved in a sort.
 | 
			
		||||
     * The given sort map should have its models loaded first.
 | 
			
		||||
     * @throws SortOperationException
 | 
			
		||||
     */
 | 
			
		||||
    protected function getBooksInvolvedInSort(Collection $sortMap): Collection
 | 
			
		||||
    {
 | 
			
		||||
        $bookIdsInvolved = collect([$this->book->id]);
 | 
			
		||||
        $bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('book'));
 | 
			
		||||
        $bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('model.book_id'));
 | 
			
		||||
        $bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
 | 
			
		||||
 | 
			
		||||
        $books = Book::hasPermission('update')->whereIn('id', $bookIdsInvolved)->get();
 | 
			
		||||
 | 
			
		||||
        if (count($books) !== count($bookIdsInvolved)) {
 | 
			
		||||
            throw new SortOperationException("Could not find all books requested in sort operation");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $books;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,44 +1,38 @@
 | 
			
		|||
<?php namespace BookStack\Entities;
 | 
			
		||||
<?php namespace BookStack\Entities\Managers;
 | 
			
		||||
 | 
			
		||||
use BookStack\Entities\Repos\EntityRepo;
 | 
			
		||||
use BookStack\Entities\Book;
 | 
			
		||||
use BookStack\Entities\Bookshelf;
 | 
			
		||||
use Illuminate\Session\Store;
 | 
			
		||||
 | 
			
		||||
class EntityContextManager
 | 
			
		||||
class EntityContext
 | 
			
		||||
{
 | 
			
		||||
    protected $session;
 | 
			
		||||
    protected $entityRepo;
 | 
			
		||||
 | 
			
		||||
    protected $KEY_SHELF_CONTEXT_ID = 'context_bookshelf_id';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * EntityContextManager constructor.
 | 
			
		||||
     * @param Store $session
 | 
			
		||||
     * @param EntityRepo $entityRepo
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(Store $session, EntityRepo $entityRepo)
 | 
			
		||||
    public function __construct(Store $session)
 | 
			
		||||
    {
 | 
			
		||||
        $this->session = $session;
 | 
			
		||||
        $this->entityRepo = $entityRepo;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the current bookshelf context for the given book.
 | 
			
		||||
     * @param Book $book
 | 
			
		||||
     * @return Bookshelf|null
 | 
			
		||||
     */
 | 
			
		||||
    public function getContextualShelfForBook(Book $book)
 | 
			
		||||
    public function getContextualShelfForBook(Book $book): ?Bookshelf
 | 
			
		||||
    {
 | 
			
		||||
        $contextBookshelfId = $this->session->get($this->KEY_SHELF_CONTEXT_ID, null);
 | 
			
		||||
        if (is_int($contextBookshelfId)) {
 | 
			
		||||
 | 
			
		||||
            /** @var Bookshelf $shelf */
 | 
			
		||||
            $shelf = $this->entityRepo->getById('bookshelf', $contextBookshelfId);
 | 
			
		||||
 | 
			
		||||
            if ($shelf && $shelf->contains($book)) {
 | 
			
		||||
                return $shelf;
 | 
			
		||||
            }
 | 
			
		||||
        if (!is_int($contextBookshelfId)) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        return null;
 | 
			
		||||
 | 
			
		||||
        $shelf = Bookshelf::visible()->find($contextBookshelfId);
 | 
			
		||||
        $shelfContainsBook = $shelf && $shelf->contains($book);
 | 
			
		||||
 | 
			
		||||
        return $shelfContainsBook ? $shelf : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,304 @@
 | 
			
		|||
<?php namespace BookStack\Entities\Managers;
 | 
			
		||||
 | 
			
		||||
use BookStack\Entities\Page;
 | 
			
		||||
use DOMDocument;
 | 
			
		||||
use DOMElement;
 | 
			
		||||
use DOMNodeList;
 | 
			
		||||
use DOMXPath;
 | 
			
		||||
 | 
			
		||||
class PageContent
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    protected $page;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * PageContent constructor.
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(Page $page)
 | 
			
		||||
    {
 | 
			
		||||
        $this->page = $page;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the content of the page with new provided HTML.
 | 
			
		||||
     */
 | 
			
		||||
    public function setNewHTML(string $html)
 | 
			
		||||
    {
 | 
			
		||||
        $this->page->html = $this->formatHtml($html);
 | 
			
		||||
        $this->page->text = $this->toPlainText();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Formats a page's html to be tagged correctly within the system.
 | 
			
		||||
     */
 | 
			
		||||
    protected function formatHtml(string $htmlText): string
 | 
			
		||||
    {
 | 
			
		||||
        if ($htmlText == '') {
 | 
			
		||||
            return $htmlText;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        libxml_use_internal_errors(true);
 | 
			
		||||
        $doc = new DOMDocument();
 | 
			
		||||
        $doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
 | 
			
		||||
 | 
			
		||||
        $container = $doc->documentElement;
 | 
			
		||||
        $body = $container->childNodes->item(0);
 | 
			
		||||
        $childNodes = $body->childNodes;
 | 
			
		||||
 | 
			
		||||
        // Set ids on top-level nodes
 | 
			
		||||
        $idMap = [];
 | 
			
		||||
        foreach ($childNodes as $index => $childNode) {
 | 
			
		||||
            $this->setUniqueId($childNode, $idMap);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Ensure no duplicate ids within child items
 | 
			
		||||
        $xPath = new DOMXPath($doc);
 | 
			
		||||
        $idElems = $xPath->query('//body//*//*[@id]');
 | 
			
		||||
        foreach ($idElems as $domElem) {
 | 
			
		||||
            $this->setUniqueId($domElem, $idMap);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Generate inner html as a string
 | 
			
		||||
        $html = '';
 | 
			
		||||
        foreach ($childNodes as $childNode) {
 | 
			
		||||
            $html .= $doc->saveHTML($childNode);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $html;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set a unique id on the given DOMElement.
 | 
			
		||||
     * A map for existing ID's should be passed in to check for current existence.
 | 
			
		||||
     * @param DOMElement $element
 | 
			
		||||
     * @param array $idMap
 | 
			
		||||
     */
 | 
			
		||||
    protected function setUniqueId($element, array &$idMap)
 | 
			
		||||
    {
 | 
			
		||||
        if (get_class($element) !== 'DOMElement') {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Overwrite id if not a BookStack custom id
 | 
			
		||||
        $existingId = $element->getAttribute('id');
 | 
			
		||||
        if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
 | 
			
		||||
            $idMap[$existingId] = true;
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Create an unique id for the element
 | 
			
		||||
        // Uses the content as a basis to ensure output is the same every time
 | 
			
		||||
        // the same content is passed through.
 | 
			
		||||
        $contentId = 'bkmrk-' . mb_substr(strtolower(preg_replace('/\s+/', '-', trim($element->nodeValue))), 0, 20);
 | 
			
		||||
        $newId = urlencode($contentId);
 | 
			
		||||
        $loopIndex = 0;
 | 
			
		||||
 | 
			
		||||
        while (isset($idMap[$newId])) {
 | 
			
		||||
            $newId = urlencode($contentId . '-' . $loopIndex);
 | 
			
		||||
            $loopIndex++;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $element->setAttribute('id', $newId);
 | 
			
		||||
        $idMap[$newId] = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a plain-text visualisation of this page.
 | 
			
		||||
     */
 | 
			
		||||
    protected function toPlainText(): string
 | 
			
		||||
    {
 | 
			
		||||
        $html = $this->render(true);
 | 
			
		||||
        return strip_tags($html);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Render the page for viewing
 | 
			
		||||
     */
 | 
			
		||||
    public function render(bool $blankIncludes = false) : string
 | 
			
		||||
    {
 | 
			
		||||
        $content = $this->page->html;
 | 
			
		||||
 | 
			
		||||
        if (!config('app.allow_content_scripts')) {
 | 
			
		||||
            $content = $this->escapeScripts($content);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($blankIncludes) {
 | 
			
		||||
            $content = $this->blankPageIncludes($content);
 | 
			
		||||
        } else {
 | 
			
		||||
            $content = $this->parsePageIncludes($content);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $content;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parse the headers on the page to get a navigation menu
 | 
			
		||||
     */
 | 
			
		||||
    public function getNavigation(string $htmlContent): array
 | 
			
		||||
    {
 | 
			
		||||
        if (empty($htmlContent)) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        libxml_use_internal_errors(true);
 | 
			
		||||
        $doc = new DOMDocument();
 | 
			
		||||
        $doc->loadHTML(mb_convert_encoding($htmlContent, 'HTML-ENTITIES', 'UTF-8'));
 | 
			
		||||
        $xPath = new DOMXPath($doc);
 | 
			
		||||
        $headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
 | 
			
		||||
 | 
			
		||||
        return $headers ? $this->headerNodesToLevelList($headers) : [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convert a DOMNodeList into an array of readable header attributes
 | 
			
		||||
     * with levels normalised to the lower header level.
 | 
			
		||||
     */
 | 
			
		||||
    protected function headerNodesToLevelList(DOMNodeList $nodeList): array
 | 
			
		||||
    {
 | 
			
		||||
        $tree = collect($nodeList)->map(function ($header) {
 | 
			
		||||
            $text = trim(str_replace("\xc2\xa0", '', $header->nodeValue));
 | 
			
		||||
            $text = mb_substr($text, 0, 100);
 | 
			
		||||
 | 
			
		||||
            return [
 | 
			
		||||
                'nodeName' => strtolower($header->nodeName),
 | 
			
		||||
                'level' => intval(str_replace('h', '', $header->nodeName)),
 | 
			
		||||
                'link' => '#' . $header->getAttribute('id'),
 | 
			
		||||
                'text' => $text,
 | 
			
		||||
            ];
 | 
			
		||||
        })->filter(function ($header) {
 | 
			
		||||
            return mb_strlen($header['text']) > 0;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Shift headers if only smaller headers have been used
 | 
			
		||||
        $levelChange = ($tree->pluck('level')->min() - 1);
 | 
			
		||||
        $tree = $tree->map(function ($header) use ($levelChange) {
 | 
			
		||||
            $header['level'] -= ($levelChange);
 | 
			
		||||
            return $header;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return $tree->toArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove any page include tags within the given HTML.
 | 
			
		||||
     */
 | 
			
		||||
    protected function blankPageIncludes(string $html) : string
 | 
			
		||||
    {
 | 
			
		||||
        return preg_replace("/{{@\s?([0-9].*?)}}/", '', $html);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parse any include tags "{{@<page_id>#section}}" to be part of the page.
 | 
			
		||||
     */
 | 
			
		||||
    protected function parsePageIncludes(string $html) : string
 | 
			
		||||
    {
 | 
			
		||||
        $matches = [];
 | 
			
		||||
        preg_match_all("/{{@\s?([0-9].*?)}}/", $html, $matches);
 | 
			
		||||
 | 
			
		||||
        foreach ($matches[1] as $index => $includeId) {
 | 
			
		||||
            $fullMatch = $matches[0][$index];
 | 
			
		||||
            $splitInclude = explode('#', $includeId, 2);
 | 
			
		||||
 | 
			
		||||
            // Get page id from reference
 | 
			
		||||
            $pageId = intval($splitInclude[0]);
 | 
			
		||||
            if (is_nan($pageId)) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Find page and skip this if page not found
 | 
			
		||||
            $matchedPage = Page::visible()->find($pageId);
 | 
			
		||||
            if ($matchedPage === null) {
 | 
			
		||||
                $html = str_replace($fullMatch, '', $html);
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // If we only have page id, just insert all page html and continue.
 | 
			
		||||
            if (count($splitInclude) === 1) {
 | 
			
		||||
                $html = str_replace($fullMatch, $matchedPage->html, $html);
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Create and load HTML into a document
 | 
			
		||||
            $innerContent = $this->fetchSectionOfPage($matchedPage, $splitInclude[1]);
 | 
			
		||||
            $html = str_replace($fullMatch, trim($innerContent), $html);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $html;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch the content from a specific section of the given page.
 | 
			
		||||
     */
 | 
			
		||||
    protected function fetchSectionOfPage(Page $page, string $sectionId): string
 | 
			
		||||
    {
 | 
			
		||||
        $topLevelTags = ['table', 'ul', 'ol'];
 | 
			
		||||
        $doc = new DOMDocument();
 | 
			
		||||
        libxml_use_internal_errors(true);
 | 
			
		||||
        $doc->loadHTML(mb_convert_encoding('<body>'.$page->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
 | 
			
		||||
 | 
			
		||||
        // Search included content for the id given and blank out if not exists.
 | 
			
		||||
        $matchingElem = $doc->getElementById($sectionId);
 | 
			
		||||
        if ($matchingElem === null) {
 | 
			
		||||
            return '';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Otherwise replace the content with the found content
 | 
			
		||||
        // Checks if the top-level wrapper should be included by matching on tag types
 | 
			
		||||
        $innerContent = '';
 | 
			
		||||
        $isTopLevel = in_array(strtolower($matchingElem->nodeName), $topLevelTags);
 | 
			
		||||
        if ($isTopLevel) {
 | 
			
		||||
            $innerContent .= $doc->saveHTML($matchingElem);
 | 
			
		||||
        } else {
 | 
			
		||||
            foreach ($matchingElem->childNodes as $childNode) {
 | 
			
		||||
                $innerContent .= $doc->saveHTML($childNode);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        libxml_clear_errors();
 | 
			
		||||
 | 
			
		||||
        return $innerContent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Escape script tags within HTML content.
 | 
			
		||||
     */
 | 
			
		||||
    protected function escapeScripts(string $html) : string
 | 
			
		||||
    {
 | 
			
		||||
        if (empty($html)) {
 | 
			
		||||
            return $html;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        libxml_use_internal_errors(true);
 | 
			
		||||
        $doc = new DOMDocument();
 | 
			
		||||
        $doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
 | 
			
		||||
        $xPath = new DOMXPath($doc);
 | 
			
		||||
 | 
			
		||||
        // Remove standard script tags
 | 
			
		||||
        $scriptElems = $xPath->query('//script');
 | 
			
		||||
        foreach ($scriptElems as $scriptElem) {
 | 
			
		||||
            $scriptElem->parentNode->removeChild($scriptElem);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Remove data or JavaScript iFrames
 | 
			
		||||
        $badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]');
 | 
			
		||||
        foreach ($badIframes as $badIframe) {
 | 
			
		||||
            $badIframe->parentNode->removeChild($badIframe);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Remove 'on*' attributes
 | 
			
		||||
        $onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
 | 
			
		||||
        foreach ($onAttributes as $attr) {
 | 
			
		||||
            /** @var \DOMAttr $attr*/
 | 
			
		||||
            $attrName = $attr->nodeName;
 | 
			
		||||
            $attr->parentNode->removeAttribute($attrName);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $html = '';
 | 
			
		||||
        $topElems = $doc->documentElement->childNodes->item(0)->childNodes;
 | 
			
		||||
        foreach ($topElems as $child) {
 | 
			
		||||
            $html .= $doc->saveHTML($child);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $html;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,74 @@
 | 
			
		|||
<?php namespace BookStack\Entities\Managers;
 | 
			
		||||
 | 
			
		||||
use BookStack\Entities\Page;
 | 
			
		||||
use BookStack\Entities\PageRevision;
 | 
			
		||||
use Carbon\Carbon;
 | 
			
		||||
use Illuminate\Database\Eloquent\Builder;
 | 
			
		||||
 | 
			
		||||
class PageEditActivity
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    protected $page;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * PageEditActivity constructor.
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(Page $page)
 | 
			
		||||
    {
 | 
			
		||||
        $this->page = $page;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if there's active editing being performed on this page.
 | 
			
		||||
     * @return bool
 | 
			
		||||
     */
 | 
			
		||||
    public function hasActiveEditing(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->activePageEditingQuery(60)->count() > 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a notification message concerning the editing activity on the page.
 | 
			
		||||
     */
 | 
			
		||||
    public function activeEditingMessage(): string
 | 
			
		||||
    {
 | 
			
		||||
        $pageDraftEdits = $this->activePageEditingQuery(60)->get();
 | 
			
		||||
        $count = $pageDraftEdits->count();
 | 
			
		||||
 | 
			
		||||
        $userMessage = $count > 1 ? trans('entities.pages_draft_edit_active.start_a', ['count' => $count]): trans('entities.pages_draft_edit_active.start_b', ['userName' => $pageDraftEdits->first()->createdBy->name]);
 | 
			
		||||
        $timeMessage = trans('entities.pages_draft_edit_active.time_b', ['minCount'=> 60]);
 | 
			
		||||
        return trans('entities.pages_draft_edit_active.message', ['start' => $userMessage, 'time' => $timeMessage]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the message to show when the user will be editing one of their drafts.
 | 
			
		||||
     * @param PageRevision $draft
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    public function getEditingActiveDraftMessage(PageRevision $draft): string
 | 
			
		||||
    {
 | 
			
		||||
        $message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $draft->updated_at->diffForHumans()]);
 | 
			
		||||
        if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) {
 | 
			
		||||
            return $message;
 | 
			
		||||
        }
 | 
			
		||||
        return $message . "\n" . trans('entities.pages_draft_edited_notification');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A query to check for active update drafts on a particular page
 | 
			
		||||
     * within the last given many minutes.
 | 
			
		||||
     */
 | 
			
		||||
    protected function activePageEditingQuery(int $withinMinutes): Builder
 | 
			
		||||
    {
 | 
			
		||||
        $checkTime = Carbon::now()->subMinutes($withinMinutes);
 | 
			
		||||
        $query = PageRevision::query()
 | 
			
		||||
            ->where('type', '=', 'update_draft')
 | 
			
		||||
            ->where('page_id', '=', $this->page->id)
 | 
			
		||||
            ->where('updated_at', '>', $this->page->updated_at)
 | 
			
		||||
            ->where('created_by', '!=', user()->id)
 | 
			
		||||
            ->where('updated_at', '>=', $checkTime)
 | 
			
		||||
            ->with('createdBy');
 | 
			
		||||
 | 
			
		||||
        return $query;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,109 @@
 | 
			
		|||
<?php namespace BookStack\Entities\Managers;
 | 
			
		||||
 | 
			
		||||
use BookStack\Entities\Book;
 | 
			
		||||
use BookStack\Entities\Bookshelf;
 | 
			
		||||
use BookStack\Entities\Chapter;
 | 
			
		||||
use BookStack\Entities\Entity;
 | 
			
		||||
use BookStack\Entities\HasCoverImage;
 | 
			
		||||
use BookStack\Entities\Page;
 | 
			
		||||
use BookStack\Exceptions\NotifyException;
 | 
			
		||||
use BookStack\Facades\Activity;
 | 
			
		||||
use BookStack\Uploads\AttachmentService;
 | 
			
		||||
use BookStack\Uploads\ImageService;
 | 
			
		||||
use Exception;
 | 
			
		||||
use Illuminate\Contracts\Container\BindingResolutionException;
 | 
			
		||||
 | 
			
		||||
class TrashCan
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove a bookshelf from the system.
 | 
			
		||||
     * @throws Exception
 | 
			
		||||
     */
 | 
			
		||||
    public function destroyShelf(Bookshelf $shelf)
 | 
			
		||||
    {
 | 
			
		||||
        $this->destroyCommonRelations($shelf);
 | 
			
		||||
        $shelf->delete();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove a book from the system.
 | 
			
		||||
     * @throws NotifyException
 | 
			
		||||
     * @throws BindingResolutionException
 | 
			
		||||
     */
 | 
			
		||||
    public function destroyBook(Book $book)
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($book->pages as $page) {
 | 
			
		||||
            $this->destroyPage($page);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach ($book->chapters as $chapter) {
 | 
			
		||||
            $this->destroyChapter($chapter);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->destroyCommonRelations($book);
 | 
			
		||||
        $book->delete();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove a page from the system.
 | 
			
		||||
     * @throws NotifyException
 | 
			
		||||
     */
 | 
			
		||||
    public function destroyPage(Page $page)
 | 
			
		||||
    {
 | 
			
		||||
        // Check if set as custom homepage & remove setting if not used or throw error if active
 | 
			
		||||
        $customHome = setting('app-homepage', '0:');
 | 
			
		||||
        if (intval($page->id) === intval(explode(':', $customHome)[0])) {
 | 
			
		||||
            if (setting('app-homepage-type') === 'page') {
 | 
			
		||||
                throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
 | 
			
		||||
            }
 | 
			
		||||
            setting()->remove('app-homepage');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->destroyCommonRelations($page);
 | 
			
		||||
 | 
			
		||||
        // Delete Attached Files
 | 
			
		||||
        $attachmentService = app(AttachmentService::class);
 | 
			
		||||
        foreach ($page->attachments as $attachment) {
 | 
			
		||||
            $attachmentService->deleteFile($attachment);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $page->delete();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove a chapter from the system.
 | 
			
		||||
     * @throws Exception
 | 
			
		||||
     */
 | 
			
		||||
    public function destroyChapter(Chapter $chapter)
 | 
			
		||||
    {
 | 
			
		||||
        if (count($chapter->pages) > 0) {
 | 
			
		||||
            foreach ($chapter->pages as $page) {
 | 
			
		||||
                $page->chapter_id = 0;
 | 
			
		||||
                $page->save();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->destroyCommonRelations($chapter);
 | 
			
		||||
        $chapter->delete();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update entity relations to remove or update outstanding connections.
 | 
			
		||||
     */
 | 
			
		||||
    protected function destroyCommonRelations(Entity $entity)
 | 
			
		||||
    {
 | 
			
		||||
        Activity::removeEntity($entity);
 | 
			
		||||
        $entity->views()->delete();
 | 
			
		||||
        $entity->permissions()->delete();
 | 
			
		||||
        $entity->tags()->delete();
 | 
			
		||||
        $entity->comments()->delete();
 | 
			
		||||
        $entity->jointPermissions()->delete();
 | 
			
		||||
        $entity->searchTerms()->delete();
 | 
			
		||||
 | 
			
		||||
        if ($entity instanceof HasCoverImage && $entity->cover) {
 | 
			
		||||
            $imageService = app()->make(ImageService::class);
 | 
			
		||||
            $imageService->destroy($entity->cover);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +1,24 @@
 | 
			
		|||
<?php namespace BookStack\Entities;
 | 
			
		||||
 | 
			
		||||
use BookStack\Uploads\Attachment;
 | 
			
		||||
use Illuminate\Database\Eloquent\Builder;
 | 
			
		||||
use Illuminate\Database\Eloquent\Collection;
 | 
			
		||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 | 
			
		||||
use Illuminate\Database\Eloquent\Relations\HasMany;
 | 
			
		||||
use Permissions;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class Page
 | 
			
		||||
 * @property int $chapter_id
 | 
			
		||||
 * @property string $html
 | 
			
		||||
 * @property string $markdown
 | 
			
		||||
 * @property string $text
 | 
			
		||||
 * @property bool $template
 | 
			
		||||
 * @property bool $draft
 | 
			
		||||
 * @property int $revision_count
 | 
			
		||||
 * @property Chapter $chapter
 | 
			
		||||
 * @property Collection $attachments
 | 
			
		||||
 */
 | 
			
		||||
class Page extends BookChild
 | 
			
		||||
{
 | 
			
		||||
    protected $fillable = ['name', 'html', 'priority', 'markdown'];
 | 
			
		||||
| 
						 | 
				
			
			@ -11,12 +28,12 @@ class Page extends BookChild
 | 
			
		|||
    public $textField = 'text';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the morph class for this model.
 | 
			
		||||
     * @return string
 | 
			
		||||
     * Get the entities that are visible to the current user.
 | 
			
		||||
     */
 | 
			
		||||
    public function getMorphClass()
 | 
			
		||||
    public function scopeVisible(Builder $query)
 | 
			
		||||
    {
 | 
			
		||||
        return 'BookStack\\Page';
 | 
			
		||||
        $query = Permissions::enforceDraftVisiblityOnQuery($query);
 | 
			
		||||
        return parent::scopeVisible($query);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			@ -32,16 +49,15 @@ class Page extends BookChild
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the parent item
 | 
			
		||||
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
 | 
			
		||||
     */
 | 
			
		||||
    public function parent()
 | 
			
		||||
    public function parent(): Entity
 | 
			
		||||
    {
 | 
			
		||||
        return $this->chapter_id ? $this->chapter() : $this->book();
 | 
			
		||||
        return $this->chapter_id ? $this->chapter : $this->book;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the chapter that this page is in, If applicable.
 | 
			
		||||
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
 | 
			
		||||
     * @return BelongsTo
 | 
			
		||||
     */
 | 
			
		||||
    public function chapter()
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -63,12 +79,12 @@ class Page extends BookChild
 | 
			
		|||
     */
 | 
			
		||||
    public function revisions()
 | 
			
		||||
    {
 | 
			
		||||
        return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc');
 | 
			
		||||
        return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc')->orderBy('id', 'desc');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the attachments assigned to this page.
 | 
			
		||||
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
 | 
			
		||||
     * @return HasMany
 | 
			
		||||
     */
 | 
			
		||||
    public function attachments()
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -86,27 +102,17 @@ class Page extends BookChild
 | 
			
		|||
        $midText = $this->draft ? '/draft/' : '/page/';
 | 
			
		||||
        $idComponent = $this->draft ? $this->id : urlencode($this->slug);
 | 
			
		||||
 | 
			
		||||
        $url = '/books/' . urlencode($bookSlug) . $midText . $idComponent;
 | 
			
		||||
        if ($path !== false) {
 | 
			
		||||
            return url('/books/' . urlencode($bookSlug) . $midText . $idComponent . '/' . trim($path, '/'));
 | 
			
		||||
            $url .= '/' . trim($path, '/');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return url('/books/' . urlencode($bookSlug) . $midText . $idComponent);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return a generalised, common raw query that can be 'unioned' across entities.
 | 
			
		||||
     * @param bool $withContent
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    public function entityRawQuery($withContent = false)
 | 
			
		||||
    {
 | 
			
		||||
        $htmlQuery = $withContent ? 'html' : "'' as html";
 | 
			
		||||
        return "'BookStack\\\\Page' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at";
 | 
			
		||||
        return url($url);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the current revision for the page if existing
 | 
			
		||||
     * @return \BookStack\Entities\PageRevision|null
 | 
			
		||||
     * @return PageRevision|null
 | 
			
		||||
     */
 | 
			
		||||
    public function getCurrentRevision()
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,21 @@
 | 
			
		|||
 | 
			
		||||
use BookStack\Auth\User;
 | 
			
		||||
use BookStack\Model;
 | 
			
		||||
use Carbon\Carbon;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class PageRevision
 | 
			
		||||
 * @property int $page_id
 | 
			
		||||
 * @property string $slug
 | 
			
		||||
 * @property string $book_slug
 | 
			
		||||
 * @property int $created_by
 | 
			
		||||
 * @property Carbon $created_at
 | 
			
		||||
 * @property string $type
 | 
			
		||||
 * @property string $summary
 | 
			
		||||
 * @property string $markdown
 | 
			
		||||
 * @property string $html
 | 
			
		||||
 * @property int $revision_number
 | 
			
		||||
 */
 | 
			
		||||
class PageRevision extends Model
 | 
			
		||||
{
 | 
			
		||||
    protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
 | 
			
		||||
| 
						 | 
				
			
			@ -41,13 +55,18 @@ class PageRevision extends Model
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the previous revision for the same page if existing
 | 
			
		||||
     * @return \BookStack\PageRevision|null
 | 
			
		||||
     * @return \BookStack\Entities\PageRevision|null
 | 
			
		||||
     */
 | 
			
		||||
    public function getPrevious()
 | 
			
		||||
    {
 | 
			
		||||
        if ($id = static::where('page_id', '=', $this->page_id)->where('id', '<', $this->id)->max('id')) {
 | 
			
		||||
            return static::find($id);
 | 
			
		||||
        $id = static::newQuery()->where('page_id', '=', $this->page_id)
 | 
			
		||||
            ->where('id', '<', $this->id)
 | 
			
		||||
            ->max('id');
 | 
			
		||||
 | 
			
		||||
        if ($id) {
 | 
			
		||||
            return static::query()->find($id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,118 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace BookStack\Entities\Repos;
 | 
			
		||||
 | 
			
		||||
use BookStack\Actions\TagRepo;
 | 
			
		||||
use BookStack\Entities\Book;
 | 
			
		||||
use BookStack\Entities\Entity;
 | 
			
		||||
use BookStack\Entities\HasCoverImage;
 | 
			
		||||
use BookStack\Exceptions\ImageUploadException;
 | 
			
		||||
use BookStack\Uploads\ImageRepo;
 | 
			
		||||
use Illuminate\Http\UploadedFile;
 | 
			
		||||
use Illuminate\Support\Collection;
 | 
			
		||||
 | 
			
		||||
class BaseRepo
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    protected $tagRepo;
 | 
			
		||||
    protected $imageRepo;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * BaseRepo constructor.
 | 
			
		||||
     * @param $tagRepo
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(TagRepo $tagRepo, ImageRepo $imageRepo)
 | 
			
		||||
    {
 | 
			
		||||
        $this->tagRepo = $tagRepo;
 | 
			
		||||
        $this->imageRepo = $imageRepo;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a new entity in the system
 | 
			
		||||
     */
 | 
			
		||||
    public function create(Entity $entity, array $input)
 | 
			
		||||
    {
 | 
			
		||||
        $entity->fill($input);
 | 
			
		||||
        $entity->forceFill([
 | 
			
		||||
            'created_by' => user()->id,
 | 
			
		||||
            'updated_by' => user()->id,
 | 
			
		||||
        ]);
 | 
			
		||||
        $entity->refreshSlug();
 | 
			
		||||
        $entity->save();
 | 
			
		||||
 | 
			
		||||
        if (isset($input['tags'])) {
 | 
			
		||||
            $this->tagRepo->saveTagsToEntity($entity, $input['tags']);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $entity->rebuildPermissions();
 | 
			
		||||
        $entity->indexForSearch();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the given entity.
 | 
			
		||||
     */
 | 
			
		||||
    public function update(Entity $entity, array $input)
 | 
			
		||||
    {
 | 
			
		||||
        $entity->fill($input);
 | 
			
		||||
        $entity->updated_by = user()->id;
 | 
			
		||||
 | 
			
		||||
        if ($entity->isDirty('name')) {
 | 
			
		||||
            $entity->refreshSlug();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $entity->save();
 | 
			
		||||
 | 
			
		||||
        if (isset($input['tags'])) {
 | 
			
		||||
            $this->tagRepo->saveTagsToEntity($entity, $input['tags']);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $entity->rebuildPermissions();
 | 
			
		||||
        $entity->indexForSearch();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the given items' cover image, or clear it.
 | 
			
		||||
     * @throws ImageUploadException
 | 
			
		||||
     * @throws \Exception
 | 
			
		||||
     */
 | 
			
		||||
    public function updateCoverImage(HasCoverImage $entity, UploadedFile $coverImage = null, bool $removeImage = false)
 | 
			
		||||
    {
 | 
			
		||||
        if ($coverImage) {
 | 
			
		||||
            $this->imageRepo->destroyImage($entity->cover);
 | 
			
		||||
            $image = $this->imageRepo->saveNew($coverImage, 'cover_book', $entity->id, 512, 512, true);
 | 
			
		||||
            $entity->cover()->associate($image);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($removeImage) {
 | 
			
		||||
            $this->imageRepo->destroyImage($entity->cover);
 | 
			
		||||
            $entity->image_id = 0;
 | 
			
		||||
            $entity->save();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the permissions of an entity.
 | 
			
		||||
     */
 | 
			
		||||
    public function updatePermissions(Entity $entity, bool $restricted, Collection $permissions = null)
 | 
			
		||||
    {
 | 
			
		||||
        $entity->restricted = $restricted;
 | 
			
		||||
        $entity->permissions()->delete();
 | 
			
		||||
 | 
			
		||||
        if (!is_null($permissions)) {
 | 
			
		||||
            $entityPermissionData = $permissions->flatMap(function ($restrictions, $roleId) {
 | 
			
		||||
                return collect($restrictions)->keys()->map(function ($action) use ($roleId) {
 | 
			
		||||
                    return [
 | 
			
		||||
                        'role_id' => $roleId,
 | 
			
		||||
                        'action' => strtolower($action),
 | 
			
		||||
                    ] ;
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            $entity->permissions()->createMany($entityPermissionData);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $entity->save();
 | 
			
		||||
        $entity->rebuildPermissions();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,46 +1,134 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
namespace BookStack\Entities\Repos;
 | 
			
		||||
<?php namespace BookStack\Entities\Repos;
 | 
			
		||||
 | 
			
		||||
use BookStack\Actions\TagRepo;
 | 
			
		||||
use BookStack\Entities\Book;
 | 
			
		||||
use BookStack\Entities\Managers\TrashCan;
 | 
			
		||||
use BookStack\Exceptions\ImageUploadException;
 | 
			
		||||
use BookStack\Exceptions\NotFoundException;
 | 
			
		||||
use BookStack\Exceptions\NotifyException;
 | 
			
		||||
use BookStack\Uploads\ImageRepo;
 | 
			
		||||
use Exception;
 | 
			
		||||
use Illuminate\Contracts\Container\BindingResolutionException;
 | 
			
		||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
 | 
			
		||||
use Illuminate\Http\UploadedFile;
 | 
			
		||||
use Illuminate\Support\Collection;
 | 
			
		||||
 | 
			
		||||
class BookRepo extends EntityRepo
 | 
			
		||||
class BookRepo
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    protected $baseRepo;
 | 
			
		||||
    protected $tagRepo;
 | 
			
		||||
    protected $imageRepo;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch a book by its slug.
 | 
			
		||||
     * @param string $slug
 | 
			
		||||
     * @return Book
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     * BookRepo constructor.
 | 
			
		||||
     * @param $tagRepo
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(BaseRepo $baseRepo, TagRepo $tagRepo, ImageRepo $imageRepo)
 | 
			
		||||
    {
 | 
			
		||||
        $this->baseRepo = $baseRepo;
 | 
			
		||||
        $this->tagRepo = $tagRepo;
 | 
			
		||||
        $this->imageRepo = $imageRepo;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all books in a paginated format.
 | 
			
		||||
     */
 | 
			
		||||
    public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator
 | 
			
		||||
    {
 | 
			
		||||
        return Book::visible()->orderBy($sort, $order)->paginate($count);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the books that were most recently viewed by this user.
 | 
			
		||||
     */
 | 
			
		||||
    public function getRecentlyViewed(int $count = 20): Collection
 | 
			
		||||
    {
 | 
			
		||||
        return Book::visible()->withLastView()
 | 
			
		||||
            ->having('last_viewed_at', '>', 0)
 | 
			
		||||
            ->orderBy('last_viewed_at', 'desc')
 | 
			
		||||
            ->take($count)->get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the most popular books in the system.
 | 
			
		||||
     */
 | 
			
		||||
    public function getPopular(int $count = 20): Collection
 | 
			
		||||
    {
 | 
			
		||||
        return Book::visible()->withViewCount()
 | 
			
		||||
            ->having('view_count', '>', 0)
 | 
			
		||||
            ->orderBy('view_count', 'desc')
 | 
			
		||||
            ->take($count)->get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the most recently created books from the system.
 | 
			
		||||
     */
 | 
			
		||||
    public function getRecentlyCreated(int $count = 20): Collection
 | 
			
		||||
    {
 | 
			
		||||
        return Book::visible()->orderBy('created_at', 'desc')
 | 
			
		||||
            ->take($count)->get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a book by its slug.
 | 
			
		||||
     */
 | 
			
		||||
    public function getBySlug(string $slug): Book
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Book $book */
 | 
			
		||||
        $book = $this->getEntityBySlug('book', $slug);
 | 
			
		||||
        $book = Book::visible()->where('slug', '=', $slug)->first();
 | 
			
		||||
 | 
			
		||||
        if ($book === null) {
 | 
			
		||||
            throw new NotFoundException(trans('errors.book_not_found'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $book;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Destroy the provided book and all its child entities.
 | 
			
		||||
     * @param Book $book
 | 
			
		||||
     * @throws NotifyException
 | 
			
		||||
     * @throws \Throwable
 | 
			
		||||
     * Create a new book in the system
 | 
			
		||||
     */
 | 
			
		||||
    public function destroyBook(Book $book)
 | 
			
		||||
    public function create(array $input): Book
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($book->pages as $page) {
 | 
			
		||||
            $this->destroyPage($page);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach ($book->chapters as $chapter) {
 | 
			
		||||
            $this->destroyChapter($chapter);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->destroyEntityCommonRelations($book);
 | 
			
		||||
        $book->delete();
 | 
			
		||||
        $book = new Book();
 | 
			
		||||
        $this->baseRepo->create($book, $input);
 | 
			
		||||
        return $book;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the given book.
 | 
			
		||||
     */
 | 
			
		||||
    public function update(Book $book, array $input): Book
 | 
			
		||||
    {
 | 
			
		||||
        $this->baseRepo->update($book, $input);
 | 
			
		||||
        return $book;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the given book's cover image, or clear it.
 | 
			
		||||
     * @throws ImageUploadException
 | 
			
		||||
     * @throws Exception
 | 
			
		||||
     */
 | 
			
		||||
    public function updateCoverImage(Book $book, UploadedFile $coverImage = null, bool $removeImage = false)
 | 
			
		||||
    {
 | 
			
		||||
        $this->baseRepo->updateCoverImage($book, $coverImage, $removeImage);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the permissions of a book.
 | 
			
		||||
     */
 | 
			
		||||
    public function updatePermissions(Book $book, bool $restricted, Collection $permissions = null)
 | 
			
		||||
    {
 | 
			
		||||
        $this->baseRepo->updatePermissions($book, $restricted, $permissions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove a book from the system.
 | 
			
		||||
     * @throws NotifyException
 | 
			
		||||
     * @throws BindingResolutionException
 | 
			
		||||
     */
 | 
			
		||||
    public function destroy(Book $book)
 | 
			
		||||
    {
 | 
			
		||||
        $trashCan = new TrashCan();
 | 
			
		||||
        $trashCan->destroyBook($book);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,173 @@
 | 
			
		|||
<?php namespace BookStack\Entities\Repos;
 | 
			
		||||
 | 
			
		||||
use BookStack\Entities\Book;
 | 
			
		||||
use BookStack\Entities\Bookshelf;
 | 
			
		||||
use BookStack\Entities\Managers\TrashCan;
 | 
			
		||||
use BookStack\Exceptions\ImageUploadException;
 | 
			
		||||
use BookStack\Exceptions\NotFoundException;
 | 
			
		||||
use Exception;
 | 
			
		||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
 | 
			
		||||
use Illuminate\Http\UploadedFile;
 | 
			
		||||
use Illuminate\Support\Collection;
 | 
			
		||||
 | 
			
		||||
class BookshelfRepo
 | 
			
		||||
{
 | 
			
		||||
    protected $baseRepo;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * BookshelfRepo constructor.
 | 
			
		||||
     * @param $baseRepo
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(BaseRepo $baseRepo)
 | 
			
		||||
    {
 | 
			
		||||
        $this->baseRepo = $baseRepo;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all bookshelves in a paginated format.
 | 
			
		||||
     */
 | 
			
		||||
    public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator
 | 
			
		||||
    {
 | 
			
		||||
        return Bookshelf::visible()->with('visibleBooks')
 | 
			
		||||
            ->orderBy($sort, $order)->paginate($count);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the bookshelves that were most recently viewed by this user.
 | 
			
		||||
     */
 | 
			
		||||
    public function getRecentlyViewed(int $count = 20): Collection
 | 
			
		||||
    {
 | 
			
		||||
        return Bookshelf::visible()->withLastView()
 | 
			
		||||
            ->having('last_viewed_at', '>', 0)
 | 
			
		||||
            ->orderBy('last_viewed_at', 'desc')
 | 
			
		||||
            ->take($count)->get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the most popular bookshelves in the system.
 | 
			
		||||
     */
 | 
			
		||||
    public function getPopular(int $count = 20): Collection
 | 
			
		||||
    {
 | 
			
		||||
        return Bookshelf::visible()->withViewCount()
 | 
			
		||||
            ->having('view_count', '>', 0)
 | 
			
		||||
            ->orderBy('view_count', 'desc')
 | 
			
		||||
            ->take($count)->get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the most recently created bookshelves from the system.
 | 
			
		||||
     */
 | 
			
		||||
    public function getRecentlyCreated(int $count = 20): Collection
 | 
			
		||||
    {
 | 
			
		||||
        return Bookshelf::visible()->orderBy('created_at', 'desc')
 | 
			
		||||
            ->take($count)->get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a shelf by its slug.
 | 
			
		||||
     */
 | 
			
		||||
    public function getBySlug(string $slug): Bookshelf
 | 
			
		||||
    {
 | 
			
		||||
        $shelf = Bookshelf::visible()->where('slug', '=', $slug)->first();
 | 
			
		||||
 | 
			
		||||
        if ($shelf === null) {
 | 
			
		||||
            throw new NotFoundException(trans('errors.bookshelf_not_found'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $shelf;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a new shelf in the system.
 | 
			
		||||
     */
 | 
			
		||||
    public function create(array $input, array $bookIds): Bookshelf
 | 
			
		||||
    {
 | 
			
		||||
        $shelf = new Bookshelf();
 | 
			
		||||
        $this->baseRepo->create($shelf, $input);
 | 
			
		||||
        $this->updateBooks($shelf, $bookIds);
 | 
			
		||||
        return $shelf;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a new shelf in the system.
 | 
			
		||||
     */
 | 
			
		||||
    public function update(Bookshelf $shelf, array $input, array $bookIds): Bookshelf
 | 
			
		||||
    {
 | 
			
		||||
        $this->baseRepo->update($shelf, $input);
 | 
			
		||||
        $this->updateBooks($shelf, $bookIds);
 | 
			
		||||
        return $shelf;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update which books are assigned to this shelf by
 | 
			
		||||
     * syncing the given book ids.
 | 
			
		||||
     * Function ensures the books are visible to the current user and existing.
 | 
			
		||||
     */
 | 
			
		||||
    protected function updateBooks(Bookshelf $shelf, array $bookIds)
 | 
			
		||||
    {
 | 
			
		||||
        $numericIDs = collect($bookIds)->map(function ($id) {
 | 
			
		||||
            return intval($id);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        $syncData = Book::visible()
 | 
			
		||||
            ->whereIn('id', $bookIds)
 | 
			
		||||
            ->get(['id'])->pluck('id')->mapWithKeys(function ($bookId) use ($numericIDs) {
 | 
			
		||||
                return [$bookId => ['order' => $numericIDs->search($bookId)]];
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        $shelf->books()->sync($syncData);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the given shelf cover image, or clear it.
 | 
			
		||||
     * @throws ImageUploadException
 | 
			
		||||
     * @throws Exception
 | 
			
		||||
     */
 | 
			
		||||
    public function updateCoverImage(Bookshelf $shelf, UploadedFile $coverImage = null, bool $removeImage = false)
 | 
			
		||||
    {
 | 
			
		||||
        $this->baseRepo->updateCoverImage($shelf, $coverImage, $removeImage);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the permissions of a bookshelf.
 | 
			
		||||
     */
 | 
			
		||||
    public function updatePermissions(Bookshelf $shelf, bool $restricted, Collection $permissions = null)
 | 
			
		||||
    {
 | 
			
		||||
        $this->baseRepo->updatePermissions($shelf, $restricted, $permissions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Copy down the permissions of the given shelf to all child books.
 | 
			
		||||
     */
 | 
			
		||||
    public function copyDownPermissions(Bookshelf $shelf): int
 | 
			
		||||
    {
 | 
			
		||||
        $shelfPermissions = $shelf->permissions()->get(['role_id', 'action'])->toArray();
 | 
			
		||||
        $shelfBooks = $shelf->books()->get();
 | 
			
		||||
        $updatedBookCount = 0;
 | 
			
		||||
 | 
			
		||||
        /** @var Book $book */
 | 
			
		||||
        foreach ($shelfBooks as $book) {
 | 
			
		||||
            if (!userCan('restrictions-manage', $book)) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            $book->permissions()->delete();
 | 
			
		||||
            $book->restricted = $shelf->restricted;
 | 
			
		||||
            $book->permissions()->createMany($shelfPermissions);
 | 
			
		||||
            $book->save();
 | 
			
		||||
            $book->rebuildPermissions();
 | 
			
		||||
            $updatedBookCount++;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $updatedBookCount;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove a bookshelf from the system.
 | 
			
		||||
     * @throws Exception
 | 
			
		||||
     */
 | 
			
		||||
    public function destroy(Bookshelf $shelf)
 | 
			
		||||
    {
 | 
			
		||||
        $trashCan = new TrashCan();
 | 
			
		||||
        $trashCan->destroyShelf($shelf);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,108 @@
 | 
			
		|||
<?php namespace BookStack\Entities\Repos;
 | 
			
		||||
 | 
			
		||||
use BookStack\Entities\Book;
 | 
			
		||||
use BookStack\Entities\Chapter;
 | 
			
		||||
use BookStack\Entities\Managers\BookContents;
 | 
			
		||||
use BookStack\Entities\Managers\TrashCan;
 | 
			
		||||
use BookStack\Exceptions\MoveOperationException;
 | 
			
		||||
use BookStack\Exceptions\NotFoundException;
 | 
			
		||||
use BookStack\Exceptions\NotifyException;
 | 
			
		||||
use Exception;
 | 
			
		||||
use Illuminate\Contracts\Container\BindingResolutionException;
 | 
			
		||||
use Illuminate\Database\Eloquent\Builder;
 | 
			
		||||
use Illuminate\Support\Collection;
 | 
			
		||||
 | 
			
		||||
class ChapterRepo
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    protected $baseRepo;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * ChapterRepo constructor.
 | 
			
		||||
     * @param $baseRepo
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(BaseRepo $baseRepo)
 | 
			
		||||
    {
 | 
			
		||||
        $this->baseRepo = $baseRepo;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a chapter via the slug.
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function getBySlug(string $bookSlug, string $chapterSlug): Chapter
 | 
			
		||||
    {
 | 
			
		||||
        $chapter = Chapter::visible()->whereSlugs($bookSlug, $chapterSlug)->first();
 | 
			
		||||
 | 
			
		||||
        if ($chapter === null) {
 | 
			
		||||
            throw new NotFoundException(trans('errors.chapter_not_found'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $chapter;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a new chapter in the system.
 | 
			
		||||
     */
 | 
			
		||||
    public function create(array $input, Book $parentBook): Chapter
 | 
			
		||||
    {
 | 
			
		||||
        $chapter = new Chapter();
 | 
			
		||||
        $chapter->book_id = $parentBook->id;
 | 
			
		||||
        $chapter->priority = (new BookContents($parentBook))->getLastPriority() + 1;
 | 
			
		||||
        $this->baseRepo->create($chapter, $input);
 | 
			
		||||
        return $chapter;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the given chapter.
 | 
			
		||||
     */
 | 
			
		||||
    public function update(Chapter $chapter, array $input): Chapter
 | 
			
		||||
    {
 | 
			
		||||
        $this->baseRepo->update($chapter, $input);
 | 
			
		||||
        return $chapter;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the permissions of a chapter.
 | 
			
		||||
     */
 | 
			
		||||
    public function updatePermissions(Chapter $chapter, bool $restricted, Collection $permissions = null)
 | 
			
		||||
    {
 | 
			
		||||
        $this->baseRepo->updatePermissions($chapter, $restricted, $permissions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove a chapter from the system.
 | 
			
		||||
     * @throws Exception
 | 
			
		||||
     */
 | 
			
		||||
    public function destroy(Chapter $chapter)
 | 
			
		||||
    {
 | 
			
		||||
        $trashCan = new TrashCan();
 | 
			
		||||
        $trashCan->destroyChapter($chapter);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Move the given chapter into a new parent book.
 | 
			
		||||
     * The $parentIdentifier must be a string of the following format:
 | 
			
		||||
     * 'book:<id>' (book:5)
 | 
			
		||||
     * @throws MoveOperationException
 | 
			
		||||
     */
 | 
			
		||||
    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');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $parent = Book::visible()->where('id', '=', $entityId)->first();
 | 
			
		||||
        if ($parent === null) {
 | 
			
		||||
            throw new MoveOperationException('Book to move chapter into not found');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $chapter->changeBook($parent->id);
 | 
			
		||||
        $chapter->rebuildPermissions();
 | 
			
		||||
        return $parent;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,843 +0,0 @@
 | 
			
		|||
<?php namespace BookStack\Entities\Repos;
 | 
			
		||||
 | 
			
		||||
use Activity;
 | 
			
		||||
use BookStack\Actions\TagRepo;
 | 
			
		||||
use BookStack\Actions\ViewService;
 | 
			
		||||
use BookStack\Auth\Permissions\PermissionService;
 | 
			
		||||
use BookStack\Auth\User;
 | 
			
		||||
use BookStack\Entities\Book;
 | 
			
		||||
use BookStack\Entities\BookChild;
 | 
			
		||||
use BookStack\Entities\Bookshelf;
 | 
			
		||||
use BookStack\Entities\Chapter;
 | 
			
		||||
use BookStack\Entities\Entity;
 | 
			
		||||
use BookStack\Entities\EntityProvider;
 | 
			
		||||
use BookStack\Entities\Page;
 | 
			
		||||
use BookStack\Entities\SearchService;
 | 
			
		||||
use BookStack\Exceptions\NotFoundException;
 | 
			
		||||
use BookStack\Exceptions\NotifyException;
 | 
			
		||||
use BookStack\Uploads\AttachmentService;
 | 
			
		||||
use DOMDocument;
 | 
			
		||||
use DOMXPath;
 | 
			
		||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
 | 
			
		||||
use Illuminate\Database\Eloquent\Builder;
 | 
			
		||||
use Illuminate\Database\Query\Builder as QueryBuilder;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
use Illuminate\Support\Collection;
 | 
			
		||||
use Throwable;
 | 
			
		||||
 | 
			
		||||
class EntityRepo
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var EntityProvider
 | 
			
		||||
     */
 | 
			
		||||
    protected $entityProvider;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var PermissionService
 | 
			
		||||
     */
 | 
			
		||||
    protected $permissionService;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var ViewService
 | 
			
		||||
     */
 | 
			
		||||
    protected $viewService;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var TagRepo
 | 
			
		||||
     */
 | 
			
		||||
    protected $tagRepo;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var SearchService
 | 
			
		||||
     */
 | 
			
		||||
    protected $searchService;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * EntityRepo constructor.
 | 
			
		||||
     * @param EntityProvider $entityProvider
 | 
			
		||||
     * @param ViewService $viewService
 | 
			
		||||
     * @param PermissionService $permissionService
 | 
			
		||||
     * @param TagRepo $tagRepo
 | 
			
		||||
     * @param SearchService $searchService
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        EntityProvider $entityProvider,
 | 
			
		||||
        ViewService $viewService,
 | 
			
		||||
        PermissionService $permissionService,
 | 
			
		||||
        TagRepo $tagRepo,
 | 
			
		||||
        SearchService $searchService
 | 
			
		||||
    ) {
 | 
			
		||||
        $this->entityProvider = $entityProvider;
 | 
			
		||||
        $this->viewService = $viewService;
 | 
			
		||||
        $this->permissionService = $permissionService;
 | 
			
		||||
        $this->tagRepo = $tagRepo;
 | 
			
		||||
        $this->searchService = $searchService;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Base query for searching entities via permission system
 | 
			
		||||
     * @param string $type
 | 
			
		||||
     * @param bool $allowDrafts
 | 
			
		||||
     * @param string $permission
 | 
			
		||||
     * @return QueryBuilder
 | 
			
		||||
     */
 | 
			
		||||
    protected function entityQuery($type, $allowDrafts = false, $permission = 'view')
 | 
			
		||||
    {
 | 
			
		||||
        $q = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type), $permission);
 | 
			
		||||
        if (strtolower($type) === 'page' && !$allowDrafts) {
 | 
			
		||||
            $q = $q->where('draft', '=', false);
 | 
			
		||||
        }
 | 
			
		||||
        return $q;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if an entity with the given id exists.
 | 
			
		||||
     * @param $type
 | 
			
		||||
     * @param $id
 | 
			
		||||
     * @return bool
 | 
			
		||||
     */
 | 
			
		||||
    public function exists($type, $id)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->entityQuery($type)->where('id', '=', $id)->exists();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an entity by ID
 | 
			
		||||
     * @param string $type
 | 
			
		||||
     * @param integer $id
 | 
			
		||||
     * @param bool $allowDrafts
 | 
			
		||||
     * @param bool $ignorePermissions
 | 
			
		||||
     * @return Entity
 | 
			
		||||
     */
 | 
			
		||||
    public function getById($type, $id, $allowDrafts = false, $ignorePermissions = false)
 | 
			
		||||
    {
 | 
			
		||||
        $query = $this->entityQuery($type, $allowDrafts);
 | 
			
		||||
 | 
			
		||||
        if ($ignorePermissions) {
 | 
			
		||||
            $query = $this->entityProvider->get($type)->newQuery();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $query->find($id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param string $type
 | 
			
		||||
     * @param []int $ids
 | 
			
		||||
     * @param bool $allowDrafts
 | 
			
		||||
     * @param bool $ignorePermissions
 | 
			
		||||
     * @return Builder[]|\Illuminate\Database\Eloquent\Collection|Collection
 | 
			
		||||
     */
 | 
			
		||||
    public function getManyById($type, $ids, $allowDrafts = false, $ignorePermissions = false)
 | 
			
		||||
    {
 | 
			
		||||
        $query = $this->entityQuery($type, $allowDrafts);
 | 
			
		||||
 | 
			
		||||
        if ($ignorePermissions) {
 | 
			
		||||
            $query = $this->entityProvider->get($type)->newQuery();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $query->whereIn('id', $ids)->get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an entity by its url slug.
 | 
			
		||||
     * @param string $type
 | 
			
		||||
     * @param string $slug
 | 
			
		||||
     * @param string|null $bookSlug
 | 
			
		||||
     * @return Entity
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function getEntityBySlug(string $type, string $slug, string $bookSlug = null): Entity
 | 
			
		||||
    {
 | 
			
		||||
        $type = strtolower($type);
 | 
			
		||||
        $query = $this->entityQuery($type)->where('slug', '=', $slug);
 | 
			
		||||
 | 
			
		||||
        if ($type === 'chapter' || $type === 'page') {
 | 
			
		||||
            $query = $query->where('book_id', '=', function (QueryBuilder $query) use ($bookSlug) {
 | 
			
		||||
                $query->select('id')
 | 
			
		||||
                    ->from($this->entityProvider->book->getTable())
 | 
			
		||||
                    ->where('slug', '=', $bookSlug)->limit(1);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $entity = $query->first();
 | 
			
		||||
 | 
			
		||||
        if ($entity === null) {
 | 
			
		||||
            throw new NotFoundException(trans('errors.' . $type . '_not_found'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $entity;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all entities of a type with the given permission, limited by count unless count is false.
 | 
			
		||||
     * @param string $type
 | 
			
		||||
     * @param integer|bool $count
 | 
			
		||||
     * @param string $permission
 | 
			
		||||
     * @return Collection
 | 
			
		||||
     */
 | 
			
		||||
    public function getAll($type, $count = 20, $permission = 'view')
 | 
			
		||||
    {
 | 
			
		||||
        $q = $this->entityQuery($type, false, $permission)->orderBy('name', 'asc');
 | 
			
		||||
        if ($count !== false) {
 | 
			
		||||
            $q = $q->take($count);
 | 
			
		||||
        }
 | 
			
		||||
        return $q->get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all entities in a paginated format
 | 
			
		||||
     * @param $type
 | 
			
		||||
     * @param int $count
 | 
			
		||||
     * @param string $sort
 | 
			
		||||
     * @param string $order
 | 
			
		||||
     * @param null|callable $queryAddition
 | 
			
		||||
     * @return LengthAwarePaginator
 | 
			
		||||
     */
 | 
			
		||||
    public function getAllPaginated($type, int $count = 10, string $sort = 'name', string $order = 'asc', $queryAddition = null)
 | 
			
		||||
    {
 | 
			
		||||
        $query = $this->entityQuery($type);
 | 
			
		||||
        $query = $this->addSortToQuery($query, $sort, $order);
 | 
			
		||||
        if ($queryAddition) {
 | 
			
		||||
            $queryAddition($query);
 | 
			
		||||
        }
 | 
			
		||||
        return $query->paginate($count);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add sorting operations to an entity query.
 | 
			
		||||
     * @param Builder $query
 | 
			
		||||
     * @param string $sort
 | 
			
		||||
     * @param string $order
 | 
			
		||||
     * @return Builder
 | 
			
		||||
     */
 | 
			
		||||
    protected function addSortToQuery(Builder $query, string $sort = 'name', string $order = 'asc')
 | 
			
		||||
    {
 | 
			
		||||
        $order = ($order === 'asc') ? 'asc' : 'desc';
 | 
			
		||||
        $propertySorts = ['name', 'created_at', 'updated_at'];
 | 
			
		||||
 | 
			
		||||
        if (in_array($sort, $propertySorts)) {
 | 
			
		||||
            return $query->orderBy($sort, $order);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $query;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the most recently created entities of the given type.
 | 
			
		||||
     * @param string $type
 | 
			
		||||
     * @param int $count
 | 
			
		||||
     * @param int $page
 | 
			
		||||
     * @param bool|callable $additionalQuery
 | 
			
		||||
     * @return Collection
 | 
			
		||||
     */
 | 
			
		||||
    public function getRecentlyCreated($type, $count = 20, $page = 0, $additionalQuery = false)
 | 
			
		||||
    {
 | 
			
		||||
        $query = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type))
 | 
			
		||||
            ->orderBy('created_at', 'desc');
 | 
			
		||||
        if (strtolower($type) === 'page') {
 | 
			
		||||
            $query = $query->where('draft', '=', false);
 | 
			
		||||
        }
 | 
			
		||||
        if ($additionalQuery !== false && is_callable($additionalQuery)) {
 | 
			
		||||
            $additionalQuery($query);
 | 
			
		||||
        }
 | 
			
		||||
        return $query->skip($page * $count)->take($count)->get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the most recently updated entities of the given type.
 | 
			
		||||
     * @param string $type
 | 
			
		||||
     * @param int $count
 | 
			
		||||
     * @param int $page
 | 
			
		||||
     * @param bool|callable $additionalQuery
 | 
			
		||||
     * @return Collection
 | 
			
		||||
     */
 | 
			
		||||
    public function getRecentlyUpdated($type, $count = 20, $page = 0, $additionalQuery = false)
 | 
			
		||||
    {
 | 
			
		||||
        $query = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type))
 | 
			
		||||
            ->orderBy('updated_at', 'desc');
 | 
			
		||||
        if (strtolower($type) === 'page') {
 | 
			
		||||
            $query = $query->where('draft', '=', false);
 | 
			
		||||
        }
 | 
			
		||||
        if ($additionalQuery !== false && is_callable($additionalQuery)) {
 | 
			
		||||
            $additionalQuery($query);
 | 
			
		||||
        }
 | 
			
		||||
        return $query->skip($page * $count)->take($count)->get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the most recently viewed entities.
 | 
			
		||||
     * @param string|bool $type
 | 
			
		||||
     * @param int $count
 | 
			
		||||
     * @param int $page
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     */
 | 
			
		||||
    public function getRecentlyViewed($type, $count = 10, $page = 0)
 | 
			
		||||
    {
 | 
			
		||||
        $filter = is_bool($type) ? false : $this->entityProvider->get($type);
 | 
			
		||||
        return $this->viewService->getUserRecentlyViewed($count, $page, $filter);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the latest pages added to the system with pagination.
 | 
			
		||||
     * @param string $type
 | 
			
		||||
     * @param int $count
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     */
 | 
			
		||||
    public function getRecentlyCreatedPaginated($type, $count = 20)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->entityQuery($type)->orderBy('created_at', 'desc')->paginate($count);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the latest pages added to the system with pagination.
 | 
			
		||||
     * @param string $type
 | 
			
		||||
     * @param int $count
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     */
 | 
			
		||||
    public function getRecentlyUpdatedPaginated($type, $count = 20)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->entityQuery($type)->orderBy('updated_at', 'desc')->paginate($count);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the most popular entities base on all views.
 | 
			
		||||
     * @param string $type
 | 
			
		||||
     * @param int $count
 | 
			
		||||
     * @param int $page
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     */
 | 
			
		||||
    public function getPopular(string $type, int $count = 10, int $page = 0)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->viewService->getPopular($count, $page, $type);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get draft pages owned by the current user.
 | 
			
		||||
     * @param int $count
 | 
			
		||||
     * @param int $page
 | 
			
		||||
     * @return Collection
 | 
			
		||||
     */
 | 
			
		||||
    public function getUserDraftPages($count = 20, $page = 0)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->entityProvider->page->where('draft', '=', true)
 | 
			
		||||
            ->where('created_by', '=', user()->id)
 | 
			
		||||
            ->orderBy('updated_at', 'desc')
 | 
			
		||||
            ->skip($count * $page)->take($count)->get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the number of entities the given user has created.
 | 
			
		||||
     * @param string $type
 | 
			
		||||
     * @param User $user
 | 
			
		||||
     * @return int
 | 
			
		||||
     */
 | 
			
		||||
    public function getUserTotalCreated(string $type, User $user)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->entityProvider->get($type)
 | 
			
		||||
            ->where('created_by', '=', $user->id)->count();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the child items for a chapter sorted by priority but
 | 
			
		||||
     * with draft items floated to the top.
 | 
			
		||||
     * @param Bookshelf $bookshelf
 | 
			
		||||
     * @return \Illuminate\Database\Eloquent\Collection|static[]
 | 
			
		||||
     */
 | 
			
		||||
    public function getBookshelfChildren(Bookshelf $bookshelf)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->permissionService->enforceEntityRestrictions('book', $bookshelf->books())->get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the direct children of a book.
 | 
			
		||||
     * @param Book $book
 | 
			
		||||
     * @return \Illuminate\Database\Eloquent\Collection
 | 
			
		||||
     */
 | 
			
		||||
    public function getBookDirectChildren(Book $book)
 | 
			
		||||
    {
 | 
			
		||||
        $pages = $this->permissionService->enforceEntityRestrictions('page', $book->directPages())->get();
 | 
			
		||||
        $chapters = $this->permissionService->enforceEntityRestrictions('chapters', $book->chapters())->get();
 | 
			
		||||
        return collect()->concat($pages)->concat($chapters)->sortBy('priority')->sortByDesc('draft');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all child objects of a book.
 | 
			
		||||
     * Returns a sorted collection of Pages and Chapters.
 | 
			
		||||
     * Loads the book slug onto child elements to prevent access database access for getting the slug.
 | 
			
		||||
     * @param Book $book
 | 
			
		||||
     * @param bool $filterDrafts
 | 
			
		||||
     * @param bool $renderPages
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     */
 | 
			
		||||
    public function getBookChildren(Book $book, $filterDrafts = false, $renderPages = false)
 | 
			
		||||
    {
 | 
			
		||||
        $q = $this->permissionService->bookChildrenQuery($book->id, $filterDrafts, $renderPages)->get();
 | 
			
		||||
        $entities = [];
 | 
			
		||||
        $parents = [];
 | 
			
		||||
        $tree = [];
 | 
			
		||||
 | 
			
		||||
        foreach ($q as $index => $rawEntity) {
 | 
			
		||||
            if ($rawEntity->entity_type ===  $this->entityProvider->page->getMorphClass()) {
 | 
			
		||||
                $entities[$index] = $this->entityProvider->page->newFromBuilder($rawEntity);
 | 
			
		||||
                if ($renderPages) {
 | 
			
		||||
                    $entities[$index]->html = $rawEntity->html;
 | 
			
		||||
                    $entities[$index]->html = $this->renderPage($entities[$index]);
 | 
			
		||||
                };
 | 
			
		||||
            } else if ($rawEntity->entity_type === $this->entityProvider->chapter->getMorphClass()) {
 | 
			
		||||
                $entities[$index] = $this->entityProvider->chapter->newFromBuilder($rawEntity);
 | 
			
		||||
                $key = $entities[$index]->entity_type . ':' . $entities[$index]->id;
 | 
			
		||||
                $parents[$key] = $entities[$index];
 | 
			
		||||
                $parents[$key]->setAttribute('pages', collect());
 | 
			
		||||
            }
 | 
			
		||||
            if ($entities[$index]->chapter_id === 0 || $entities[$index]->chapter_id === '0') {
 | 
			
		||||
                $tree[] = $entities[$index];
 | 
			
		||||
            }
 | 
			
		||||
            $entities[$index]->book = $book;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach ($entities as $entity) {
 | 
			
		||||
            if ($entity->chapter_id === 0 || $entity->chapter_id === '0') {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            $parentKey = $this->entityProvider->chapter->getMorphClass() . ':' . $entity->chapter_id;
 | 
			
		||||
            if (!isset($parents[$parentKey])) {
 | 
			
		||||
                $tree[] = $entity;
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            $chapter = $parents[$parentKey];
 | 
			
		||||
            $chapter->pages->push($entity);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return collect($tree);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the child items for a chapter sorted by priority but
 | 
			
		||||
     * with draft items floated to the top.
 | 
			
		||||
     * @param Chapter $chapter
 | 
			
		||||
     * @return \Illuminate\Database\Eloquent\Collection|static[]
 | 
			
		||||
     */
 | 
			
		||||
    public function getChapterChildren(Chapter $chapter)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->permissionService->enforceEntityRestrictions('page', $chapter->pages())
 | 
			
		||||
            ->orderBy('draft', 'DESC')->orderBy('priority', 'ASC')->get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the next sequential priority for a new child element in the given book.
 | 
			
		||||
     * @param Book $book
 | 
			
		||||
     * @return int
 | 
			
		||||
     */
 | 
			
		||||
    public function getNewBookPriority(Book $book)
 | 
			
		||||
    {
 | 
			
		||||
        $lastElem = $this->getBookChildren($book)->pop();
 | 
			
		||||
        return $lastElem ? $lastElem->priority + 1 : 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a new priority for a new page to be added to the given chapter.
 | 
			
		||||
     * @param Chapter $chapter
 | 
			
		||||
     * @return int
 | 
			
		||||
     */
 | 
			
		||||
    public function getNewChapterPriority(Chapter $chapter)
 | 
			
		||||
    {
 | 
			
		||||
        $lastPage = $chapter->pages('DESC')->first();
 | 
			
		||||
        return $lastPage !== null ? $lastPage->priority + 1 : 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Find a suitable slug for an entity.
 | 
			
		||||
     * @param string $type
 | 
			
		||||
     * @param string $name
 | 
			
		||||
     * @param bool|integer $currentId
 | 
			
		||||
     * @param bool|integer $bookId Only pass if type is not a book
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    public function findSuitableSlug($type, $name, $currentId = false, $bookId = false)
 | 
			
		||||
    {
 | 
			
		||||
        $slug = $this->nameToSlug($name);
 | 
			
		||||
        while ($this->slugExists($type, $slug, $currentId, $bookId)) {
 | 
			
		||||
            $slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
 | 
			
		||||
        }
 | 
			
		||||
        return $slug;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Updates entity restrictions from a request
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @param Entity $entity
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function updateEntityPermissionsFromRequest(Request $request, Entity $entity)
 | 
			
		||||
    {
 | 
			
		||||
        $entity->restricted = $request->get('restricted', '') === 'true';
 | 
			
		||||
        $entity->permissions()->delete();
 | 
			
		||||
 | 
			
		||||
        if ($request->filled('restrictions')) {
 | 
			
		||||
            $entityPermissionData = collect($request->get('restrictions'))->flatMap(function($restrictions, $roleId) {
 | 
			
		||||
                return collect($restrictions)->keys()->map(function($action) use ($roleId) {
 | 
			
		||||
                    return [
 | 
			
		||||
                        'role_id' => $roleId,
 | 
			
		||||
                        'action' => strtolower($action),
 | 
			
		||||
                    ] ;
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            $entity->permissions()->createMany($entityPermissionData);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $entity->save();
 | 
			
		||||
        $entity->rebuildPermissions();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a new entity from request input.
 | 
			
		||||
     * Used for books and chapters.
 | 
			
		||||
     * @param string $type
 | 
			
		||||
     * @param array $input
 | 
			
		||||
     * @param Book|null $book
 | 
			
		||||
     * @return Entity
 | 
			
		||||
     */
 | 
			
		||||
    public function createFromInput(string $type, array $input = [], Book $book = null)
 | 
			
		||||
    {
 | 
			
		||||
        $entityModel = $this->entityProvider->get($type)->newInstance($input);
 | 
			
		||||
        $entityModel->created_by = user()->id;
 | 
			
		||||
        $entityModel->updated_by = user()->id;
 | 
			
		||||
 | 
			
		||||
        if ($book) {
 | 
			
		||||
            $entityModel->book_id = $book->id;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $entityModel->refreshSlug();
 | 
			
		||||
        $entityModel->save();
 | 
			
		||||
 | 
			
		||||
        if (isset($input['tags'])) {
 | 
			
		||||
            $this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $entityModel->rebuildPermissions();
 | 
			
		||||
        $this->searchService->indexEntity($entityModel);
 | 
			
		||||
        return $entityModel;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update entity details from request input.
 | 
			
		||||
     * Used for shelves, books and chapters.
 | 
			
		||||
     */
 | 
			
		||||
    public function updateFromInput(Entity $entityModel, array $input): Entity
 | 
			
		||||
    {
 | 
			
		||||
        $entityModel->fill($input);
 | 
			
		||||
        $entityModel->updated_by = user()->id;
 | 
			
		||||
 | 
			
		||||
        if ($entityModel->isDirty('name')) {
 | 
			
		||||
            $entityModel->refreshSlug();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $entityModel->save();
 | 
			
		||||
 | 
			
		||||
        if (isset($input['tags'])) {
 | 
			
		||||
            $this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $entityModel->rebuildPermissions();
 | 
			
		||||
        $this->searchService->indexEntity($entityModel);
 | 
			
		||||
        return $entityModel;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sync the books assigned to a shelf from a comma-separated list
 | 
			
		||||
     * of book IDs.
 | 
			
		||||
     * @param Bookshelf $shelf
 | 
			
		||||
     * @param string $books
 | 
			
		||||
     */
 | 
			
		||||
    public function updateShelfBooks(Bookshelf $shelf, string $books)
 | 
			
		||||
    {
 | 
			
		||||
        $ids = explode(',', $books);
 | 
			
		||||
 | 
			
		||||
        // Check books exist and match ordering
 | 
			
		||||
        $bookIds = $this->entityQuery('book')->whereIn('id', $ids)->get(['id'])->pluck('id');
 | 
			
		||||
        $syncData = [];
 | 
			
		||||
        foreach ($ids as $index => $id) {
 | 
			
		||||
            if ($bookIds->contains($id)) {
 | 
			
		||||
                $syncData[$id] = ['order' => $index];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $shelf->books()->sync($syncData);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Change the book that an entity belongs to.
 | 
			
		||||
     */
 | 
			
		||||
    public function changeBook(BookChild $bookChild, int $newBookId): Entity
 | 
			
		||||
    {
 | 
			
		||||
        $bookChild->book_id = $newBookId;
 | 
			
		||||
        $bookChild->refreshSlug();
 | 
			
		||||
        $bookChild->save();
 | 
			
		||||
 | 
			
		||||
        // Update related activity
 | 
			
		||||
        $bookChild->activity()->update(['book_id' => $newBookId]);
 | 
			
		||||
 | 
			
		||||
        // Update all child pages if a chapter
 | 
			
		||||
        if ($bookChild->isA('chapter')) {
 | 
			
		||||
            foreach ($bookChild->pages as $page) {
 | 
			
		||||
                $this->changeBook($page, $newBookId);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $bookChild;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Render the page for viewing
 | 
			
		||||
     * @param Page $page
 | 
			
		||||
     * @param bool $blankIncludes
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    public function renderPage(Page $page, bool $blankIncludes = false) : string
 | 
			
		||||
    {
 | 
			
		||||
        $content = $page->html;
 | 
			
		||||
 | 
			
		||||
        if (!config('app.allow_content_scripts')) {
 | 
			
		||||
            $content = $this->escapeScripts($content);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($blankIncludes) {
 | 
			
		||||
            $content = $this->blankPageIncludes($content);
 | 
			
		||||
        } else {
 | 
			
		||||
            $content = $this->parsePageIncludes($content);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $content;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove any page include tags within the given HTML.
 | 
			
		||||
     * @param string $html
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    protected function blankPageIncludes(string $html) : string
 | 
			
		||||
    {
 | 
			
		||||
        return preg_replace("/{{@\s?([0-9].*?)}}/", '', $html);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parse any include tags "{{@<page_id>#section}}" to be part of the page.
 | 
			
		||||
     * @param string $html
 | 
			
		||||
     * @return mixed|string
 | 
			
		||||
     */
 | 
			
		||||
    protected function parsePageIncludes(string $html) : string
 | 
			
		||||
    {
 | 
			
		||||
        $matches = [];
 | 
			
		||||
        preg_match_all("/{{@\s?([0-9].*?)}}/", $html, $matches);
 | 
			
		||||
 | 
			
		||||
        $topLevelTags = ['table', 'ul', 'ol'];
 | 
			
		||||
        foreach ($matches[1] as $index => $includeId) {
 | 
			
		||||
            $splitInclude = explode('#', $includeId, 2);
 | 
			
		||||
            $pageId = intval($splitInclude[0]);
 | 
			
		||||
            if (is_nan($pageId)) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $matchedPage = $this->getById('page', $pageId);
 | 
			
		||||
            if ($matchedPage === null) {
 | 
			
		||||
                $html = str_replace($matches[0][$index], '', $html);
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (count($splitInclude) === 1) {
 | 
			
		||||
                $html = str_replace($matches[0][$index], $matchedPage->html, $html);
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $doc = new DOMDocument();
 | 
			
		||||
            libxml_use_internal_errors(true);
 | 
			
		||||
            $doc->loadHTML(mb_convert_encoding('<body>'.$matchedPage->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
 | 
			
		||||
            $matchingElem = $doc->getElementById($splitInclude[1]);
 | 
			
		||||
            if ($matchingElem === null) {
 | 
			
		||||
                $html = str_replace($matches[0][$index], '', $html);
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            $innerContent = '';
 | 
			
		||||
            $isTopLevel = in_array(strtolower($matchingElem->nodeName), $topLevelTags);
 | 
			
		||||
            if ($isTopLevel) {
 | 
			
		||||
                $innerContent .= $doc->saveHTML($matchingElem);
 | 
			
		||||
            } else {
 | 
			
		||||
                foreach ($matchingElem->childNodes as $childNode) {
 | 
			
		||||
                    $innerContent .= $doc->saveHTML($childNode);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            libxml_clear_errors();
 | 
			
		||||
            $html = str_replace($matches[0][$index], trim($innerContent), $html);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $html;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Escape script tags within HTML content.
 | 
			
		||||
     * @param string $html
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    protected function escapeScripts(string $html) : string
 | 
			
		||||
    {
 | 
			
		||||
        if ($html == '') {
 | 
			
		||||
            return $html;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        libxml_use_internal_errors(true);
 | 
			
		||||
        $doc = new DOMDocument();
 | 
			
		||||
        $doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
 | 
			
		||||
        $xPath = new DOMXPath($doc);
 | 
			
		||||
 | 
			
		||||
        // Remove standard script tags
 | 
			
		||||
        $scriptElems = $xPath->query('//script');
 | 
			
		||||
        foreach ($scriptElems as $scriptElem) {
 | 
			
		||||
            $scriptElem->parentNode->removeChild($scriptElem);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Remove data or JavaScript iFrames
 | 
			
		||||
        $badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]');
 | 
			
		||||
        foreach ($badIframes as $badIframe) {
 | 
			
		||||
            $badIframe->parentNode->removeChild($badIframe);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Remove 'on*' attributes
 | 
			
		||||
        $onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
 | 
			
		||||
        foreach ($onAttributes as $attr) {
 | 
			
		||||
            /** @var \DOMAttr $attr*/
 | 
			
		||||
            $attrName = $attr->nodeName;
 | 
			
		||||
            $attr->parentNode->removeAttribute($attrName);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $html = '';
 | 
			
		||||
        $topElems = $doc->documentElement->childNodes->item(0)->childNodes;
 | 
			
		||||
        foreach ($topElems as $child) {
 | 
			
		||||
            $html .= $doc->saveHTML($child);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $html;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Search for image usage within page content.
 | 
			
		||||
     * @param $imageString
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     */
 | 
			
		||||
    public function searchForImage($imageString)
 | 
			
		||||
    {
 | 
			
		||||
        $pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get(['id', 'name', 'slug', 'book_id']);
 | 
			
		||||
        foreach ($pages as $page) {
 | 
			
		||||
            $page->url = $page->getUrl();
 | 
			
		||||
            $page->html = '';
 | 
			
		||||
            $page->text = '';
 | 
			
		||||
        }
 | 
			
		||||
        return count($pages) > 0 ? $pages : false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Destroy a bookshelf instance
 | 
			
		||||
     * @param Bookshelf $shelf
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function destroyBookshelf(Bookshelf $shelf)
 | 
			
		||||
    {
 | 
			
		||||
        $this->destroyEntityCommonRelations($shelf);
 | 
			
		||||
        $shelf->delete();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Destroy a chapter and its relations.
 | 
			
		||||
     * @param Chapter $chapter
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function destroyChapter(Chapter $chapter)
 | 
			
		||||
    {
 | 
			
		||||
        if (count($chapter->pages) > 0) {
 | 
			
		||||
            foreach ($chapter->pages as $page) {
 | 
			
		||||
                $page->chapter_id = 0;
 | 
			
		||||
                $page->save();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        $this->destroyEntityCommonRelations($chapter);
 | 
			
		||||
        $chapter->delete();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Destroy a given page along with its dependencies.
 | 
			
		||||
     * @param Page $page
 | 
			
		||||
     * @throws NotifyException
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function destroyPage(Page $page)
 | 
			
		||||
    {
 | 
			
		||||
        // Check if set as custom homepage & remove setting if not used or throw error if active
 | 
			
		||||
        $customHome = setting('app-homepage', '0:');
 | 
			
		||||
        if (intval($page->id) === intval(explode(':', $customHome)[0])) {
 | 
			
		||||
            if (setting('app-homepage-type') === 'page') {
 | 
			
		||||
                throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
 | 
			
		||||
            }
 | 
			
		||||
            setting()->remove('app-homepage');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->destroyEntityCommonRelations($page);
 | 
			
		||||
 | 
			
		||||
        // Delete Attached Files
 | 
			
		||||
        $attachmentService = app(AttachmentService::class);
 | 
			
		||||
        foreach ($page->attachments as $attachment) {
 | 
			
		||||
            $attachmentService->deleteFile($attachment);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $page->delete();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Destroy or handle the common relations connected to an entity.
 | 
			
		||||
     * @param Entity $entity
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    protected function destroyEntityCommonRelations(Entity $entity)
 | 
			
		||||
    {
 | 
			
		||||
        Activity::removeEntity($entity);
 | 
			
		||||
        $entity->views()->delete();
 | 
			
		||||
        $entity->permissions()->delete();
 | 
			
		||||
        $entity->tags()->delete();
 | 
			
		||||
        $entity->comments()->delete();
 | 
			
		||||
        $this->permissionService->deleteJointPermissionsForEntity($entity);
 | 
			
		||||
        $this->searchService->deleteEntityTerms($entity);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Copy the permissions of a bookshelf to all child books.
 | 
			
		||||
     * Returns the number of books that had permissions updated.
 | 
			
		||||
     * @param Bookshelf $bookshelf
 | 
			
		||||
     * @return int
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function copyBookshelfPermissions(Bookshelf $bookshelf)
 | 
			
		||||
    {
 | 
			
		||||
        $shelfPermissions = $bookshelf->permissions()->get(['role_id', 'action'])->toArray();
 | 
			
		||||
        $shelfBooks = $bookshelf->books()->get();
 | 
			
		||||
        $updatedBookCount = 0;
 | 
			
		||||
 | 
			
		||||
        /** @var Book $book */
 | 
			
		||||
        foreach ($shelfBooks as $book) {
 | 
			
		||||
            if (!userCan('restrictions-manage', $book)) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            $book->permissions()->delete();
 | 
			
		||||
            $book->restricted = $bookshelf->restricted;
 | 
			
		||||
            $book->permissions()->createMany($shelfPermissions);
 | 
			
		||||
            $book->save();
 | 
			
		||||
            $book->rebuildPermissions();
 | 
			
		||||
            $updatedBookCount++;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $updatedBookCount;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -3,91 +3,199 @@
 | 
			
		|||
use BookStack\Entities\Book;
 | 
			
		||||
use BookStack\Entities\Chapter;
 | 
			
		||||
use BookStack\Entities\Entity;
 | 
			
		||||
use BookStack\Entities\Managers\BookContents;
 | 
			
		||||
use BookStack\Entities\Managers\PageContent;
 | 
			
		||||
use BookStack\Entities\Managers\TrashCan;
 | 
			
		||||
use BookStack\Entities\Page;
 | 
			
		||||
use BookStack\Entities\PageRevision;
 | 
			
		||||
use Carbon\Carbon;
 | 
			
		||||
use DOMDocument;
 | 
			
		||||
use DOMElement;
 | 
			
		||||
use DOMXPath;
 | 
			
		||||
use BookStack\Exceptions\MoveOperationException;
 | 
			
		||||
use BookStack\Exceptions\NotFoundException;
 | 
			
		||||
use BookStack\Exceptions\NotifyException;
 | 
			
		||||
use BookStack\Exceptions\PermissionsException;
 | 
			
		||||
use Illuminate\Database\Eloquent\Builder;
 | 
			
		||||
use Illuminate\Pagination\LengthAwarePaginator;
 | 
			
		||||
use Illuminate\Support\Collection;
 | 
			
		||||
 | 
			
		||||
class PageRepo extends EntityRepo
 | 
			
		||||
class PageRepo
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    protected $baseRepo;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get page by slug.
 | 
			
		||||
     * @param string $pageSlug
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @return Page
 | 
			
		||||
     * @throws \BookStack\Exceptions\NotFoundException
 | 
			
		||||
     * PageRepo constructor.
 | 
			
		||||
     */
 | 
			
		||||
    public function getBySlug(string $pageSlug, string $bookSlug)
 | 
			
		||||
    public function __construct(BaseRepo $baseRepo)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->getEntityBySlug('page', $pageSlug, $bookSlug);
 | 
			
		||||
        $this->baseRepo = $baseRepo;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Search through page revisions and retrieve the last page in the
 | 
			
		||||
     * current book that has a slug equal to the one given.
 | 
			
		||||
     * @param string $pageSlug
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @return null|Page
 | 
			
		||||
     * Get a page by ID.
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function getPageByOldSlug(string $pageSlug, string $bookSlug)
 | 
			
		||||
    public function getById(int $id): Page
 | 
			
		||||
    {
 | 
			
		||||
        $revision = $this->entityProvider->pageRevision->where('slug', '=', $pageSlug)
 | 
			
		||||
            ->whereHas('page', function ($query) {
 | 
			
		||||
                $this->permissionService->enforceEntityRestrictions('page', $query);
 | 
			
		||||
        $page = Page::visible()->with(['book'])->find($id);
 | 
			
		||||
 | 
			
		||||
        if (!$page) {
 | 
			
		||||
            throw new NotFoundException(trans('errors.page_not_found'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $page;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a page its book and own slug.
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function getBySlug(string $bookSlug, string $pageSlug): Page
 | 
			
		||||
    {
 | 
			
		||||
        $page = Page::visible()->whereSlugs($bookSlug, $pageSlug)->first();
 | 
			
		||||
 | 
			
		||||
        if (!$page) {
 | 
			
		||||
            throw new NotFoundException(trans('errors.page_not_found'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $page;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a page by its old slug but checking the revisions table
 | 
			
		||||
     * for the last revision that matched the given page and book slug.
 | 
			
		||||
     */
 | 
			
		||||
    public function getByOldSlug(string $bookSlug, string $pageSlug): ?Page
 | 
			
		||||
    {
 | 
			
		||||
        $revision = PageRevision::query()
 | 
			
		||||
            ->whereHas('page', function (Builder $query) {
 | 
			
		||||
                $query->visible();
 | 
			
		||||
            })
 | 
			
		||||
            ->where('slug', '=', $pageSlug)
 | 
			
		||||
            ->where('type', '=', 'version')
 | 
			
		||||
            ->where('book_slug', '=', $bookSlug)
 | 
			
		||||
            ->orderBy('created_at', 'desc')
 | 
			
		||||
            ->with('page')->first();
 | 
			
		||||
        return $revision !== null ? $revision->page : null;
 | 
			
		||||
            ->with('page')
 | 
			
		||||
            ->first();
 | 
			
		||||
        return $revision ? $revision->page : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Updates a page with any fillable data and saves it into the database.
 | 
			
		||||
     * @param Page $page
 | 
			
		||||
     * @param int $book_id
 | 
			
		||||
     * @param array $input
 | 
			
		||||
     * @return Page
 | 
			
		||||
     * @throws \Exception
 | 
			
		||||
     * Get pages that have been marked as a template.
 | 
			
		||||
     */
 | 
			
		||||
    public function updatePage(Page $page, int $book_id, array $input)
 | 
			
		||||
    public function getTemplates(int $count = 10, int $page = 1, string $search = ''): LengthAwarePaginator
 | 
			
		||||
    {
 | 
			
		||||
        $query = Page::visible()
 | 
			
		||||
            ->where('template', '=', true)
 | 
			
		||||
            ->orderBy('name', 'asc')
 | 
			
		||||
            ->skip(($page - 1) * $count)
 | 
			
		||||
            ->take($count);
 | 
			
		||||
 | 
			
		||||
        if ($search) {
 | 
			
		||||
            $query->where('name', 'like', '%' . $search . '%');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $paginator = $query->paginate($count, ['*'], 'page', $page);
 | 
			
		||||
        $paginator->withPath('/templates');
 | 
			
		||||
 | 
			
		||||
        return $paginator;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a parent item via slugs.
 | 
			
		||||
     */
 | 
			
		||||
    public function getParentFromSlugs(string $bookSlug, string $chapterSlug = null): Entity
 | 
			
		||||
    {
 | 
			
		||||
        if ($chapterSlug !== null) {
 | 
			
		||||
            return $chapter = Chapter::visible()->whereSlugs($bookSlug, $chapterSlug)->firstOrFail();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the draft copy of the given page for the current user.
 | 
			
		||||
     */
 | 
			
		||||
    public function getUserDraft(Page $page): ?PageRevision
 | 
			
		||||
    {
 | 
			
		||||
        $revision = $this->getUserDraftQuery($page)->first();
 | 
			
		||||
        return $revision;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a new draft page belonging to the given parent entity.
 | 
			
		||||
     */
 | 
			
		||||
    public function getNewDraftPage(Entity $parent)
 | 
			
		||||
    {
 | 
			
		||||
        $page = (new Page())->forceFill([
 | 
			
		||||
            'name' => trans('entities.pages_initial_name'),
 | 
			
		||||
            'created_by' => user()->id,
 | 
			
		||||
            'updated_by' => user()->id,
 | 
			
		||||
            'draft' => true,
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        if ($parent instanceof Chapter) {
 | 
			
		||||
            $page->chapter_id = $parent->id;
 | 
			
		||||
            $page->book_id = $parent->book_id;
 | 
			
		||||
        } else {
 | 
			
		||||
            $page->book_id = $parent->id;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $page->save();
 | 
			
		||||
        $page->refresh()->rebuildPermissions();
 | 
			
		||||
        return $page;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Publish a draft page to make it a live, non-draft page.
 | 
			
		||||
     */
 | 
			
		||||
    public function publishDraft(Page $draft, array $input): Page
 | 
			
		||||
    {
 | 
			
		||||
        $this->baseRepo->update($draft, $input);
 | 
			
		||||
        if (isset($input['template']) && userCan('templates-manage')) {
 | 
			
		||||
            $draft->template = ($input['template'] === 'true');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $pageContent = new PageContent($draft);
 | 
			
		||||
        $pageContent->setNewHTML($input['html']);
 | 
			
		||||
        $draft->draft = false;
 | 
			
		||||
        $draft->revision_count = 1;
 | 
			
		||||
        $draft->priority = $this->getNewPriority($draft);
 | 
			
		||||
        $draft->refreshSlug();
 | 
			
		||||
        $draft->save();
 | 
			
		||||
 | 
			
		||||
        $this->savePageRevision($draft, trans('entities.pages_initial_revision'));
 | 
			
		||||
        $draft->indexForSearch();
 | 
			
		||||
        return $draft->refresh();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update a page in the system.
 | 
			
		||||
     */
 | 
			
		||||
    public function update(Page $page, array $input): Page
 | 
			
		||||
    {
 | 
			
		||||
        // Hold the old details to compare later
 | 
			
		||||
        $oldHtml = $page->html;
 | 
			
		||||
        $oldName = $page->name;
 | 
			
		||||
 | 
			
		||||
        // Save page tags if present
 | 
			
		||||
        if (isset($input['tags'])) {
 | 
			
		||||
            $this->tagRepo->saveTagsToEntity($page, $input['tags']);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (isset($input['template']) && userCan('templates-manage')) {
 | 
			
		||||
            $page->template = ($input['template'] === 'true');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->baseRepo->update($page, $input);
 | 
			
		||||
 | 
			
		||||
        // Update with new details
 | 
			
		||||
        $userId = user()->id;
 | 
			
		||||
        $page->fill($input);
 | 
			
		||||
        $page->html = $this->formatHtml($input['html']);
 | 
			
		||||
        $page->text = $this->pageToPlainText($page);
 | 
			
		||||
        $page->updated_by = $userId;
 | 
			
		||||
        $pageContent = new PageContent($page);
 | 
			
		||||
        $pageContent->setNewHTML($input['html']);
 | 
			
		||||
        $page->revision_count++;
 | 
			
		||||
 | 
			
		||||
        if (setting('app-editor') !== 'markdown') {
 | 
			
		||||
            $page->markdown = '';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($page->isDirty('name')) {
 | 
			
		||||
            $page->refreshSlug();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $page->save();
 | 
			
		||||
 | 
			
		||||
        // Remove all update drafts for this user & page.
 | 
			
		||||
        $this->userUpdatePageDraftsQuery($page, $userId)->delete();
 | 
			
		||||
        $this->getUserDraftQuery($page)->delete();
 | 
			
		||||
 | 
			
		||||
        // Save a revision after updating
 | 
			
		||||
        $summary = $input['summary'] ?? null;
 | 
			
		||||
| 
						 | 
				
			
			@ -95,24 +203,20 @@ class PageRepo extends EntityRepo
 | 
			
		|||
            $this->savePageRevision($page, $summary);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->searchService->indexEntity($page);
 | 
			
		||||
 | 
			
		||||
        return $page;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Saves a page revision into the system.
 | 
			
		||||
     * @param Page $page
 | 
			
		||||
     * @param null|string $summary
 | 
			
		||||
     * @return PageRevision
 | 
			
		||||
     * @throws \Exception
 | 
			
		||||
     */
 | 
			
		||||
    public function savePageRevision(Page $page, string $summary = null)
 | 
			
		||||
    protected function savePageRevision(Page $page, string $summary = null)
 | 
			
		||||
    {
 | 
			
		||||
        $revision = $this->entityProvider->pageRevision->newInstance($page->toArray());
 | 
			
		||||
        $revision = new PageRevision($page->toArray());
 | 
			
		||||
 | 
			
		||||
        if (setting('app-editor') !== 'markdown') {
 | 
			
		||||
            $revision->markdown = '';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $revision->page_id = $page->id;
 | 
			
		||||
        $revision->slug = $page->slug;
 | 
			
		||||
        $revision->book_slug = $page->book->slug;
 | 
			
		||||
| 
						 | 
				
			
			@ -123,163 +227,29 @@ class PageRepo extends EntityRepo
 | 
			
		|||
        $revision->revision_number = $page->revision_count;
 | 
			
		||||
        $revision->save();
 | 
			
		||||
 | 
			
		||||
        $revisionLimit = config('app.revision_limit');
 | 
			
		||||
        if ($revisionLimit !== false) {
 | 
			
		||||
            $revisionsToDelete = $this->entityProvider->pageRevision->where('page_id', '=', $page->id)
 | 
			
		||||
                ->orderBy('created_at', 'desc')->skip(intval($revisionLimit))->take(10)->get(['id']);
 | 
			
		||||
            if ($revisionsToDelete->count() > 0) {
 | 
			
		||||
                $this->entityProvider->pageRevision->whereIn('id', $revisionsToDelete->pluck('id'))->delete();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->deleteOldRevisions($page);
 | 
			
		||||
        return $revision;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Formats a page's html to be tagged correctly within the system.
 | 
			
		||||
     * @param string $htmlText
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    protected function formatHtml(string $htmlText)
 | 
			
		||||
    {
 | 
			
		||||
        if ($htmlText == '') {
 | 
			
		||||
            return $htmlText;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        libxml_use_internal_errors(true);
 | 
			
		||||
        $doc = new DOMDocument();
 | 
			
		||||
        $doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
 | 
			
		||||
 | 
			
		||||
        $container = $doc->documentElement;
 | 
			
		||||
        $body = $container->childNodes->item(0);
 | 
			
		||||
        $childNodes = $body->childNodes;
 | 
			
		||||
 | 
			
		||||
        // Set ids on top-level nodes
 | 
			
		||||
        $idMap = [];
 | 
			
		||||
        foreach ($childNodes as $index => $childNode) {
 | 
			
		||||
            $this->setUniqueId($childNode, $idMap);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Ensure no duplicate ids within child items
 | 
			
		||||
        $xPath = new DOMXPath($doc);
 | 
			
		||||
        $idElems = $xPath->query('//body//*//*[@id]');
 | 
			
		||||
        foreach ($idElems as $domElem) {
 | 
			
		||||
            $this->setUniqueId($domElem, $idMap);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Generate inner html as a string
 | 
			
		||||
        $html = '';
 | 
			
		||||
        foreach ($childNodes as $childNode) {
 | 
			
		||||
            $html .= $doc->saveHTML($childNode);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $html;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set a unique id on the given DOMElement.
 | 
			
		||||
     * A map for existing ID's should be passed in to check for current existence.
 | 
			
		||||
     * @param DOMElement $element
 | 
			
		||||
     * @param array $idMap
 | 
			
		||||
     */
 | 
			
		||||
    protected function setUniqueId($element, array &$idMap)
 | 
			
		||||
    {
 | 
			
		||||
        if (get_class($element) !== 'DOMElement') {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Overwrite id if not a BookStack custom id
 | 
			
		||||
        $existingId = $element->getAttribute('id');
 | 
			
		||||
        if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
 | 
			
		||||
            $idMap[$existingId] = true;
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Create an unique id for the element
 | 
			
		||||
        // Uses the content as a basis to ensure output is the same every time
 | 
			
		||||
        // the same content is passed through.
 | 
			
		||||
        $contentId = 'bkmrk-' . mb_substr(strtolower(preg_replace('/\s+/', '-', trim($element->nodeValue))), 0, 20);
 | 
			
		||||
        $newId = urlencode($contentId);
 | 
			
		||||
        $loopIndex = 0;
 | 
			
		||||
 | 
			
		||||
        while (isset($idMap[$newId])) {
 | 
			
		||||
            $newId = urlencode($contentId . '-' . $loopIndex);
 | 
			
		||||
            $loopIndex++;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $element->setAttribute('id', $newId);
 | 
			
		||||
        $idMap[$newId] = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the plain text version of a page's content.
 | 
			
		||||
     * @param \BookStack\Entities\Page $page
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    protected function pageToPlainText(Page $page) : string
 | 
			
		||||
    {
 | 
			
		||||
        $html = $this->renderPage($page, true);
 | 
			
		||||
        return strip_tags($html);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a new draft page instance.
 | 
			
		||||
     * @param Book $book
 | 
			
		||||
     * @param Chapter|null $chapter
 | 
			
		||||
     * @return \BookStack\Entities\Page
 | 
			
		||||
     * @throws \Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function getDraftPage(Book $book, Chapter $chapter = null)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->entityProvider->page->newInstance();
 | 
			
		||||
        $page->name = trans('entities.pages_initial_name');
 | 
			
		||||
        $page->created_by = user()->id;
 | 
			
		||||
        $page->updated_by = user()->id;
 | 
			
		||||
        $page->draft = true;
 | 
			
		||||
 | 
			
		||||
        if ($chapter) {
 | 
			
		||||
            $page->chapter_id = $chapter->id;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $book->pages()->save($page);
 | 
			
		||||
        $page->refresh()->rebuildPermissions();
 | 
			
		||||
        return $page;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Save a page update draft.
 | 
			
		||||
     * @param Page $page
 | 
			
		||||
     * @param array $data
 | 
			
		||||
     * @return PageRevision|Page
 | 
			
		||||
     */
 | 
			
		||||
    public function updatePageDraft(Page $page, array $data = [])
 | 
			
		||||
    public function updatePageDraft(Page $page, array $input)
 | 
			
		||||
    {
 | 
			
		||||
        // If the page itself is a draft simply update that
 | 
			
		||||
        if ($page->draft) {
 | 
			
		||||
            $page->fill($data);
 | 
			
		||||
            if (isset($data['html'])) {
 | 
			
		||||
                $page->text = $this->pageToPlainText($page);
 | 
			
		||||
            $page->fill($input);
 | 
			
		||||
            if (isset($input['html'])) {
 | 
			
		||||
                $content = new PageContent($page);
 | 
			
		||||
                $content->setNewHTML($input['html']);
 | 
			
		||||
            }
 | 
			
		||||
            $page->save();
 | 
			
		||||
            return $page;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Otherwise save the data to a revision
 | 
			
		||||
        $userId = user()->id;
 | 
			
		||||
        $drafts = $this->userUpdatePageDraftsQuery($page, $userId)->get();
 | 
			
		||||
 | 
			
		||||
        if ($drafts->count() > 0) {
 | 
			
		||||
            $draft = $drafts->first();
 | 
			
		||||
        } else {
 | 
			
		||||
            $draft = $this->entityProvider->pageRevision->newInstance();
 | 
			
		||||
            $draft->page_id = $page->id;
 | 
			
		||||
            $draft->slug = $page->slug;
 | 
			
		||||
            $draft->book_slug = $page->book->slug;
 | 
			
		||||
            $draft->created_by = $userId;
 | 
			
		||||
            $draft->type = 'update_draft';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $draft->fill($data);
 | 
			
		||||
        $draft = $this->getPageRevisionToUpdate($page);
 | 
			
		||||
        $draft->fill($input);
 | 
			
		||||
        if (setting('app-editor') !== 'markdown') {
 | 
			
		||||
            $draft->markdown = '';
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -289,227 +259,76 @@ class PageRepo extends EntityRepo
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Publish a draft page to make it a normal page.
 | 
			
		||||
     * Sets the slug and updates the content.
 | 
			
		||||
     * @param Page $draftPage
 | 
			
		||||
     * @param array $input
 | 
			
		||||
     * @return Page
 | 
			
		||||
     * @throws \Exception
 | 
			
		||||
     * Destroy a page from the system.
 | 
			
		||||
     * @throws NotifyException
 | 
			
		||||
     */
 | 
			
		||||
    public function publishPageDraft(Page $draftPage, array $input)
 | 
			
		||||
    public function destroy(Page $page)
 | 
			
		||||
    {
 | 
			
		||||
        $draftPage->fill($input);
 | 
			
		||||
 | 
			
		||||
        // Save page tags if present
 | 
			
		||||
        if (isset($input['tags'])) {
 | 
			
		||||
            $this->tagRepo->saveTagsToEntity($draftPage, $input['tags']);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (isset($input['template']) && userCan('templates-manage')) {
 | 
			
		||||
            $draftPage->template = ($input['template'] === 'true');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $draftPage->html = $this->formatHtml($input['html']);
 | 
			
		||||
        $draftPage->text = $this->pageToPlainText($draftPage);
 | 
			
		||||
        $draftPage->draft = false;
 | 
			
		||||
        $draftPage->revision_count = 1;
 | 
			
		||||
        $draftPage->refreshSlug();
 | 
			
		||||
        $draftPage->save();
 | 
			
		||||
        $this->savePageRevision($draftPage, trans('entities.pages_initial_revision'));
 | 
			
		||||
        $this->searchService->indexEntity($draftPage);
 | 
			
		||||
        return $draftPage;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The base query for getting user update drafts.
 | 
			
		||||
     * @param Page $page
 | 
			
		||||
     * @param $userId
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     */
 | 
			
		||||
    protected function userUpdatePageDraftsQuery(Page $page, int $userId)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->entityProvider->pageRevision->where('created_by', '=', $userId)
 | 
			
		||||
            ->where('type', 'update_draft')
 | 
			
		||||
            ->where('page_id', '=', $page->id)
 | 
			
		||||
            ->orderBy('created_at', 'desc');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the latest updated draft revision for a particular page and user.
 | 
			
		||||
     * @param Page $page
 | 
			
		||||
     * @param $userId
 | 
			
		||||
     * @return PageRevision|null
 | 
			
		||||
     */
 | 
			
		||||
    public function getUserPageDraft(Page $page, int $userId)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->userUpdatePageDraftsQuery($page, $userId)->first();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the notification message that informs the user that they are editing a draft page.
 | 
			
		||||
     * @param PageRevision $draft
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    public function getUserPageDraftMessage(PageRevision $draft)
 | 
			
		||||
    {
 | 
			
		||||
        $message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $draft->updated_at->diffForHumans()]);
 | 
			
		||||
        if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) {
 | 
			
		||||
            return $message;
 | 
			
		||||
        }
 | 
			
		||||
        return $message . "\n" . trans('entities.pages_draft_edited_notification');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A query to check for active update drafts on a particular page.
 | 
			
		||||
     * @param Page $page
 | 
			
		||||
     * @param int $minRange
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     */
 | 
			
		||||
    protected function activePageEditingQuery(Page $page, int $minRange = null)
 | 
			
		||||
    {
 | 
			
		||||
        $query = $this->entityProvider->pageRevision->where('type', '=', 'update_draft')
 | 
			
		||||
            ->where('page_id', '=', $page->id)
 | 
			
		||||
            ->where('updated_at', '>', $page->updated_at)
 | 
			
		||||
            ->where('created_by', '!=', user()->id)
 | 
			
		||||
            ->with('createdBy');
 | 
			
		||||
 | 
			
		||||
        if ($minRange !== null) {
 | 
			
		||||
            $query = $query->where('updated_at', '>=', Carbon::now()->subMinutes($minRange));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $query;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a page is being actively editing.
 | 
			
		||||
     * Checks for edits since last page updated.
 | 
			
		||||
     * Passing in a minuted range will check for edits
 | 
			
		||||
     * within the last x minutes.
 | 
			
		||||
     * @param Page $page
 | 
			
		||||
     * @param int $minRange
 | 
			
		||||
     * @return bool
 | 
			
		||||
     */
 | 
			
		||||
    public function isPageEditingActive(Page $page, int $minRange = null)
 | 
			
		||||
    {
 | 
			
		||||
        $draftSearch = $this->activePageEditingQuery($page, $minRange);
 | 
			
		||||
        return $draftSearch->count() > 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a notification message concerning the editing activity on a particular page.
 | 
			
		||||
     * @param Page $page
 | 
			
		||||
     * @param int $minRange
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    public function getPageEditingActiveMessage(Page $page, int $minRange = null)
 | 
			
		||||
    {
 | 
			
		||||
        $pageDraftEdits = $this->activePageEditingQuery($page, $minRange)->get();
 | 
			
		||||
 | 
			
		||||
        $userMessage = $pageDraftEdits->count() > 1 ? trans('entities.pages_draft_edit_active.start_a', ['count' => $pageDraftEdits->count()]): trans('entities.pages_draft_edit_active.start_b', ['userName' => $pageDraftEdits->first()->createdBy->name]);
 | 
			
		||||
        $timeMessage = $minRange === null ? trans('entities.pages_draft_edit_active.time_a') : trans('entities.pages_draft_edit_active.time_b', ['minCount'=>$minRange]);
 | 
			
		||||
        return trans('entities.pages_draft_edit_active.message', ['start' => $userMessage, 'time' => $timeMessage]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parse the headers on the page to get a navigation menu
 | 
			
		||||
     * @param string $pageContent
 | 
			
		||||
     * @return array
 | 
			
		||||
     */
 | 
			
		||||
    public function getPageNav(string $pageContent)
 | 
			
		||||
    {
 | 
			
		||||
        if ($pageContent == '') {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
        libxml_use_internal_errors(true);
 | 
			
		||||
        $doc = new DOMDocument();
 | 
			
		||||
        $doc->loadHTML(mb_convert_encoding($pageContent, 'HTML-ENTITIES', 'UTF-8'));
 | 
			
		||||
        $xPath = new DOMXPath($doc);
 | 
			
		||||
        $headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
 | 
			
		||||
 | 
			
		||||
        if (is_null($headers)) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $tree = collect($headers)->map(function ($header) {
 | 
			
		||||
            $text = trim(str_replace("\xc2\xa0", '', $header->nodeValue));
 | 
			
		||||
            $text = mb_substr($text, 0, 100);
 | 
			
		||||
 | 
			
		||||
            return [
 | 
			
		||||
                'nodeName' => strtolower($header->nodeName),
 | 
			
		||||
                'level' => intval(str_replace('h', '', $header->nodeName)),
 | 
			
		||||
                'link' => '#' . $header->getAttribute('id'),
 | 
			
		||||
                'text' => $text,
 | 
			
		||||
            ];
 | 
			
		||||
        })->filter(function ($header) {
 | 
			
		||||
            return mb_strlen($header['text']) > 0;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Shift headers if only smaller headers have been used
 | 
			
		||||
        $levelChange = ($tree->pluck('level')->min() - 1);
 | 
			
		||||
        $tree = $tree->map(function ($header) use ($levelChange) {
 | 
			
		||||
            $header['level'] -= ($levelChange);
 | 
			
		||||
            return $header;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return $tree->toArray();
 | 
			
		||||
        $trashCan = new TrashCan();
 | 
			
		||||
        $trashCan->destroyPage($page);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Restores a revision's content back into a page.
 | 
			
		||||
     * @param Page $page
 | 
			
		||||
     * @param Book $book
 | 
			
		||||
     * @param  int $revisionId
 | 
			
		||||
     * @return Page
 | 
			
		||||
     * @throws \Exception
 | 
			
		||||
     */
 | 
			
		||||
    public function restorePageRevision(Page $page, Book $book, int $revisionId)
 | 
			
		||||
    public function restoreRevision(Page $page, int $revisionId): Page
 | 
			
		||||
    {
 | 
			
		||||
        $page->revision_count++;
 | 
			
		||||
        $this->savePageRevision($page);
 | 
			
		||||
 | 
			
		||||
        $revision = $page->revisions()->where('id', '=', $revisionId)->first();
 | 
			
		||||
        $page->fill($revision->toArray());
 | 
			
		||||
        $page->text = $this->pageToPlainText($page);
 | 
			
		||||
        $content = new PageContent($page);
 | 
			
		||||
        $content->setNewHTML($page->html);
 | 
			
		||||
        $page->updated_by = user()->id;
 | 
			
		||||
        $page->refreshSlug();
 | 
			
		||||
        $page->save();
 | 
			
		||||
 | 
			
		||||
        $this->searchService->indexEntity($page);
 | 
			
		||||
        $page->indexForSearch();
 | 
			
		||||
        return $page;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Change the page's parent to the given entity.
 | 
			
		||||
     * @param Page $page
 | 
			
		||||
     * @param Entity $parent
 | 
			
		||||
     * Move the given page into a new parent book or chapter.
 | 
			
		||||
     * The $parentIdentifier must be a string of the following format:
 | 
			
		||||
     * 'book:<id>' (book:5)
 | 
			
		||||
     * @throws MoveOperationException
 | 
			
		||||
     * @throws PermissionsException
 | 
			
		||||
     */
 | 
			
		||||
    public function changePageParent(Page $page, Entity $parent)
 | 
			
		||||
    public function move(Page $page, string $parentIdentifier): Book
 | 
			
		||||
    {
 | 
			
		||||
        $book = $parent->isA('book') ? $parent : $parent->book;
 | 
			
		||||
        $page->chapter_id = $parent->isA('chapter') ? $parent->id : 0;
 | 
			
		||||
        $page->save();
 | 
			
		||||
 | 
			
		||||
        if ($page->book->id !== $book->id) {
 | 
			
		||||
            $page = $this->changeBook($page, $book->id);
 | 
			
		||||
        $parent = $this->findParentByIdentifier($parentIdentifier);
 | 
			
		||||
        if ($parent === null) {
 | 
			
		||||
            throw new MoveOperationException('Book or chapter to move page into not found');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $page->load('book');
 | 
			
		||||
        $book->rebuildPermissions();
 | 
			
		||||
        if (!userCan('page-create', $parent)) {
 | 
			
		||||
            throw new PermissionsException('User does not have permission to create a page within the new parent');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $page->changeBook($parent instanceof Book ? $parent->id : $parent->book->id);
 | 
			
		||||
        $page->rebuildPermissions();
 | 
			
		||||
        return $parent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a copy of a page in a new location with a new name.
 | 
			
		||||
     * @param \BookStack\Entities\Page $page
 | 
			
		||||
     * @param \BookStack\Entities\Entity $newParent
 | 
			
		||||
     * @param string $newName
 | 
			
		||||
     * @return \BookStack\Entities\Page
 | 
			
		||||
     * @throws \Throwable
 | 
			
		||||
     * Copy an existing page in the system.
 | 
			
		||||
     * Optionally providing a new parent via string identifier and a new name.
 | 
			
		||||
     * @throws MoveOperationException
 | 
			
		||||
     * @throws PermissionsException
 | 
			
		||||
     */
 | 
			
		||||
    public function copyPage(Page $page, Entity $newParent, string $newName = '')
 | 
			
		||||
    public function copy(Page $page, string $parentIdentifier = null, string $newName = null): Page
 | 
			
		||||
    {
 | 
			
		||||
        $newBook = $newParent->isA('book') ? $newParent : $newParent->book;
 | 
			
		||||
        $newChapter = $newParent->isA('chapter') ? $newParent : null;
 | 
			
		||||
        $copyPage = $this->getDraftPage($newBook, $newChapter);
 | 
			
		||||
        $parent = $parentIdentifier ? $this->findParentByIdentifier($parentIdentifier) : $page->parent();
 | 
			
		||||
        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
 | 
			
		||||
| 
						 | 
				
			
			@ -525,38 +344,116 @@ class PageRepo extends EntityRepo
 | 
			
		|||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Set priority
 | 
			
		||||
        if ($newParent->isA('chapter')) {
 | 
			
		||||
            $pageData['priority'] = $this->getNewChapterPriority($newParent);
 | 
			
		||||
        } else {
 | 
			
		||||
            $pageData['priority'] = $this->getNewBookPriority($newParent);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->publishPageDraft($copyPage, $pageData);
 | 
			
		||||
        return $this->publishDraft($copyPage, $pageData);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get pages that have been marked as templates.
 | 
			
		||||
     * @param int $count
 | 
			
		||||
     * @param int $page
 | 
			
		||||
     * @param string $search
 | 
			
		||||
     * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
 | 
			
		||||
     * Find a page parent entity via a identifier string in the format:
 | 
			
		||||
     * {type}:{id}
 | 
			
		||||
     * Example: (book:5)
 | 
			
		||||
     * @throws MoveOperationException
 | 
			
		||||
     */
 | 
			
		||||
    public function getPageTemplates(int $count = 10, int $page = 1, string $search = '')
 | 
			
		||||
    protected function findParentByIdentifier(string $identifier): ?Entity
 | 
			
		||||
    {
 | 
			
		||||
        $query = $this->entityQuery('page')
 | 
			
		||||
            ->where('template', '=', true)
 | 
			
		||||
            ->orderBy('name', 'asc')
 | 
			
		||||
            ->skip(($page - 1) * $count)
 | 
			
		||||
            ->take($count);
 | 
			
		||||
        $stringExploded = explode(':', $identifier);
 | 
			
		||||
        $entityType = $stringExploded[0];
 | 
			
		||||
        $entityId = intval($stringExploded[1]);
 | 
			
		||||
 | 
			
		||||
        if ($search) {
 | 
			
		||||
            $query->where('name', 'like', '%' . $search . '%');
 | 
			
		||||
        if ($entityType !== 'book' && $entityType !== 'chapter') {
 | 
			
		||||
            throw new MoveOperationException('Pages can only be in books or chapters');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $paginator = $query->paginate($count, ['*'], 'page', $page);
 | 
			
		||||
        $paginator->withPath('/templates');
 | 
			
		||||
        $parentClass = $entityType === 'book' ? Book::class : Chapter::class;
 | 
			
		||||
        return $parentClass::visible()->where('id', '=', $entityId)->first();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        return $paginator;
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the permissions of a page.
 | 
			
		||||
     */
 | 
			
		||||
    public function updatePermissions(Page $page, bool $restricted, Collection $permissions = null)
 | 
			
		||||
    {
 | 
			
		||||
        $this->baseRepo->updatePermissions($page, $restricted, $permissions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Change the page's parent to the given entity.
 | 
			
		||||
     */
 | 
			
		||||
    protected function changeParent(Page $page, Entity $parent)
 | 
			
		||||
    {
 | 
			
		||||
        $book = ($parent instanceof Book) ? $parent : $parent->book;
 | 
			
		||||
        $page->chapter_id = ($parent instanceof Chapter) ? $parent->id : 0;
 | 
			
		||||
        $page->save();
 | 
			
		||||
 | 
			
		||||
        if ($page->book->id !== $book->id) {
 | 
			
		||||
            $page->changeBook($book->id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $page->load('book');
 | 
			
		||||
        $book->rebuildPermissions();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a page revision to update for the given page.
 | 
			
		||||
     * Checks for an existing revisions before providing a fresh one.
 | 
			
		||||
     */
 | 
			
		||||
    protected function getPageRevisionToUpdate(Page $page): PageRevision
 | 
			
		||||
    {
 | 
			
		||||
        $drafts = $this->getUserDraftQuery($page)->get();
 | 
			
		||||
        if ($drafts->count() > 0) {
 | 
			
		||||
            return $drafts->first();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $draft = new PageRevision();
 | 
			
		||||
        $draft->page_id = $page->id;
 | 
			
		||||
        $draft->slug = $page->slug;
 | 
			
		||||
        $draft->book_slug = $page->book->slug;
 | 
			
		||||
        $draft->created_by = user()->id;
 | 
			
		||||
        $draft->type = 'update_draft';
 | 
			
		||||
        return $draft;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete old revisions, for the given page, from the system.
 | 
			
		||||
     */
 | 
			
		||||
    protected function deleteOldRevisions(Page $page)
 | 
			
		||||
    {
 | 
			
		||||
        $revisionLimit = config('app.revision_limit');
 | 
			
		||||
        if ($revisionLimit === false) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $revisionsToDelete = PageRevision::query()
 | 
			
		||||
            ->where('page_id', '=', $page->id)
 | 
			
		||||
            ->orderBy('created_at', 'desc')
 | 
			
		||||
            ->skip(intval($revisionLimit))
 | 
			
		||||
            ->take(10)
 | 
			
		||||
            ->get(['id']);
 | 
			
		||||
        if ($revisionsToDelete->count() > 0) {
 | 
			
		||||
            PageRevision::query()->whereIn('id', $revisionsToDelete->pluck('id'))->delete();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a new priority for a page
 | 
			
		||||
     */
 | 
			
		||||
    protected function getNewPriority(Page $page): int
 | 
			
		||||
    {
 | 
			
		||||
        if ($page->parent() instanceof Chapter) {
 | 
			
		||||
            $lastPage = $page->parent()->pages('desc')->first();
 | 
			
		||||
            return $lastPage ? $lastPage->priority + 1 : 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return (new BookContents($page->book))->getLastPriority() + 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the query to find the user's draft copies of the given page.
 | 
			
		||||
     */
 | 
			
		||||
    protected function getUserDraftQuery(Page $page)
 | 
			
		||||
    {
 | 
			
		||||
        return PageRevision::query()->where('created_by', '=', user()->id)
 | 
			
		||||
            ->where('type', 'update_draft')
 | 
			
		||||
            ->where('page_id', '=', $page->id)
 | 
			
		||||
            ->orderBy('created_at', 'desc');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -59,6 +59,4 @@ class SlugGenerator
 | 
			
		|||
 | 
			
		||||
        return $query->count() > 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
<?php namespace BookStack\Exceptions;
 | 
			
		||||
 | 
			
		||||
use Exception;
 | 
			
		||||
 | 
			
		||||
class MoveOperationException extends Exception
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
<?php namespace BookStack\Exceptions;
 | 
			
		||||
 | 
			
		||||
use Exception;
 | 
			
		||||
 | 
			
		||||
class SortOperationException extends Exception
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,37 +1,37 @@
 | 
			
		|||
<?php namespace BookStack\Http\Controllers;
 | 
			
		||||
 | 
			
		||||
use BookStack\Entities\Repos\EntityRepo;
 | 
			
		||||
use BookStack\Entities\Repos\PageRepo;
 | 
			
		||||
use BookStack\Exceptions\FileUploadException;
 | 
			
		||||
use BookStack\Exceptions\NotFoundException;
 | 
			
		||||
use BookStack\Uploads\Attachment;
 | 
			
		||||
use BookStack\Uploads\AttachmentService;
 | 
			
		||||
use Exception;
 | 
			
		||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
use Illuminate\Validation\ValidationException;
 | 
			
		||||
 | 
			
		||||
class AttachmentController extends Controller
 | 
			
		||||
{
 | 
			
		||||
    protected $attachmentService;
 | 
			
		||||
    protected $attachment;
 | 
			
		||||
    protected $entityRepo;
 | 
			
		||||
    protected $pageRepo;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * AttachmentController constructor.
 | 
			
		||||
     * @param \BookStack\Uploads\AttachmentService $attachmentService
 | 
			
		||||
     * @param Attachment $attachment
 | 
			
		||||
     * @param EntityRepo $entityRepo
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(AttachmentService $attachmentService, Attachment $attachment, EntityRepo $entityRepo)
 | 
			
		||||
    public function __construct(AttachmentService $attachmentService, Attachment $attachment, PageRepo $pageRepo)
 | 
			
		||||
    {
 | 
			
		||||
        $this->attachmentService = $attachmentService;
 | 
			
		||||
        $this->attachment = $attachment;
 | 
			
		||||
        $this->entityRepo = $entityRepo;
 | 
			
		||||
        $this->pageRepo = $pageRepo;
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Endpoint at which attachments are uploaded to.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
 | 
			
		||||
     * @throws ValidationException
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function upload(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -41,7 +41,7 @@ class AttachmentController extends Controller
 | 
			
		|||
        ]);
 | 
			
		||||
 | 
			
		||||
        $pageId = $request->get('uploaded_to');
 | 
			
		||||
        $page = $this->entityRepo->getById('page', $pageId, true);
 | 
			
		||||
        $page = $this->pageRepo->getById($pageId);
 | 
			
		||||
 | 
			
		||||
        $this->checkPermission('attachment-create-all');
 | 
			
		||||
        $this->checkOwnablePermission('page-update', $page);
 | 
			
		||||
| 
						 | 
				
			
			@ -59,10 +59,8 @@ class AttachmentController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update an uploaded attachment.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @param int $attachmentId
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     * @throws \Illuminate\Validation\ValidationException
 | 
			
		||||
     * @throws ValidationException
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function uploadUpdate(Request $request, $attachmentId)
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -72,7 +70,7 @@ class AttachmentController extends Controller
 | 
			
		|||
        ]);
 | 
			
		||||
 | 
			
		||||
        $pageId = $request->get('uploaded_to');
 | 
			
		||||
        $page = $this->entityRepo->getById('page', $pageId, true);
 | 
			
		||||
        $page = $this->pageRepo->getById($pageId);
 | 
			
		||||
        $attachment = $this->attachment->findOrFail($attachmentId);
 | 
			
		||||
 | 
			
		||||
        $this->checkOwnablePermission('page-update', $page);
 | 
			
		||||
| 
						 | 
				
			
			@ -95,10 +93,8 @@ class AttachmentController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the details of an existing file.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @param $attachmentId
 | 
			
		||||
     * @return Attachment|mixed
 | 
			
		||||
     * @throws \Illuminate\Validation\ValidationException
 | 
			
		||||
     * @throws ValidationException
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function update(Request $request, $attachmentId)
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -109,7 +105,7 @@ class AttachmentController extends Controller
 | 
			
		|||
        ]);
 | 
			
		||||
 | 
			
		||||
        $pageId = $request->get('uploaded_to');
 | 
			
		||||
        $page = $this->entityRepo->getById('page', $pageId, true);
 | 
			
		||||
        $page = $this->pageRepo->getById($pageId);
 | 
			
		||||
        $attachment = $this->attachment->findOrFail($attachmentId);
 | 
			
		||||
 | 
			
		||||
        $this->checkOwnablePermission('page-update', $page);
 | 
			
		||||
| 
						 | 
				
			
			@ -125,8 +121,8 @@ class AttachmentController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Attach a link to a page.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     * @throws ValidationException
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function attachLink(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -137,7 +133,7 @@ class AttachmentController extends Controller
 | 
			
		|||
        ]);
 | 
			
		||||
 | 
			
		||||
        $pageId = $request->get('uploaded_to');
 | 
			
		||||
        $page = $this->entityRepo->getById('page', $pageId, true);
 | 
			
		||||
        $page = $this->pageRepo->getById($pageId);
 | 
			
		||||
 | 
			
		||||
        $this->checkPermission('attachment-create-all');
 | 
			
		||||
        $this->checkOwnablePermission('page-update', $page);
 | 
			
		||||
| 
						 | 
				
			
			@ -151,30 +147,26 @@ class AttachmentController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the attachments for a specific page.
 | 
			
		||||
     * @param $pageId
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     */
 | 
			
		||||
    public function listForPage($pageId)
 | 
			
		||||
    public function listForPage(int $pageId)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->entityRepo->getById('page', $pageId, true);
 | 
			
		||||
        $page = $this->pageRepo->getById($pageId);
 | 
			
		||||
        $this->checkOwnablePermission('page-view', $page);
 | 
			
		||||
        return response()->json($page->attachments);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the attachment sorting.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @param $pageId
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     * @throws \Illuminate\Validation\ValidationException
 | 
			
		||||
     * @throws ValidationException
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function sortForPage(Request $request, $pageId)
 | 
			
		||||
    public function sortForPage(Request $request, int $pageId)
 | 
			
		||||
    {
 | 
			
		||||
        $this->validate($request, [
 | 
			
		||||
            'files' => 'required|array',
 | 
			
		||||
            'files.*.id' => 'required|integer',
 | 
			
		||||
        ]);
 | 
			
		||||
        $page = $this->entityRepo->getById('page', $pageId);
 | 
			
		||||
        $page = $this->pageRepo->getById($pageId);
 | 
			
		||||
        $this->checkOwnablePermission('page-update', $page);
 | 
			
		||||
 | 
			
		||||
        $attachments = $request->get('files');
 | 
			
		||||
| 
						 | 
				
			
			@ -184,16 +176,15 @@ class AttachmentController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an attachment from storage.
 | 
			
		||||
     * @param $attachmentId
 | 
			
		||||
     * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Symfony\Component\HttpFoundation\Response
 | 
			
		||||
     * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
 | 
			
		||||
     * @throws FileNotFoundException
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function get($attachmentId)
 | 
			
		||||
    public function get(int $attachmentId)
 | 
			
		||||
    {
 | 
			
		||||
        $attachment = $this->attachment->findOrFail($attachmentId);
 | 
			
		||||
        $page = $this->entityRepo->getById('page', $attachment->uploaded_to);
 | 
			
		||||
        if ($page === null) {
 | 
			
		||||
        try {
 | 
			
		||||
            $page = $this->pageRepo->getById($attachment->uploaded_to);
 | 
			
		||||
        } catch (NotFoundException $exception) {
 | 
			
		||||
            throw new NotFoundException(trans('errors.attachment_not_found'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -211,9 +202,9 @@ class AttachmentController extends Controller
 | 
			
		|||
     * Delete a specific attachment in the system.
 | 
			
		||||
     * @param $attachmentId
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     * @throws \Exception
 | 
			
		||||
     * @throws Exception
 | 
			
		||||
     */
 | 
			
		||||
    public function delete($attachmentId)
 | 
			
		||||
    public function delete(int $attachmentId)
 | 
			
		||||
    {
 | 
			
		||||
        $attachment = $this->attachment->findOrFail($attachmentId);
 | 
			
		||||
        $this->checkOwnablePermission('attachment-delete', $attachment);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -65,14 +65,14 @@ class ConfirmEmailController extends Controller
 | 
			
		|||
            $userId = $this->emailConfirmationService->checkTokenAndGetUserId($token);
 | 
			
		||||
        } catch (Exception $exception) {
 | 
			
		||||
            if ($exception instanceof UserTokenNotFoundException) {
 | 
			
		||||
                $this->showErrorNotification( trans('errors.email_confirmation_invalid'));
 | 
			
		||||
                $this->showErrorNotification(trans('errors.email_confirmation_invalid'));
 | 
			
		||||
                return redirect('/register');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if ($exception instanceof UserTokenExpiredException) {
 | 
			
		||||
                $user = $this->userRepo->getById($exception->userId);
 | 
			
		||||
                $this->emailConfirmationService->sendConfirmation($user);
 | 
			
		||||
                $this->showErrorNotification( trans('errors.email_confirmation_expired'));
 | 
			
		||||
                $this->showErrorNotification(trans('errors.email_confirmation_expired'));
 | 
			
		||||
                return redirect('/register/confirm');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -84,7 +84,7 @@ class ConfirmEmailController extends Controller
 | 
			
		|||
        $user->save();
 | 
			
		||||
 | 
			
		||||
        auth()->login($user);
 | 
			
		||||
        $this->showSuccessNotification( trans('auth.email_confirm_success'));
 | 
			
		||||
        $this->showSuccessNotification(trans('auth.email_confirm_success'));
 | 
			
		||||
        $this->emailConfirmationService->deleteByUser($user);
 | 
			
		||||
 | 
			
		||||
        return redirect('/');
 | 
			
		||||
| 
						 | 
				
			
			@ -106,11 +106,11 @@ class ConfirmEmailController extends Controller
 | 
			
		|||
        try {
 | 
			
		||||
            $this->emailConfirmationService->sendConfirmation($user);
 | 
			
		||||
        } catch (Exception $e) {
 | 
			
		||||
            $this->showErrorNotification( trans('auth.email_confirm_send_error'));
 | 
			
		||||
            $this->showErrorNotification(trans('auth.email_confirm_send_error'));
 | 
			
		||||
            return redirect('/register/confirm');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->showSuccessNotification( trans('auth.email_confirm_resent'));
 | 
			
		||||
        $this->showSuccessNotification(trans('auth.email_confirm_resent'));
 | 
			
		||||
        return redirect('/register/confirm');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -53,7 +53,7 @@ class ForgotPasswordController extends Controller
 | 
			
		|||
 | 
			
		||||
        if ($response === Password::RESET_LINK_SENT) {
 | 
			
		||||
            $message = trans('auth.reset_password_sent_success', ['email' => $request->get('email')]);
 | 
			
		||||
            $this->showSuccessNotification( $message);
 | 
			
		||||
            $this->showSuccessNotification($message);
 | 
			
		||||
            return back()->with('status', trans($response));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,7 +44,7 @@ class ResetPasswordController extends Controller
 | 
			
		|||
    protected function sendResetResponse(Request $request, $response)
 | 
			
		||||
    {
 | 
			
		||||
        $message = trans('auth.reset_password_success');
 | 
			
		||||
        $this->showSuccessNotification( $message);
 | 
			
		||||
        $this->showSuccessNotification($message);
 | 
			
		||||
        return redirect($this->redirectPath())
 | 
			
		||||
            ->with('status', trans($response));
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -77,7 +77,7 @@ class UserInviteController extends Controller
 | 
			
		|||
        $user->save();
 | 
			
		||||
 | 
			
		||||
        auth()->login($user);
 | 
			
		||||
        $this->showSuccessNotification( trans('auth.user_invite_success', ['appName' => setting('app-name')]));
 | 
			
		||||
        $this->showSuccessNotification(trans('auth.user_invite_success', ['appName' => setting('app-name')]));
 | 
			
		||||
        $this->inviteService->deleteByUser($user);
 | 
			
		||||
 | 
			
		||||
        return redirect('/');
 | 
			
		||||
| 
						 | 
				
			
			@ -96,7 +96,7 @@ class UserInviteController extends Controller
 | 
			
		|||
        }
 | 
			
		||||
 | 
			
		||||
        if ($exception instanceof UserTokenExpiredException) {
 | 
			
		||||
            $this->showErrorNotification( trans('errors.invite_token_expired'));
 | 
			
		||||
            $this->showErrorNotification(trans('errors.invite_token_expired'));
 | 
			
		||||
            return redirect('/password/email');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,22 +1,14 @@
 | 
			
		|||
<?php namespace BookStack\Http\Controllers;
 | 
			
		||||
 | 
			
		||||
use Activity;
 | 
			
		||||
use BookStack\Auth\UserRepo;
 | 
			
		||||
use BookStack\Entities\Book;
 | 
			
		||||
use BookStack\Entities\Managers\BookContents;
 | 
			
		||||
use BookStack\Entities\Bookshelf;
 | 
			
		||||
use BookStack\Entities\EntityContextManager;
 | 
			
		||||
use BookStack\Entities\Managers\EntityContext;
 | 
			
		||||
use BookStack\Entities\Repos\BookRepo;
 | 
			
		||||
use BookStack\Exceptions\ImageUploadException;
 | 
			
		||||
use BookStack\Exceptions\NotFoundException;
 | 
			
		||||
use BookStack\Exceptions\NotifyException;
 | 
			
		||||
use BookStack\Uploads\ImageRepo;
 | 
			
		||||
use Illuminate\Contracts\View\Factory;
 | 
			
		||||
use Illuminate\Http\RedirectResponse;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
use Illuminate\Http\Response;
 | 
			
		||||
use Illuminate\Routing\Redirector;
 | 
			
		||||
use Illuminate\Validation\ValidationException;
 | 
			
		||||
use Illuminate\View\View;
 | 
			
		||||
use Throwable;
 | 
			
		||||
use Views;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -24,33 +16,20 @@ class BookController extends Controller
 | 
			
		|||
{
 | 
			
		||||
 | 
			
		||||
    protected $bookRepo;
 | 
			
		||||
    protected $userRepo;
 | 
			
		||||
    protected $entityContextManager;
 | 
			
		||||
    protected $imageRepo;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * BookController constructor.
 | 
			
		||||
     * @param BookRepo $bookRepo
 | 
			
		||||
     * @param UserRepo $userRepo
 | 
			
		||||
     * @param EntityContextManager $entityContextManager
 | 
			
		||||
     * @param ImageRepo $imageRepo
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        BookRepo $bookRepo,
 | 
			
		||||
        UserRepo $userRepo,
 | 
			
		||||
        EntityContextManager $entityContextManager,
 | 
			
		||||
        ImageRepo $imageRepo
 | 
			
		||||
    ) {
 | 
			
		||||
    public function __construct(EntityContext $entityContextManager, BookRepo $bookRepo)
 | 
			
		||||
    {
 | 
			
		||||
        $this->bookRepo = $bookRepo;
 | 
			
		||||
        $this->userRepo = $userRepo;
 | 
			
		||||
        $this->entityContextManager = $entityContextManager;
 | 
			
		||||
        $this->imageRepo = $imageRepo;
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Display a listing of the book.
 | 
			
		||||
     * @return Response
 | 
			
		||||
     */
 | 
			
		||||
    public function index()
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -58,10 +37,10 @@ class BookController extends Controller
 | 
			
		|||
        $sort = setting()->getForCurrentUser('books_sort', 'name');
 | 
			
		||||
        $order = setting()->getForCurrentUser('books_sort_order', 'asc');
 | 
			
		||||
 | 
			
		||||
        $books = $this->bookRepo->getAllPaginated('book', 18, $sort, $order);
 | 
			
		||||
        $recents = $this->isSignedIn() ? $this->bookRepo->getRecentlyViewed('book', 4, 0) : false;
 | 
			
		||||
        $popular = $this->bookRepo->getPopular('book', 4, 0);
 | 
			
		||||
        $new = $this->bookRepo->getRecentlyCreated('book', 4, 0);
 | 
			
		||||
        $books = $this->bookRepo->getAllPaginated(18, $sort, $order);
 | 
			
		||||
        $recents = $this->isSignedIn() ? $this->bookRepo->getRecentlyViewed(4) : false;
 | 
			
		||||
        $popular = $this->bookRepo->getPopular(4);
 | 
			
		||||
        $new = $this->bookRepo->getRecentlyCreated(4);
 | 
			
		||||
 | 
			
		||||
        $this->entityContextManager->clearShelfContext();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -79,19 +58,17 @@ class BookController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the form for creating a new book.
 | 
			
		||||
     * @param string $shelfSlug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function create(string $shelfSlug = null)
 | 
			
		||||
    {
 | 
			
		||||
        $this->checkPermission('book-create-all');
 | 
			
		||||
 | 
			
		||||
        $bookshelf = null;
 | 
			
		||||
        if ($shelfSlug !== null) {
 | 
			
		||||
            $bookshelf = $this->bookRepo->getEntityBySlug('bookshelf', $shelfSlug);
 | 
			
		||||
            $bookshelf = Bookshelf::visible()->where('slug', '=', $shelfSlug)->firstOrFail();
 | 
			
		||||
            $this->checkOwnablePermission('bookshelf-update', $bookshelf);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->checkPermission('book-create-all');
 | 
			
		||||
        $this->setPageTitle(trans('entities.books_create'));
 | 
			
		||||
        return view('books.create', [
 | 
			
		||||
            'bookshelf' => $bookshelf
 | 
			
		||||
| 
						 | 
				
			
			@ -100,11 +77,6 @@ class BookController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Store a newly created book in storage.
 | 
			
		||||
     *
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @param string $shelfSlug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     * @throws ImageUploadException
 | 
			
		||||
     * @throws ValidationException
 | 
			
		||||
     */
 | 
			
		||||
| 
						 | 
				
			
			@ -114,19 +86,17 @@ class BookController extends Controller
 | 
			
		|||
        $this->validate($request, [
 | 
			
		||||
            'name' => 'required|string|max:255',
 | 
			
		||||
            'description' => 'string|max:1000',
 | 
			
		||||
            'image' => $this->imageRepo->getImageValidationRules(),
 | 
			
		||||
            'image' => $this->getImageValidationRules(),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $bookshelf = null;
 | 
			
		||||
        if ($shelfSlug !== null) {
 | 
			
		||||
            /** @var Bookshelf $bookshelf */
 | 
			
		||||
            $bookshelf = $this->bookRepo->getEntityBySlug('bookshelf', $shelfSlug);
 | 
			
		||||
            $bookshelf = Bookshelf::visible()->where('slug', '=', $shelfSlug)->firstOrFail();
 | 
			
		||||
            $this->checkOwnablePermission('bookshelf-update', $bookshelf);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /** @var Book $book */
 | 
			
		||||
        $book = $this->bookRepo->createFromInput('book', $request->all());
 | 
			
		||||
        $this->bookUpdateActions($book, $request);
 | 
			
		||||
        $book = $this->bookRepo->create($request->all());
 | 
			
		||||
        $this->bookRepo->updateCoverImage($book, $request->file('image', null));
 | 
			
		||||
        Activity::add($book, 'book_create', $book->id);
 | 
			
		||||
 | 
			
		||||
        if ($bookshelf) {
 | 
			
		||||
| 
						 | 
				
			
			@ -139,17 +109,11 @@ class BookController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Display the specified book.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @param string $slug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function show(Request $request, string $slug)
 | 
			
		||||
    {
 | 
			
		||||
        $book = $this->bookRepo->getBySlug($slug);
 | 
			
		||||
        $this->checkOwnablePermission('book-view', $book);
 | 
			
		||||
 | 
			
		||||
        $bookChildren = $this->bookRepo->getBookChildren($book);
 | 
			
		||||
        $bookChildren = (new BookContents($book))->getTree(true);
 | 
			
		||||
 | 
			
		||||
        Views::add($book);
 | 
			
		||||
        if ($request->has('shelf')) {
 | 
			
		||||
| 
						 | 
				
			
			@ -167,9 +131,6 @@ class BookController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the form for editing the specified book.
 | 
			
		||||
     * @param string $slug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function edit(string $slug)
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -181,11 +142,7 @@ class BookController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the specified book in storage.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @param string $slug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @throws ImageUploadException
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     * @throws ValidationException
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
| 
						 | 
				
			
			@ -196,22 +153,20 @@ class BookController extends Controller
 | 
			
		|||
        $this->validate($request, [
 | 
			
		||||
            'name' => 'required|string|max:255',
 | 
			
		||||
            'description' => 'string|max:1000',
 | 
			
		||||
            'image' => $this->imageRepo->getImageValidationRules(),
 | 
			
		||||
            'image' => $this->getImageValidationRules(),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
         $book = $this->bookRepo->updateFromInput($book, $request->all());
 | 
			
		||||
         $this->bookUpdateActions($book, $request);
 | 
			
		||||
        $book = $this->bookRepo->update($book, $request->all());
 | 
			
		||||
        $resetCover = $request->has('image_reset');
 | 
			
		||||
        $this->bookRepo->updateCoverImage($book, $request->file('image', null), $resetCover);
 | 
			
		||||
 | 
			
		||||
         Activity::add($book, 'book_update', $book->id);
 | 
			
		||||
        Activity::add($book, 'book_update', $book->id);
 | 
			
		||||
 | 
			
		||||
         return redirect($book->getUrl());
 | 
			
		||||
        return redirect($book->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Shows the page to confirm deletion
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @return View
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     * Shows the page to confirm deletion.
 | 
			
		||||
     */
 | 
			
		||||
    public function showDelete(string $bookSlug)
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -222,115 +177,7 @@ class BookController extends Controller
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Shows the view which allows pages to be re-ordered and sorted.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @return View
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function sort(string $bookSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $book = $this->bookRepo->getBySlug($bookSlug);
 | 
			
		||||
        $this->checkOwnablePermission('book-update', $book);
 | 
			
		||||
 | 
			
		||||
        $bookChildren = $this->bookRepo->getBookChildren($book, true);
 | 
			
		||||
 | 
			
		||||
        $this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
 | 
			
		||||
        return view('books.sort', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Shows the sort box for a single book.
 | 
			
		||||
     * Used via AJAX when loading in extra books to a sort.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @return Factory|View
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function sortItem(string $bookSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $book = $this->bookRepo->getBySlug($bookSlug);
 | 
			
		||||
        $bookChildren = $this->bookRepo->getBookChildren($book);
 | 
			
		||||
        return view('books.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Saves an array of sort mapping to pages and chapters.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @return RedirectResponse|Redirector
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function saveSort(Request $request, string $bookSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $book = $this->bookRepo->getBySlug($bookSlug);
 | 
			
		||||
        $this->checkOwnablePermission('book-update', $book);
 | 
			
		||||
 | 
			
		||||
        // Return if no map sent
 | 
			
		||||
        if (!$request->filled('sort-tree')) {
 | 
			
		||||
            return redirect($book->getUrl());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Sort pages and chapters
 | 
			
		||||
        $sortMap = collect(json_decode($request->get('sort-tree')));
 | 
			
		||||
        $bookIdsInvolved = collect([$book->id]);
 | 
			
		||||
 | 
			
		||||
        // Load models into map
 | 
			
		||||
        $sortMap->each(function ($mapItem) use ($bookIdsInvolved) {
 | 
			
		||||
            $mapItem->type = ($mapItem->type === 'page' ? 'page' : 'chapter');
 | 
			
		||||
            $mapItem->model = $this->bookRepo->getById($mapItem->type, $mapItem->id);
 | 
			
		||||
            // Store source and target books
 | 
			
		||||
            $bookIdsInvolved->push(intval($mapItem->model->book_id));
 | 
			
		||||
            $bookIdsInvolved->push(intval($mapItem->book));
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Get the books involved in the sort
 | 
			
		||||
        $bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
 | 
			
		||||
        $booksInvolved = $this->bookRepo->getManyById('book', $bookIdsInvolved, false, true);
 | 
			
		||||
 | 
			
		||||
        // Throw permission error if invalid ids or inaccessible books given.
 | 
			
		||||
        if (count($bookIdsInvolved) !== count($booksInvolved)) {
 | 
			
		||||
            $this->showPermissionError();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check permissions of involved books
 | 
			
		||||
        $booksInvolved->each(function (Book $book) {
 | 
			
		||||
             $this->checkOwnablePermission('book-update', $book);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Perform the sort
 | 
			
		||||
        $sortMap->each(function ($mapItem) {
 | 
			
		||||
            $model = $mapItem->model;
 | 
			
		||||
 | 
			
		||||
            $priorityChanged = intval($model->priority) !== intval($mapItem->sort);
 | 
			
		||||
            $bookChanged = intval($model->book_id) !== intval($mapItem->book);
 | 
			
		||||
            $chapterChanged = ($mapItem->type === 'page') && intval($model->chapter_id) !== $mapItem->parentChapter;
 | 
			
		||||
 | 
			
		||||
            if ($bookChanged) {
 | 
			
		||||
                $this->bookRepo->changeBook($model, $mapItem->book);
 | 
			
		||||
            }
 | 
			
		||||
            if ($chapterChanged) {
 | 
			
		||||
                $model->chapter_id = intval($mapItem->parentChapter);
 | 
			
		||||
                $model->save();
 | 
			
		||||
            }
 | 
			
		||||
            if ($priorityChanged) {
 | 
			
		||||
                $model->priority = intval($mapItem->sort);
 | 
			
		||||
                $model->save();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Rebuild permissions and add activity for involved books.
 | 
			
		||||
        $booksInvolved->each(function (Book $book) {
 | 
			
		||||
            $book->rebuildPermissions();
 | 
			
		||||
            Activity::add($book, 'book_sort', $book->id);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return redirect($book->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove the specified book from storage.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     * Remove the specified book from the system.
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     * @throws NotifyException
 | 
			
		||||
     */
 | 
			
		||||
| 
						 | 
				
			
			@ -338,72 +185,40 @@ class BookController extends Controller
 | 
			
		|||
    {
 | 
			
		||||
        $book = $this->bookRepo->getBySlug($bookSlug);
 | 
			
		||||
        $this->checkOwnablePermission('book-delete', $book);
 | 
			
		||||
        Activity::addMessage('book_delete', $book->name);
 | 
			
		||||
 | 
			
		||||
        if ($book->cover) {
 | 
			
		||||
            $this->imageRepo->destroyImage($book->cover);
 | 
			
		||||
        }
 | 
			
		||||
        $this->bookRepo->destroyBook($book);
 | 
			
		||||
        Activity::addMessage('book_delete', $book->name);
 | 
			
		||||
        $this->bookRepo->destroy($book);
 | 
			
		||||
 | 
			
		||||
        return redirect('/books');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the Restrictions view.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @return Factory|View
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     * Show the permissions view.
 | 
			
		||||
     */
 | 
			
		||||
    public function showPermissions(string $bookSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $book = $this->bookRepo->getBySlug($bookSlug);
 | 
			
		||||
        $this->checkOwnablePermission('restrictions-manage', $book);
 | 
			
		||||
        $roles = $this->userRepo->getRestrictableRoles();
 | 
			
		||||
 | 
			
		||||
        return view('books.permissions', [
 | 
			
		||||
            'book' => $book,
 | 
			
		||||
            'roles' => $roles
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set the restrictions for this book.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @return RedirectResponse|Redirector
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function permissions(Request $request, string $bookSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $book = $this->bookRepo->getBySlug($bookSlug);
 | 
			
		||||
        $this->checkOwnablePermission('restrictions-manage', $book);
 | 
			
		||||
        $this->bookRepo->updateEntityPermissionsFromRequest($request, $book);
 | 
			
		||||
 | 
			
		||||
        $restricted = $request->get('restricted') === 'true';
 | 
			
		||||
        $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
 | 
			
		||||
        $this->bookRepo->updatePermissions($book, $restricted, $permissions);
 | 
			
		||||
 | 
			
		||||
        $this->showSuccessNotification(trans('entities.books_permissions_updated'));
 | 
			
		||||
        return redirect($book->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Common actions to run on book update.
 | 
			
		||||
     * Handles updating the cover image.
 | 
			
		||||
     * @param Book $book
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @throws ImageUploadException
 | 
			
		||||
     */
 | 
			
		||||
    protected function bookUpdateActions(Book $book, Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        // Update the cover image if in request
 | 
			
		||||
        if ($request->has('image')) {
 | 
			
		||||
            $this->imageRepo->destroyImage($book->cover);
 | 
			
		||||
            $newImage = $request->file('image');
 | 
			
		||||
            $image = $this->imageRepo->saveNew($newImage, 'cover_book', $book->id, 512, 512, true);
 | 
			
		||||
            $book->image_id = $image->id;
 | 
			
		||||
            $book->save();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($request->has('image_reset')) {
 | 
			
		||||
            $this->imageRepo->destroyImage($book->cover);
 | 
			
		||||
            $book->image_id = 0;
 | 
			
		||||
            $book->save();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,25 +4,16 @@ namespace BookStack\Http\Controllers;
 | 
			
		|||
 | 
			
		||||
use BookStack\Entities\ExportService;
 | 
			
		||||
use BookStack\Entities\Repos\BookRepo;
 | 
			
		||||
use BookStack\Exceptions\NotFoundException;
 | 
			
		||||
use Throwable;
 | 
			
		||||
 | 
			
		||||
class BookExportController extends Controller
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @var BookRepo
 | 
			
		||||
     */
 | 
			
		||||
    protected $bookRepo;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var ExportService
 | 
			
		||||
     */
 | 
			
		||||
    protected $bookRepo;
 | 
			
		||||
    protected $exportService;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * BookExportController constructor.
 | 
			
		||||
     * @param BookRepo $bookRepo
 | 
			
		||||
     * @param ExportService $exportService
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(BookRepo $bookRepo, ExportService $exportService)
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -33,9 +24,6 @@ class BookExportController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Export a book as a PDF file.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function pdf(string $bookSlug)
 | 
			
		||||
| 
						 | 
				
			
			@ -47,9 +35,6 @@ class BookExportController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Export a book as a contained HTML file.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function html(string $bookSlug)
 | 
			
		||||
| 
						 | 
				
			
			@ -61,9 +46,6 @@ class BookExportController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Export a book as a plain text file.
 | 
			
		||||
     * @param $bookSlug
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function plainText(string $bookSlug)
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,82 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace BookStack\Http\Controllers;
 | 
			
		||||
 | 
			
		||||
use BookStack\Entities\Book;
 | 
			
		||||
use BookStack\Entities\Managers\BookContents;
 | 
			
		||||
use BookStack\Entities\Repos\BookRepo;
 | 
			
		||||
use BookStack\Exceptions\SortOperationException;
 | 
			
		||||
use BookStack\Facades\Activity;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
 | 
			
		||||
class BookSortController extends Controller
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    protected $bookRepo;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * BookSortController constructor.
 | 
			
		||||
     * @param $bookRepo
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(BookRepo $bookRepo)
 | 
			
		||||
    {
 | 
			
		||||
        $this->bookRepo = $bookRepo;
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Shows the view which allows pages to be re-ordered and sorted.
 | 
			
		||||
     */
 | 
			
		||||
    public function show(string $bookSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $book = $this->bookRepo->getBySlug($bookSlug);
 | 
			
		||||
        $this->checkOwnablePermission('book-update', $book);
 | 
			
		||||
 | 
			
		||||
        $bookChildren = (new BookContents($book))->getTree(false);
 | 
			
		||||
 | 
			
		||||
        $this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
 | 
			
		||||
        return view('books.sort', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Shows the sort box for a single book.
 | 
			
		||||
     * Used via AJAX when loading in extra books to a sort.
 | 
			
		||||
     */
 | 
			
		||||
    public function showItem(string $bookSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $book = $this->bookRepo->getBySlug($bookSlug);
 | 
			
		||||
        $bookChildren = (new BookContents($book))->getTree();
 | 
			
		||||
        return view('books.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sorts a book using a given mapping array.
 | 
			
		||||
     */
 | 
			
		||||
    public function update(Request $request, string $bookSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $book = $this->bookRepo->getBySlug($bookSlug);
 | 
			
		||||
        $this->checkOwnablePermission('book-update', $book);
 | 
			
		||||
 | 
			
		||||
        // Return if no map sent
 | 
			
		||||
        if (!$request->filled('sort-tree')) {
 | 
			
		||||
            return redirect($book->getUrl());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $sortMap = collect(json_decode($request->get('sort-tree')));
 | 
			
		||||
        $bookContents = new BookContents($book);
 | 
			
		||||
        $booksInvolved = collect();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            $booksInvolved = $bookContents->sortUsingMap($sortMap);
 | 
			
		||||
        } catch (SortOperationException $exception) {
 | 
			
		||||
            $this->showPermissionError();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Rebuild permissions and add activity for involved books.
 | 
			
		||||
        $booksInvolved->each(function (Book $book) {
 | 
			
		||||
            Activity::add($book, 'book_sort', $book->id);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return redirect($book->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,34 +1,30 @@
 | 
			
		|||
<?php namespace BookStack\Http\Controllers;
 | 
			
		||||
 | 
			
		||||
use Activity;
 | 
			
		||||
use BookStack\Auth\UserRepo;
 | 
			
		||||
use BookStack\Entities\Bookshelf;
 | 
			
		||||
use BookStack\Entities\EntityContextManager;
 | 
			
		||||
use BookStack\Entities\Repos\EntityRepo;
 | 
			
		||||
use BookStack\Entities\Book;
 | 
			
		||||
use BookStack\Entities\Managers\EntityContext;
 | 
			
		||||
use BookStack\Entities\Repos\BookshelfRepo;
 | 
			
		||||
use BookStack\Exceptions\ImageUploadException;
 | 
			
		||||
use BookStack\Exceptions\NotFoundException;
 | 
			
		||||
use BookStack\Uploads\ImageRepo;
 | 
			
		||||
use Exception;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
use Illuminate\Http\Response;
 | 
			
		||||
use Illuminate\Validation\ValidationException;
 | 
			
		||||
use Views;
 | 
			
		||||
 | 
			
		||||
class BookshelfController extends Controller
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    protected $entityRepo;
 | 
			
		||||
    protected $userRepo;
 | 
			
		||||
    protected $bookshelfRepo;
 | 
			
		||||
    protected $entityContextManager;
 | 
			
		||||
    protected $imageRepo;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * BookController constructor.
 | 
			
		||||
     * @param EntityRepo $entityRepo
 | 
			
		||||
     * @param UserRepo $userRepo
 | 
			
		||||
     * @param EntityContextManager $entityContextManager
 | 
			
		||||
     * @param ImageRepo $imageRepo
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, EntityContextManager $entityContextManager, ImageRepo $imageRepo)
 | 
			
		||||
    public function __construct(BookshelfRepo $bookshelfRepo, EntityContext $entityContextManager, ImageRepo $imageRepo)
 | 
			
		||||
    {
 | 
			
		||||
        $this->entityRepo = $entityRepo;
 | 
			
		||||
        $this->userRepo = $userRepo;
 | 
			
		||||
        $this->bookshelfRepo = $bookshelfRepo;
 | 
			
		||||
        $this->entityContextManager = $entityContextManager;
 | 
			
		||||
        $this->imageRepo = $imageRepo;
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
| 
						 | 
				
			
			@ -36,7 +32,6 @@ class BookshelfController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Display a listing of the book.
 | 
			
		||||
     * @return Response
 | 
			
		||||
     */
 | 
			
		||||
    public function index()
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -49,14 +44,10 @@ class BookshelfController extends Controller
 | 
			
		|||
            'updated_at' => trans('common.sort_updated_at'),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        $shelves = $this->entityRepo->getAllPaginated('bookshelf', 18, $sort, $order);
 | 
			
		||||
        foreach ($shelves as $shelf) {
 | 
			
		||||
            $shelf->books = $this->entityRepo->getBookshelfChildren($shelf);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $recents = $this->isSignedIn() ? $this->entityRepo->getRecentlyViewed('bookshelf', 4, 0) : false;
 | 
			
		||||
        $popular = $this->entityRepo->getPopular('bookshelf', 4, 0);
 | 
			
		||||
        $new = $this->entityRepo->getRecentlyCreated('bookshelf', 4, 0);
 | 
			
		||||
        $shelves = $this->bookshelfRepo->getAllPaginated(18, $sort, $order);
 | 
			
		||||
        $recents = $this->isSignedIn() ? $this->bookshelfRepo->getRecentlyViewed(4) : false;
 | 
			
		||||
        $popular = $this->bookshelfRepo->getPopular(4);
 | 
			
		||||
        $new = $this->bookshelfRepo->getRecentlyCreated(4);
 | 
			
		||||
 | 
			
		||||
        $this->entityContextManager->clearShelfContext();
 | 
			
		||||
        $this->setPageTitle(trans('entities.shelves'));
 | 
			
		||||
| 
						 | 
				
			
			@ -74,21 +65,19 @@ class BookshelfController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the form for creating a new bookshelf.
 | 
			
		||||
     * @return Response
 | 
			
		||||
     */
 | 
			
		||||
    public function create()
 | 
			
		||||
    {
 | 
			
		||||
        $this->checkPermission('bookshelf-create-all');
 | 
			
		||||
        $books = $this->entityRepo->getAll('book', false, 'update');
 | 
			
		||||
        $books = Book::hasPermission('update')->get();
 | 
			
		||||
        $this->setPageTitle(trans('entities.shelves_create'));
 | 
			
		||||
        return view('shelves.create', ['books' => $books]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Store a newly created bookshelf in storage.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @throws \BookStack\Exceptions\ImageUploadException
 | 
			
		||||
     * @throws ValidationException
 | 
			
		||||
     * @throws ImageUploadException
 | 
			
		||||
     */
 | 
			
		||||
    public function store(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -96,80 +85,63 @@ class BookshelfController extends Controller
 | 
			
		|||
        $this->validate($request, [
 | 
			
		||||
            'name' => 'required|string|max:255',
 | 
			
		||||
            'description' => 'string|max:1000',
 | 
			
		||||
            'image' => $this->imageRepo->getImageValidationRules(),
 | 
			
		||||
            'image' => $this->getImageValidationRules(),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $shelf = $this->entityRepo->createFromInput('bookshelf', $request->all());
 | 
			
		||||
        $this->shelfUpdateActions($shelf, $request);
 | 
			
		||||
        $bookIds = explode(',', $request->get('books', ''));
 | 
			
		||||
        $shelf = $this->bookshelfRepo->create($request->all(), $bookIds);
 | 
			
		||||
        $this->bookshelfRepo->updateCoverImage($shelf);
 | 
			
		||||
 | 
			
		||||
        Activity::add($shelf, 'bookshelf_create');
 | 
			
		||||
        return redirect($shelf->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Display the specified bookshelf.
 | 
			
		||||
     * @param String $slug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @throws \BookStack\Exceptions\NotFoundException
 | 
			
		||||
     * Display the bookshelf of the given slug.
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function show(string $slug)
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Bookshelf $shelf */
 | 
			
		||||
        $shelf = $this->entityRepo->getEntityBySlug('bookshelf', $slug);
 | 
			
		||||
        $shelf = $this->bookshelfRepo->getBySlug($slug);
 | 
			
		||||
        $this->checkOwnablePermission('book-view', $shelf);
 | 
			
		||||
 | 
			
		||||
        $books = $this->entityRepo->getBookshelfChildren($shelf);
 | 
			
		||||
        Views::add($shelf);
 | 
			
		||||
        $this->entityContextManager->setShelfContext($shelf->id);
 | 
			
		||||
 | 
			
		||||
        $this->setPageTitle($shelf->getShortName());
 | 
			
		||||
 | 
			
		||||
        return view('shelves.show', [
 | 
			
		||||
            'shelf' => $shelf,
 | 
			
		||||
            'books' => $books,
 | 
			
		||||
            'activity' => Activity::entityActivity($shelf, 20, 1)
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the form for editing the specified bookshelf.
 | 
			
		||||
     * @param $slug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @throws \BookStack\Exceptions\NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function edit(string $slug)
 | 
			
		||||
    {
 | 
			
		||||
        $shelf = $this->entityRepo->getEntityBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
 | 
			
		||||
        $shelf = $this->bookshelfRepo->getBySlug($slug);
 | 
			
		||||
        $this->checkOwnablePermission('bookshelf-update', $shelf);
 | 
			
		||||
 | 
			
		||||
        $shelfBooks = $this->entityRepo->getBookshelfChildren($shelf);
 | 
			
		||||
        $shelfBookIds = $shelfBooks->pluck('id');
 | 
			
		||||
        $books = $this->entityRepo->getAll('book', false, 'update');
 | 
			
		||||
        $books = $books->filter(function ($book) use ($shelfBookIds) {
 | 
			
		||||
             return !$shelfBookIds->contains($book->id);
 | 
			
		||||
        });
 | 
			
		||||
        $shelfBookIds = $shelf->books()->get(['id'])->pluck('id');
 | 
			
		||||
        $books = Book::hasPermission('update')->whereNotIn('id', $shelfBookIds)->get();
 | 
			
		||||
 | 
			
		||||
        $this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $shelf->getShortName()]));
 | 
			
		||||
        return view('shelves.edit', [
 | 
			
		||||
            'shelf' => $shelf,
 | 
			
		||||
            'books' => $books,
 | 
			
		||||
            'shelfBooks' => $shelfBooks,
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the specified bookshelf in storage.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @param string $slug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @throws \BookStack\Exceptions\NotFoundException
 | 
			
		||||
     * @throws \BookStack\Exceptions\ImageUploadException
 | 
			
		||||
     * @throws ValidationException
 | 
			
		||||
     * @throws ImageUploadException
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function update(Request $request, string $slug)
 | 
			
		||||
    {
 | 
			
		||||
        $shelf = $this->entityRepo->getEntityBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
 | 
			
		||||
        $shelf = $this->bookshelfRepo->getBySlug($slug);
 | 
			
		||||
        $this->checkOwnablePermission('bookshelf-update', $shelf);
 | 
			
		||||
        $this->validate($request, [
 | 
			
		||||
            'name' => 'required|string|max:255',
 | 
			
		||||
| 
						 | 
				
			
			@ -177,24 +149,22 @@ class BookshelfController extends Controller
 | 
			
		|||
            'image' => $this->imageRepo->getImageValidationRules(),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
         $shelf = $this->entityRepo->updateFromInput($shelf, $request->all());
 | 
			
		||||
         $this->shelfUpdateActions($shelf, $request);
 | 
			
		||||
 | 
			
		||||
         Activity::add($shelf, 'bookshelf_update');
 | 
			
		||||
        $bookIds = explode(',', $request->get('books', ''));
 | 
			
		||||
        $shelf = $this->bookshelfRepo->update($shelf, $request->all(), $bookIds);
 | 
			
		||||
        $resetCover = $request->has('image_reset');
 | 
			
		||||
        $this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null), $resetCover);
 | 
			
		||||
        Activity::add($shelf, 'bookshelf_update');
 | 
			
		||||
 | 
			
		||||
         return redirect($shelf->getUrl());
 | 
			
		||||
        return redirect($shelf->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Shows the page to confirm deletion
 | 
			
		||||
     * @param $slug
 | 
			
		||||
     * @return \Illuminate\View\View
 | 
			
		||||
     * @throws \BookStack\Exceptions\NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function showDelete(string $slug)
 | 
			
		||||
    {
 | 
			
		||||
        $shelf = $this->entityRepo->getEntityBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
 | 
			
		||||
        $shelf = $this->bookshelfRepo->getBySlug($slug);
 | 
			
		||||
        $this->checkOwnablePermission('bookshelf-delete', $shelf);
 | 
			
		||||
 | 
			
		||||
        $this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()]));
 | 
			
		||||
| 
						 | 
				
			
			@ -203,101 +173,58 @@ class BookshelfController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove the specified bookshelf from storage.
 | 
			
		||||
     * @param string $slug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @throws \BookStack\Exceptions\NotFoundException
 | 
			
		||||
     * @throws \Throwable
 | 
			
		||||
     * @throws Exception
 | 
			
		||||
     */
 | 
			
		||||
    public function destroy(string $slug)
 | 
			
		||||
    {
 | 
			
		||||
        $shelf = $this->entityRepo->getEntityBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
 | 
			
		||||
        $shelf = $this->bookshelfRepo->getBySlug($slug);
 | 
			
		||||
        $this->checkOwnablePermission('bookshelf-delete', $shelf);
 | 
			
		||||
        Activity::addMessage('bookshelf_delete', $shelf->name);
 | 
			
		||||
 | 
			
		||||
        if ($shelf->cover) {
 | 
			
		||||
            $this->imageRepo->destroyImage($shelf->cover);
 | 
			
		||||
        }
 | 
			
		||||
        $this->entityRepo->destroyBookshelf($shelf);
 | 
			
		||||
        Activity::addMessage('bookshelf_delete', $shelf->name);
 | 
			
		||||
        $this->bookshelfRepo->destroy($shelf);
 | 
			
		||||
 | 
			
		||||
        return redirect('/shelves');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the permissions view.
 | 
			
		||||
     * @param string $slug
 | 
			
		||||
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
 | 
			
		||||
     * @throws \BookStack\Exceptions\NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function showPermissions(string $slug)
 | 
			
		||||
    {
 | 
			
		||||
        $shelf = $this->entityRepo->getEntityBySlug('bookshelf', $slug);
 | 
			
		||||
        $shelf = $this->bookshelfRepo->getBySlug($slug);
 | 
			
		||||
        $this->checkOwnablePermission('restrictions-manage', $shelf);
 | 
			
		||||
 | 
			
		||||
        $roles = $this->userRepo->getRestrictableRoles();
 | 
			
		||||
        return view('shelves.permissions', [
 | 
			
		||||
            'shelf' => $shelf,
 | 
			
		||||
            'roles' => $roles
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set the permissions for this bookshelf.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @param string $slug
 | 
			
		||||
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
 | 
			
		||||
     * @throws \BookStack\Exceptions\NotFoundException
 | 
			
		||||
     * @throws \Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function permissions(Request $request, string $slug)
 | 
			
		||||
    {
 | 
			
		||||
        $shelf = $this->entityRepo->getEntityBySlug('bookshelf', $slug);
 | 
			
		||||
        $shelf = $this->bookshelfRepo->getBySlug($slug);
 | 
			
		||||
        $this->checkOwnablePermission('restrictions-manage', $shelf);
 | 
			
		||||
 | 
			
		||||
        $this->entityRepo->updateEntityPermissionsFromRequest($request, $shelf);
 | 
			
		||||
        $this->showSuccessNotification( trans('entities.shelves_permissions_updated'));
 | 
			
		||||
        $restricted = $request->get('restricted') === 'true';
 | 
			
		||||
        $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
 | 
			
		||||
        $this->bookshelfRepo->updatePermissions($shelf, $restricted, $permissions);
 | 
			
		||||
 | 
			
		||||
        $this->showSuccessNotification(trans('entities.shelves_permissions_updated'));
 | 
			
		||||
        return redirect($shelf->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Copy the permissions of a bookshelf to the child books.
 | 
			
		||||
     * @param string $slug
 | 
			
		||||
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
 | 
			
		||||
     * @throws \BookStack\Exceptions\NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function copyPermissions(string $slug)
 | 
			
		||||
    {
 | 
			
		||||
        $shelf = $this->entityRepo->getEntityBySlug('bookshelf', $slug);
 | 
			
		||||
        $shelf = $this->bookshelfRepo->getBySlug($slug);
 | 
			
		||||
        $this->checkOwnablePermission('restrictions-manage', $shelf);
 | 
			
		||||
 | 
			
		||||
        $updateCount = $this->entityRepo->copyBookshelfPermissions($shelf);
 | 
			
		||||
        $this->showSuccessNotification( trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
 | 
			
		||||
        $updateCount = $this->bookshelfRepo->copyDownPermissions($shelf);
 | 
			
		||||
        $this->showSuccessNotification(trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
 | 
			
		||||
        return redirect($shelf->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Common actions to run on bookshelf update.
 | 
			
		||||
     * @param Bookshelf $shelf
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @throws \BookStack\Exceptions\ImageUploadException
 | 
			
		||||
     */
 | 
			
		||||
    protected function shelfUpdateActions(Bookshelf $shelf, Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        // Update the books that the shelf references
 | 
			
		||||
        $this->entityRepo->updateShelfBooks($shelf, $request->get('books', ''));
 | 
			
		||||
 | 
			
		||||
        // Update the cover image if in request
 | 
			
		||||
        if ($request->has('image')) {
 | 
			
		||||
            $newImage = $request->file('image');
 | 
			
		||||
            $this->imageRepo->destroyImage($shelf->cover);
 | 
			
		||||
            $image = $this->imageRepo->saveNew($newImage, 'cover_shelf', $shelf->id, 512, 512, true);
 | 
			
		||||
            $shelf->image_id = $image->id;
 | 
			
		||||
            $shelf->save();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($request->has('image_reset')) {
 | 
			
		||||
            $this->imageRepo->destroyImage($shelf->cover);
 | 
			
		||||
            $shelf->image_id = 0;
 | 
			
		||||
            $shelf->save();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,50 +1,45 @@
 | 
			
		|||
<?php namespace BookStack\Http\Controllers;
 | 
			
		||||
 | 
			
		||||
use Activity;
 | 
			
		||||
use BookStack\Auth\UserRepo;
 | 
			
		||||
use BookStack\Entities\Repos\EntityRepo;
 | 
			
		||||
use BookStack\Entities\Book;
 | 
			
		||||
use BookStack\Entities\Managers\BookContents;
 | 
			
		||||
use BookStack\Entities\Repos\ChapterRepo;
 | 
			
		||||
use BookStack\Exceptions\MoveOperationException;
 | 
			
		||||
use BookStack\Exceptions\NotFoundException;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
use Illuminate\Http\Response;
 | 
			
		||||
use Illuminate\Validation\ValidationException;
 | 
			
		||||
use Throwable;
 | 
			
		||||
use Views;
 | 
			
		||||
 | 
			
		||||
class ChapterController extends Controller
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    protected $userRepo;
 | 
			
		||||
    protected $entityRepo;
 | 
			
		||||
    protected $chapterRepo;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * ChapterController constructor.
 | 
			
		||||
     * @param EntityRepo $entityRepo
 | 
			
		||||
     * @param UserRepo $userRepo
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(EntityRepo $entityRepo, UserRepo $userRepo)
 | 
			
		||||
    public function __construct(ChapterRepo $chapterRepo)
 | 
			
		||||
    {
 | 
			
		||||
        $this->entityRepo = $entityRepo;
 | 
			
		||||
        $this->userRepo = $userRepo;
 | 
			
		||||
        $this->chapterRepo = $chapterRepo;
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the form for creating a new chapter.
 | 
			
		||||
     * @param $bookSlug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     */
 | 
			
		||||
    public function create($bookSlug)
 | 
			
		||||
    public function create(string $bookSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $book = $this->entityRepo->getEntityBySlug('book', $bookSlug);
 | 
			
		||||
        $book = Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
 | 
			
		||||
        $this->checkOwnablePermission('chapter-create', $book);
 | 
			
		||||
 | 
			
		||||
        $this->setPageTitle(trans('entities.chapters_create'));
 | 
			
		||||
        return view('chapters.create', ['book' => $book, 'current' => $book]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Store a newly created chapter in storage.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @throws \BookStack\Exceptions\NotFoundException
 | 
			
		||||
     * @throws \Illuminate\Validation\ValidationException
 | 
			
		||||
     * @throws ValidationException
 | 
			
		||||
     */
 | 
			
		||||
    public function store(Request $request, string $bookSlug)
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -52,30 +47,28 @@ class ChapterController extends Controller
 | 
			
		|||
            'name' => 'required|string|max:255'
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $book = $this->entityRepo->getEntityBySlug('book', $bookSlug);
 | 
			
		||||
        $book = Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
 | 
			
		||||
        $this->checkOwnablePermission('chapter-create', $book);
 | 
			
		||||
 | 
			
		||||
        $input = $request->all();
 | 
			
		||||
        $input['priority'] = $this->entityRepo->getNewBookPriority($book);
 | 
			
		||||
        $chapter = $this->entityRepo->createFromInput('chapter', $input, $book);
 | 
			
		||||
        $chapter = $this->chapterRepo->create($request->all(), $book);
 | 
			
		||||
        Activity::add($chapter, 'chapter_create', $book->id);
 | 
			
		||||
 | 
			
		||||
        return redirect($chapter->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Display the specified chapter.
 | 
			
		||||
     * @param $bookSlug
 | 
			
		||||
     * @param $chapterSlug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     */
 | 
			
		||||
    public function show($bookSlug, $chapterSlug)
 | 
			
		||||
    public function show(string $bookSlug, string $chapterSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $chapter = $this->entityRepo->getEntityBySlug('chapter', $chapterSlug, $bookSlug);
 | 
			
		||||
        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
 | 
			
		||||
        $this->checkOwnablePermission('chapter-view', $chapter);
 | 
			
		||||
        $sidebarTree = $this->entityRepo->getBookChildren($chapter->book);
 | 
			
		||||
 | 
			
		||||
        $sidebarTree = (new BookContents($chapter->book))->getTree();
 | 
			
		||||
        $pages = $chapter->getVisiblePages();
 | 
			
		||||
        Views::add($chapter);
 | 
			
		||||
 | 
			
		||||
        $this->setPageTitle($chapter->getShortName());
 | 
			
		||||
        $pages = $this->entityRepo->getChapterChildren($chapter);
 | 
			
		||||
        return view('chapters.show', [
 | 
			
		||||
            'book' => $chapter->book,
 | 
			
		||||
            'chapter' => $chapter,
 | 
			
		||||
| 
						 | 
				
			
			@ -87,79 +80,71 @@ class ChapterController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the form for editing the specified chapter.
 | 
			
		||||
     * @param $bookSlug
 | 
			
		||||
     * @param $chapterSlug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     */
 | 
			
		||||
    public function edit($bookSlug, $chapterSlug)
 | 
			
		||||
    public function edit(string $bookSlug, string $chapterSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $chapter = $this->entityRepo->getEntityBySlug('chapter', $chapterSlug, $bookSlug);
 | 
			
		||||
        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
 | 
			
		||||
        $this->checkOwnablePermission('chapter-update', $chapter);
 | 
			
		||||
 | 
			
		||||
        $this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
 | 
			
		||||
        return view('chapters.edit', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the specified chapter in storage.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $chapterSlug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @throws \BookStack\Exceptions\NotFoundException
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function update(Request $request, string $bookSlug, string $chapterSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $chapter = $this->entityRepo->getEntityBySlug('chapter', $chapterSlug, $bookSlug);
 | 
			
		||||
        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
 | 
			
		||||
        $this->checkOwnablePermission('chapter-update', $chapter);
 | 
			
		||||
 | 
			
		||||
        $this->entityRepo->updateFromInput($chapter, $request->all());
 | 
			
		||||
        $this->chapterRepo->update($chapter, $request->all());
 | 
			
		||||
        Activity::add($chapter, 'chapter_update', $chapter->book->id);
 | 
			
		||||
 | 
			
		||||
        return redirect($chapter->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Shows the page to confirm deletion of this chapter.
 | 
			
		||||
     * @param $bookSlug
 | 
			
		||||
     * @param $chapterSlug
 | 
			
		||||
     * @return \Illuminate\View\View
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function showDelete($bookSlug, $chapterSlug)
 | 
			
		||||
    public function showDelete(string $bookSlug, string $chapterSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $chapter = $this->entityRepo->getEntityBySlug('chapter', $chapterSlug, $bookSlug);
 | 
			
		||||
        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
 | 
			
		||||
        $this->checkOwnablePermission('chapter-delete', $chapter);
 | 
			
		||||
 | 
			
		||||
        $this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
 | 
			
		||||
        return view('chapters.delete', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove the specified chapter from storage.
 | 
			
		||||
     * @param $bookSlug
 | 
			
		||||
     * @param $chapterSlug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function destroy($bookSlug, $chapterSlug)
 | 
			
		||||
    public function destroy(string $bookSlug, string $chapterSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $chapter = $this->entityRepo->getEntityBySlug('chapter', $chapterSlug, $bookSlug);
 | 
			
		||||
        $book = $chapter->book;
 | 
			
		||||
        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
 | 
			
		||||
        $this->checkOwnablePermission('chapter-delete', $chapter);
 | 
			
		||||
        Activity::addMessage('chapter_delete', $chapter->name, $book->id);
 | 
			
		||||
        $this->entityRepo->destroyChapter($chapter);
 | 
			
		||||
        return redirect($book->getUrl());
 | 
			
		||||
 | 
			
		||||
        Activity::addMessage('chapter_delete', $chapter->name, $chapter->book->id);
 | 
			
		||||
        $this->chapterRepo->destroy($chapter);
 | 
			
		||||
 | 
			
		||||
        return redirect($chapter->book->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the page for moving a chapter.
 | 
			
		||||
     * @param $bookSlug
 | 
			
		||||
     * @param $chapterSlug
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     * @throws \BookStack\Exceptions\NotFoundException
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function showMove($bookSlug, $chapterSlug)
 | 
			
		||||
    public function showMove(string $bookSlug, string $chapterSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $chapter = $this->entityRepo->getEntityBySlug('chapter', $chapterSlug, $bookSlug);
 | 
			
		||||
        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
 | 
			
		||||
        $this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
 | 
			
		||||
        $this->checkOwnablePermission('chapter-update', $chapter);
 | 
			
		||||
        $this->checkOwnablePermission('chapter-delete', $chapter);
 | 
			
		||||
 | 
			
		||||
        return view('chapters.move', [
 | 
			
		||||
            'chapter' => $chapter,
 | 
			
		||||
            'book' => $chapter->book
 | 
			
		||||
| 
						 | 
				
			
			@ -168,15 +153,11 @@ class ChapterController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Perform the move action for a chapter.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $chapterSlug
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     * @throws \BookStack\Exceptions\NotFoundException
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function move(Request $request, string $bookSlug, string $chapterSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $chapter = $this->entityRepo->getEntityBySlug('chapter', $chapterSlug, $bookSlug);
 | 
			
		||||
        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
 | 
			
		||||
        $this->checkOwnablePermission('chapter-update', $chapter);
 | 
			
		||||
        $this->checkOwnablePermission('chapter-delete', $chapter);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -185,63 +166,47 @@ class ChapterController extends Controller
 | 
			
		|||
            return redirect($chapter->getUrl());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $stringExploded = explode(':', $entitySelection);
 | 
			
		||||
        $entityType = $stringExploded[0];
 | 
			
		||||
        $entityId = intval($stringExploded[1]);
 | 
			
		||||
 | 
			
		||||
        $parent = false;
 | 
			
		||||
 | 
			
		||||
        if ($entityType == 'book') {
 | 
			
		||||
            $parent = $this->entityRepo->getById('book', $entityId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($parent === false || $parent === null) {
 | 
			
		||||
            $this->showErrorNotification( trans('errors.selected_book_not_found'));
 | 
			
		||||
        try {
 | 
			
		||||
            $newBook = $this->chapterRepo->move($chapter, $entitySelection);
 | 
			
		||||
        } catch (MoveOperationException $exception) {
 | 
			
		||||
            $this->showErrorNotification(trans('errors.selected_book_not_found'));
 | 
			
		||||
            return redirect()->back();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->entityRepo->changeBook($chapter, $parent->id);
 | 
			
		||||
        $chapter->rebuildPermissions();
 | 
			
		||||
 | 
			
		||||
        Activity::add($chapter, 'chapter_move', $chapter->book->id);
 | 
			
		||||
        $this->showSuccessNotification( trans('entities.chapter_move_success', ['bookName' => $parent->name]));
 | 
			
		||||
        Activity::add($chapter, 'chapter_move', $newBook->id);
 | 
			
		||||
 | 
			
		||||
        $this->showSuccessNotification(trans('entities.chapter_move_success', ['bookName' => $newBook->name]));
 | 
			
		||||
        return redirect($chapter->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the Restrictions view.
 | 
			
		||||
     * @param $bookSlug
 | 
			
		||||
     * @param $chapterSlug
 | 
			
		||||
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
 | 
			
		||||
     * @throws \BookStack\Exceptions\NotFoundException
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function showPermissions($bookSlug, $chapterSlug)
 | 
			
		||||
    public function showPermissions(string $bookSlug, string $chapterSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $chapter = $this->entityRepo->getEntityBySlug('chapter', $chapterSlug, $bookSlug);
 | 
			
		||||
        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
 | 
			
		||||
        $this->checkOwnablePermission('restrictions-manage', $chapter);
 | 
			
		||||
        $roles = $this->userRepo->getRestrictableRoles();
 | 
			
		||||
 | 
			
		||||
        return view('chapters.permissions', [
 | 
			
		||||
            'chapter' => $chapter,
 | 
			
		||||
            'roles' => $roles
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set the restrictions for this chapter.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $chapterSlug
 | 
			
		||||
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
 | 
			
		||||
     * @throws \BookStack\Exceptions\NotFoundException
 | 
			
		||||
     * @throws \Throwable
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function permissions(Request $request, string $bookSlug, string $chapterSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $chapter = $this->entityRepo->getEntityBySlug('chapter', $chapterSlug, $bookSlug);
 | 
			
		||||
        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
 | 
			
		||||
        $this->checkOwnablePermission('restrictions-manage', $chapter);
 | 
			
		||||
        $this->entityRepo->updateEntityPermissionsFromRequest($request, $chapter);
 | 
			
		||||
        $this->showSuccessNotification( trans('entities.chapters_permissions_success'));
 | 
			
		||||
 | 
			
		||||
        $restricted = $request->get('restricted') === 'true';
 | 
			
		||||
        $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
 | 
			
		||||
        $this->chapterRepo->updatePermissions($chapter, $restricted, $permissions);
 | 
			
		||||
 | 
			
		||||
        $this->showSuccessNotification(trans('entities.chapters_permissions_success'));
 | 
			
		||||
        return redirect($chapter->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,77 +1,57 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace BookStack\Http\Controllers;
 | 
			
		||||
<?php namespace BookStack\Http\Controllers;
 | 
			
		||||
 | 
			
		||||
use BookStack\Entities\ExportService;
 | 
			
		||||
use BookStack\Entities\Repos\EntityRepo;
 | 
			
		||||
use BookStack\Entities\Repos\ChapterRepo;
 | 
			
		||||
use BookStack\Exceptions\NotFoundException;
 | 
			
		||||
use Illuminate\Http\Response;
 | 
			
		||||
use Throwable;
 | 
			
		||||
 | 
			
		||||
class ChapterExportController extends Controller
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @var EntityRepo
 | 
			
		||||
     */
 | 
			
		||||
    protected $entityRepo;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var ExportService
 | 
			
		||||
     */
 | 
			
		||||
    protected $chapterRepo;
 | 
			
		||||
    protected $exportService;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * ChapterExportController constructor.
 | 
			
		||||
     * @param EntityRepo $entityRepo
 | 
			
		||||
     * @param ExportService $exportService
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(EntityRepo $entityRepo, ExportService $exportService)
 | 
			
		||||
    public function __construct(ChapterRepo $chapterRepo, ExportService $exportService)
 | 
			
		||||
    {
 | 
			
		||||
        $this->entityRepo = $entityRepo;
 | 
			
		||||
        $this->chapterRepo = $chapterRepo;
 | 
			
		||||
        $this->exportService = $exportService;
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Exports a chapter to pdf .
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $chapterSlug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * Exports a chapter to pdf.
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function pdf(string $bookSlug, string $chapterSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $chapter = $this->entityRepo->getEntityBySlug('chapter', $chapterSlug, $bookSlug);
 | 
			
		||||
        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
 | 
			
		||||
        $pdfContent = $this->exportService->chapterToPdf($chapter);
 | 
			
		||||
        return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Export a chapter to a self-contained HTML file.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $chapterSlug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function html(string $bookSlug, string $chapterSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $chapter = $this->entityRepo->getEntityBySlug('chapter', $chapterSlug, $bookSlug);
 | 
			
		||||
        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
 | 
			
		||||
        $containedHtml = $this->exportService->chapterToContainedHtml($chapter);
 | 
			
		||||
        return $this->downloadResponse($containedHtml, $chapterSlug . '.html');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Export a chapter to a simple plaintext .txt file.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $chapterSlug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function plainText(string $bookSlug, string $chapterSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $chapter = $this->entityRepo->getEntityBySlug('chapter', $chapterSlug, $bookSlug);
 | 
			
		||||
        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
 | 
			
		||||
        $chapterText = $this->exportService->chapterToPlainText($chapter);
 | 
			
		||||
        return $this->downloadResponse($chapterText, $chapterSlug . '.txt');
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,44 +2,36 @@
 | 
			
		|||
 | 
			
		||||
use Activity;
 | 
			
		||||
use BookStack\Actions\CommentRepo;
 | 
			
		||||
use BookStack\Entities\Repos\EntityRepo;
 | 
			
		||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
 | 
			
		||||
use BookStack\Entities\Page;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
use Illuminate\Validation\ValidationException;
 | 
			
		||||
 | 
			
		||||
class CommentController extends Controller
 | 
			
		||||
{
 | 
			
		||||
    protected $entityRepo;
 | 
			
		||||
    protected $commentRepo;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * CommentController constructor.
 | 
			
		||||
     * @param \BookStack\Entities\Repos\EntityRepo $entityRepo
 | 
			
		||||
     * @param \BookStack\Actions\CommentRepo $commentRepo
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(EntityRepo $entityRepo, CommentRepo $commentRepo)
 | 
			
		||||
    public function __construct(CommentRepo $commentRepo)
 | 
			
		||||
    {
 | 
			
		||||
        $this->entityRepo = $entityRepo;
 | 
			
		||||
        $this->commentRepo = $commentRepo;
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Save a new comment for a Page
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @param integer $pageId
 | 
			
		||||
     * @param null|integer $commentId
 | 
			
		||||
     * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
 | 
			
		||||
     * @throws ValidationException
 | 
			
		||||
     */
 | 
			
		||||
    public function savePageComment(Request $request, $pageId, $commentId = null)
 | 
			
		||||
    public function savePageComment(Request $request, int $pageId, int $commentId = null)
 | 
			
		||||
    {
 | 
			
		||||
        $this->validate($request, [
 | 
			
		||||
            'text' => 'required|string',
 | 
			
		||||
            'html' => 'required|string',
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            $page = $this->entityRepo->getById('page', $pageId, true);
 | 
			
		||||
        } catch (ModelNotFoundException $e) {
 | 
			
		||||
        $page = Page::visible()->find($pageId);
 | 
			
		||||
        if ($page === null) {
 | 
			
		||||
            return response('Not found', 404);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -59,11 +51,9 @@ class CommentController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update an existing comment.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @param integer $commentId
 | 
			
		||||
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
 | 
			
		||||
     * @throws ValidationException
 | 
			
		||||
     */
 | 
			
		||||
    public function update(Request $request, $commentId)
 | 
			
		||||
    public function update(Request $request, int $commentId)
 | 
			
		||||
    {
 | 
			
		||||
        $this->validate($request, [
 | 
			
		||||
            'text' => 'required|string',
 | 
			
		||||
| 
						 | 
				
			
			@ -80,13 +70,12 @@ class CommentController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete a comment from the system.
 | 
			
		||||
     * @param integer $id
 | 
			
		||||
     * @return \Illuminate\Http\JsonResponse
 | 
			
		||||
     */
 | 
			
		||||
    public function destroy($id)
 | 
			
		||||
    public function destroy(int $id)
 | 
			
		||||
    {
 | 
			
		||||
        $comment = $this->commentRepo->getById($id);
 | 
			
		||||
        $this->checkOwnablePermission('comment-delete', $comment);
 | 
			
		||||
 | 
			
		||||
        $this->commentRepo->delete($comment);
 | 
			
		||||
        return response()->json(['message' => trans('entities.comment_deleted')]);
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -59,7 +59,7 @@ abstract class Controller extends BaseController
 | 
			
		|||
            $response = response()->json(['error' => trans('errors.permissionJson')], 403);
 | 
			
		||||
        } else {
 | 
			
		||||
            $response = redirect('/');
 | 
			
		||||
            $this->showErrorNotification( trans('errors.permission'));
 | 
			
		||||
            $this->showErrorNotification(trans('errors.permission'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new HttpResponseException($response);
 | 
			
		||||
| 
						 | 
				
			
			@ -129,7 +129,7 @@ abstract class Controller extends BaseController
 | 
			
		|||
     */
 | 
			
		||||
    protected function jsonError($messageText = "", $statusCode = 500)
 | 
			
		||||
    {
 | 
			
		||||
        return response()->json(['message' => $messageText], $statusCode);
 | 
			
		||||
        return response()->json(['message' => $messageText, 'status' => 'error'], $statusCode);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			@ -189,4 +189,12 @@ abstract class Controller extends BaseController
 | 
			
		|||
    {
 | 
			
		||||
        session()->flash('error', $message);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the validation rules for image files.
 | 
			
		||||
     */
 | 
			
		||||
    protected function getImageValidationRules(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'image_extension|no_double_extension|mimes:jpeg,png,gif,bmp,webp,tiff';
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,23 +1,16 @@
 | 
			
		|||
<?php namespace BookStack\Http\Controllers;
 | 
			
		||||
 | 
			
		||||
use Activity;
 | 
			
		||||
use BookStack\Entities\Repos\EntityRepo;
 | 
			
		||||
use BookStack\Entities\Book;
 | 
			
		||||
use BookStack\Entities\Managers\PageContent;
 | 
			
		||||
use BookStack\Entities\Page;
 | 
			
		||||
use BookStack\Entities\Repos\BookRepo;
 | 
			
		||||
use BookStack\Entities\Repos\BookshelfRepo;
 | 
			
		||||
use Illuminate\Http\Response;
 | 
			
		||||
use Views;
 | 
			
		||||
 | 
			
		||||
class HomeController extends Controller
 | 
			
		||||
{
 | 
			
		||||
    protected $entityRepo;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * HomeController constructor.
 | 
			
		||||
     * @param EntityRepo $entityRepo
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(EntityRepo $entityRepo)
 | 
			
		||||
    {
 | 
			
		||||
        $this->entityRepo = $entityRepo;
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Display the homepage.
 | 
			
		||||
| 
						 | 
				
			
			@ -26,10 +19,20 @@ class HomeController extends Controller
 | 
			
		|||
    public function index()
 | 
			
		||||
    {
 | 
			
		||||
        $activity = Activity::latest(10);
 | 
			
		||||
        $draftPages = $this->isSignedIn() ? $this->entityRepo->getUserDraftPages(6) : [];
 | 
			
		||||
        $draftPages = [];
 | 
			
		||||
 | 
			
		||||
        if ($this->isSignedIn()) {
 | 
			
		||||
            $draftPages = Page::visible()->where('draft', '=', true)
 | 
			
		||||
                ->where('created_by', '=', user()->id)
 | 
			
		||||
                ->orderBy('updated_at', 'desc')->take(6)->get();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $recentFactor = count($draftPages) > 0 ? 0.5 : 1;
 | 
			
		||||
        $recents = $this->isSignedIn() ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor);
 | 
			
		||||
        $recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12);
 | 
			
		||||
        $recents = $this->isSignedIn() ?
 | 
			
		||||
              Views::getUserRecentlyViewed(12*$recentFactor, 0)
 | 
			
		||||
            : Book::visible()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();
 | 
			
		||||
        $recentlyUpdatedPages = Page::visible()->where('draft', false)
 | 
			
		||||
            ->orderBy('updated_at', 'desc')->take(12)->get();
 | 
			
		||||
 | 
			
		||||
        $homepageOptions = ['default', 'books', 'bookshelves', 'page'];
 | 
			
		||||
        $homepageOption = setting('app-homepage-type', 'default');
 | 
			
		||||
| 
						 | 
				
			
			@ -66,16 +69,18 @@ class HomeController extends Controller
 | 
			
		|||
        }
 | 
			
		||||
 | 
			
		||||
        if ($homepageOption === 'bookshelves') {
 | 
			
		||||
            $shelves = $this->entityRepo->getAllPaginated('bookshelf', 18, $commonData['sort'], $commonData['order']);
 | 
			
		||||
            $shelfRepo = app(BookshelfRepo::class);
 | 
			
		||||
            $shelves = app(BookshelfRepo::class)->getAllPaginated(18, $commonData['sort'], $commonData['order']);
 | 
			
		||||
            foreach ($shelves as $shelf) {
 | 
			
		||||
                $shelf->books = $this->entityRepo->getBookshelfChildren($shelf);
 | 
			
		||||
                $shelf->books = $shelf->visibleBooks;
 | 
			
		||||
            }
 | 
			
		||||
            $data = array_merge($commonData, ['shelves' => $shelves]);
 | 
			
		||||
            return view('common.home-shelves', $data);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($homepageOption === 'books') {
 | 
			
		||||
            $books = $this->entityRepo->getAllPaginated('book', 18, $commonData['sort'], $commonData['order']);
 | 
			
		||||
            $bookRepo = app(BookRepo::class);
 | 
			
		||||
            $books = $bookRepo->getAllPaginated(18, $commonData['sort'], $commonData['order']);
 | 
			
		||||
            $data = array_merge($commonData, ['books' => $books]);
 | 
			
		||||
            return view('common.home-book', $data);
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -83,8 +88,9 @@ class HomeController extends Controller
 | 
			
		|||
        if ($homepageOption === 'page') {
 | 
			
		||||
            $homepageSetting = setting('app-homepage', '0:');
 | 
			
		||||
            $id = intval(explode(':', $homepageSetting)[0]);
 | 
			
		||||
            $customHomepage = $this->entityRepo->getById('page', $id, false, true);
 | 
			
		||||
            $this->entityRepo->renderPage($customHomepage, true);
 | 
			
		||||
            $customHomepage = Page::query()->where('draft', '=', false)->findOrFail($id);
 | 
			
		||||
            $pageContent = new PageContent($customHomepage);
 | 
			
		||||
            $customHomepage->html = $pageContent->render(true);
 | 
			
		||||
            return view('common.home-custom', array_merge($commonData, ['customHomepage' => $customHomepage]));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
<?php namespace BookStack\Http\Controllers\Images;
 | 
			
		||||
 | 
			
		||||
use BookStack\Entities\Repos\EntityRepo;
 | 
			
		||||
use BookStack\Entities\Page;
 | 
			
		||||
use BookStack\Exceptions\ImageUploadException;
 | 
			
		||||
use BookStack\Http\Controllers\Controller;
 | 
			
		||||
use BookStack\Repos\PageRepo;
 | 
			
		||||
| 
						 | 
				
			
			@ -69,16 +69,21 @@ class ImageController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the usage of an image on pages.
 | 
			
		||||
     * @param \BookStack\Entities\Repos\EntityRepo $entityRepo
 | 
			
		||||
     * @param $id
 | 
			
		||||
     * @return \Illuminate\Http\JsonResponse
 | 
			
		||||
     */
 | 
			
		||||
    public function usage(EntityRepo $entityRepo, $id)
 | 
			
		||||
    public function usage(int $id)
 | 
			
		||||
    {
 | 
			
		||||
        $image = $this->imageRepo->getById($id);
 | 
			
		||||
        $this->checkImagePermission($image);
 | 
			
		||||
        $pageSearch = $entityRepo->searchForImage($image->url);
 | 
			
		||||
        return response()->json($pageSearch);
 | 
			
		||||
 | 
			
		||||
        $pages = Page::visible()->where('html', 'like', '%' . $image->url . '%')->get(['id', 'name', 'slug', 'book_id']);
 | 
			
		||||
        foreach ($pages as $page) {
 | 
			
		||||
            $page->url = $page->getUrl();
 | 
			
		||||
            $page->html = '';
 | 
			
		||||
            $page->text = '';
 | 
			
		||||
        }
 | 
			
		||||
        $result = count($pages) > 0 ? $pages : false;
 | 
			
		||||
 | 
			
		||||
        return response()->json($result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,18 +1,17 @@
 | 
			
		|||
<?php namespace BookStack\Http\Controllers;
 | 
			
		||||
 | 
			
		||||
use Activity;
 | 
			
		||||
use BookStack\Auth\UserRepo;
 | 
			
		||||
use BookStack\Entities\Managers\BookContents;
 | 
			
		||||
use BookStack\Entities\Managers\PageContent;
 | 
			
		||||
use BookStack\Entities\Managers\PageEditActivity;
 | 
			
		||||
use BookStack\Entities\Page;
 | 
			
		||||
use BookStack\Entities\Repos\PageRepo;
 | 
			
		||||
use BookStack\Exceptions\NotFoundException;
 | 
			
		||||
use BookStack\Exceptions\NotifyException;
 | 
			
		||||
use BookStack\Exceptions\PermissionsException;
 | 
			
		||||
use Exception;
 | 
			
		||||
use GatherContent\Htmldiff\Htmldiff;
 | 
			
		||||
use Illuminate\Contracts\View\Factory;
 | 
			
		||||
use Illuminate\Http\JsonResponse;
 | 
			
		||||
use Illuminate\Http\RedirectResponse;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
use Illuminate\Http\Response;
 | 
			
		||||
use Illuminate\Routing\Redirector;
 | 
			
		||||
use Illuminate\View\View;
 | 
			
		||||
use Illuminate\Validation\ValidationException;
 | 
			
		||||
use Throwable;
 | 
			
		||||
use Views;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -20,44 +19,28 @@ class PageController extends Controller
 | 
			
		|||
{
 | 
			
		||||
 | 
			
		||||
    protected $pageRepo;
 | 
			
		||||
    protected $userRepo;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * PageController constructor.
 | 
			
		||||
     * @param PageRepo $pageRepo
 | 
			
		||||
     * @param UserRepo $userRepo
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(PageRepo $pageRepo, UserRepo $userRepo)
 | 
			
		||||
    public function __construct(PageRepo $pageRepo)
 | 
			
		||||
    {
 | 
			
		||||
        $this->pageRepo = $pageRepo;
 | 
			
		||||
        $this->userRepo = $userRepo;
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the form for creating a new page.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $chapterSlug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @internal param bool $pageSlug
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function create($bookSlug, $chapterSlug = null)
 | 
			
		||||
    public function create(string $bookSlug, string $chapterSlug = null)
 | 
			
		||||
    {
 | 
			
		||||
        if ($chapterSlug !== null) {
 | 
			
		||||
            $chapter = $this->pageRepo->getEntityBySlug('chapter', $chapterSlug, $bookSlug);
 | 
			
		||||
            $book = $chapter->book;
 | 
			
		||||
        } else {
 | 
			
		||||
            $chapter = null;
 | 
			
		||||
            $book = $this->pageRepo->getEntityBySlug('book', $bookSlug);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $parent = $chapter ? $chapter : $book;
 | 
			
		||||
        $parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
 | 
			
		||||
        $this->checkOwnablePermission('page-create', $parent);
 | 
			
		||||
 | 
			
		||||
        // Redirect to draft edit screen if signed in
 | 
			
		||||
        if ($this->isSignedIn()) {
 | 
			
		||||
            $draft = $this->pageRepo->getDraftPage($book, $chapter);
 | 
			
		||||
            $draft = $this->pageRepo->getNewDraftPage($parent);
 | 
			
		||||
            return redirect($draft->getUrl());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -68,51 +51,38 @@ class PageController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a new page as a guest user.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string|null $chapterSlug
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     * @throws ValidationException
 | 
			
		||||
     */
 | 
			
		||||
    public function createAsGuest(Request $request, $bookSlug, $chapterSlug = null)
 | 
			
		||||
    public function createAsGuest(Request $request, string $bookSlug, string $chapterSlug = null)
 | 
			
		||||
    {
 | 
			
		||||
        $this->validate($request, [
 | 
			
		||||
            'name' => 'required|string|max:255'
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        if ($chapterSlug !== null) {
 | 
			
		||||
            $chapter = $this->pageRepo->getEntityBySlug('chapter', $chapterSlug, $bookSlug);
 | 
			
		||||
            $book = $chapter->book;
 | 
			
		||||
        } else {
 | 
			
		||||
            $chapter = null;
 | 
			
		||||
            $book = $this->pageRepo->getEntityBySlug('book', $bookSlug);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $parent = $chapter ? $chapter : $book;
 | 
			
		||||
        $parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
 | 
			
		||||
        $this->checkOwnablePermission('page-create', $parent);
 | 
			
		||||
 | 
			
		||||
        $page = $this->pageRepo->getDraftPage($book, $chapter);
 | 
			
		||||
        $this->pageRepo->publishPageDraft($page, [
 | 
			
		||||
        $page = $this->pageRepo->getNewDraftPage($parent);
 | 
			
		||||
        $this->pageRepo->publishDraft($page, [
 | 
			
		||||
            'name' => $request->get('name'),
 | 
			
		||||
            'html' => ''
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        return redirect($page->getUrl('/edit'));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show form to continue editing a draft page.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param int $pageId
 | 
			
		||||
     * @return Factory|View
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function editDraft($bookSlug, $pageId)
 | 
			
		||||
    public function editDraft(string $bookSlug, int $pageId)
 | 
			
		||||
    {
 | 
			
		||||
        $draft = $this->pageRepo->getById('page', $pageId, true);
 | 
			
		||||
        $this->checkOwnablePermission('page-create', $draft->parent);
 | 
			
		||||
        $draft = $this->pageRepo->getById($pageId);
 | 
			
		||||
        $this->checkOwnablePermission('page-create', $draft->parent());
 | 
			
		||||
        $this->setPageTitle(trans('entities.pages_edit_draft'));
 | 
			
		||||
 | 
			
		||||
        $draftsEnabled = $this->isSignedIn();
 | 
			
		||||
        $templates = $this->pageRepo->getPageTemplates(10);
 | 
			
		||||
        $templates = $this->pageRepo->getTemplates(10);
 | 
			
		||||
 | 
			
		||||
        return view('pages.edit', [
 | 
			
		||||
            'page' => $draft,
 | 
			
		||||
| 
						 | 
				
			
			@ -125,63 +95,50 @@ class PageController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Store a new page by changing a draft into a page.
 | 
			
		||||
     * @param  Request $request
 | 
			
		||||
     * @param  string $bookSlug
 | 
			
		||||
     * @param  int $pageId
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     * @throws ValidationException
 | 
			
		||||
     */
 | 
			
		||||
    public function store(Request $request, $bookSlug, $pageId)
 | 
			
		||||
    public function store(Request $request, string $bookSlug, int $pageId)
 | 
			
		||||
    {
 | 
			
		||||
        $this->validate($request, [
 | 
			
		||||
            'name' => 'required|string|max:255'
 | 
			
		||||
        ]);
 | 
			
		||||
        $draftPage = $this->pageRepo->getById($pageId);
 | 
			
		||||
        $this->checkOwnablePermission('page-create', $draftPage->parent());
 | 
			
		||||
 | 
			
		||||
        $input = $request->all();
 | 
			
		||||
        $draftPage = $this->pageRepo->getById('page', $pageId, true);
 | 
			
		||||
        $book = $draftPage->book;
 | 
			
		||||
        $page = $this->pageRepo->publishDraft($draftPage, $request->all());
 | 
			
		||||
        Activity::add($page, 'page_create', $draftPage->book->id);
 | 
			
		||||
 | 
			
		||||
        $parent = $draftPage->parent;
 | 
			
		||||
        $this->checkOwnablePermission('page-create', $parent);
 | 
			
		||||
 | 
			
		||||
        if ($parent->isA('chapter')) {
 | 
			
		||||
            $input['priority'] = $this->pageRepo->getNewChapterPriority($parent);
 | 
			
		||||
        } else {
 | 
			
		||||
            $input['priority'] = $this->pageRepo->getNewBookPriority($parent);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $page = $this->pageRepo->publishPageDraft($draftPage, $input);
 | 
			
		||||
 | 
			
		||||
        Activity::add($page, 'page_create', $book->id);
 | 
			
		||||
        return redirect($page->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Display the specified page.
 | 
			
		||||
     * If the page is not found via the slug the revisions are searched for a match.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $pageSlug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function show($bookSlug, $pageSlug)
 | 
			
		||||
    public function show(string $bookSlug, string $pageSlug)
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
            $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
 | 
			
		||||
            $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
 | 
			
		||||
        } catch (NotFoundException $e) {
 | 
			
		||||
            $page = $this->pageRepo->getPageByOldSlug($pageSlug, $bookSlug);
 | 
			
		||||
            $page = $this->pageRepo->getByOldSlug($bookSlug, $pageSlug);
 | 
			
		||||
 | 
			
		||||
            if ($page === null) {
 | 
			
		||||
                throw $e;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return redirect($page->getUrl());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->checkOwnablePermission('page-view', $page);
 | 
			
		||||
 | 
			
		||||
        $page->html = $this->pageRepo->renderPage($page);
 | 
			
		||||
        $sidebarTree = $this->pageRepo->getBookChildren($page->book);
 | 
			
		||||
        $pageNav = $this->pageRepo->getPageNav($page->html);
 | 
			
		||||
        $pageContent = (new PageContent($page));
 | 
			
		||||
        $page->html = $pageContent->render();
 | 
			
		||||
        $sidebarTree = (new BookContents($page->book))->getTree();
 | 
			
		||||
        $pageNav = $pageContent->getNavigation($page->html);
 | 
			
		||||
 | 
			
		||||
        // check if the comment's are enabled
 | 
			
		||||
        // Check if page comments are enabled
 | 
			
		||||
        $commentsEnabled = !setting('app-disable-comments');
 | 
			
		||||
        if ($commentsEnabled) {
 | 
			
		||||
            $page->load(['comments.createdBy']);
 | 
			
		||||
| 
						 | 
				
			
			@ -190,7 +147,8 @@ class PageController extends Controller
 | 
			
		|||
        Views::add($page);
 | 
			
		||||
        $this->setPageTitle($page->getShortName());
 | 
			
		||||
        return view('pages.show', [
 | 
			
		||||
            'page' => $page,'book' => $page->book,
 | 
			
		||||
            'page' => $page,
 | 
			
		||||
            'book' => $page->book,
 | 
			
		||||
            'current' => $page,
 | 
			
		||||
            'sidebarTree' => $sidebarTree,
 | 
			
		||||
            'commentsEnabled' => $commentsEnabled,
 | 
			
		||||
| 
						 | 
				
			
			@ -200,52 +158,47 @@ class PageController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get page from an ajax request.
 | 
			
		||||
     * @param int $pageId
 | 
			
		||||
     * @return JsonResponse
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function getPageAjax($pageId)
 | 
			
		||||
    public function getPageAjax(int $pageId)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getById('page', $pageId);
 | 
			
		||||
        $page = $this->pageRepo->getById($pageId);
 | 
			
		||||
        return response()->json($page);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the form for editing the specified page.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $pageSlug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function edit($bookSlug, $pageSlug)
 | 
			
		||||
    public function edit(string $bookSlug, string $pageSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
 | 
			
		||||
        $this->checkOwnablePermission('page-update', $page);
 | 
			
		||||
        $this->setPageTitle(trans('entities.pages_editing_named', ['pageName'=>$page->getShortName()]));
 | 
			
		||||
 | 
			
		||||
        $page->isDraft = false;
 | 
			
		||||
        $editActivity = new PageEditActivity($page);
 | 
			
		||||
 | 
			
		||||
        // Check for active editing
 | 
			
		||||
        $warnings = [];
 | 
			
		||||
        if ($this->pageRepo->isPageEditingActive($page, 60)) {
 | 
			
		||||
            $warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60);
 | 
			
		||||
        if ($editActivity->hasActiveEditing()) {
 | 
			
		||||
            $warnings[] = $editActivity->activeEditingMessage();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check for a current draft version for this user
 | 
			
		||||
        $userPageDraft = $this->pageRepo->getUserPageDraft($page, user()->id);
 | 
			
		||||
        if ($userPageDraft !== null) {
 | 
			
		||||
            $page->name = $userPageDraft->name;
 | 
			
		||||
            $page->html = $userPageDraft->html;
 | 
			
		||||
            $page->markdown = $userPageDraft->markdown;
 | 
			
		||||
        $userDraft = $this->pageRepo->getUserDraft($page);
 | 
			
		||||
        if ($userDraft !== null) {
 | 
			
		||||
            $page->forceFill($userDraft->only(['name', 'html', 'markdown']));
 | 
			
		||||
            $page->isDraft = true;
 | 
			
		||||
            $warnings [] = $this->pageRepo->getUserPageDraftMessage($userPageDraft);
 | 
			
		||||
            $warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (count($warnings) > 0) {
 | 
			
		||||
            $this->showWarningNotification( implode("\n", $warnings));
 | 
			
		||||
            $this->showWarningNotification(implode("\n", $warnings));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $templates = $this->pageRepo->getTemplates(10);
 | 
			
		||||
        $draftsEnabled = $this->isSignedIn();
 | 
			
		||||
        $templates = $this->pageRepo->getPageTemplates(10);
 | 
			
		||||
 | 
			
		||||
        $this->setPageTitle(trans('entities.pages_editing_named', ['pageName' => $page->getShortName()]));
 | 
			
		||||
        return view('pages.edit', [
 | 
			
		||||
            'page' => $page,
 | 
			
		||||
            'book' => $page->book,
 | 
			
		||||
| 
						 | 
				
			
			@ -257,39 +210,34 @@ class PageController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the specified page in storage.
 | 
			
		||||
     * @param  Request $request
 | 
			
		||||
     * @param  string $bookSlug
 | 
			
		||||
     * @param  string $pageSlug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @throws ValidationException
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function update(Request $request, $bookSlug, $pageSlug)
 | 
			
		||||
    public function update(Request $request, string $bookSlug, string $pageSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $this->validate($request, [
 | 
			
		||||
            'name' => 'required|string|max:255'
 | 
			
		||||
        ]);
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
 | 
			
		||||
        $this->checkOwnablePermission('page-update', $page);
 | 
			
		||||
        $this->pageRepo->updatePage($page, $page->book->id, $request->all());
 | 
			
		||||
 | 
			
		||||
        $this->pageRepo->update($page, $request->all());
 | 
			
		||||
        Activity::add($page, 'page_update', $page->book->id);
 | 
			
		||||
 | 
			
		||||
        return redirect($page->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Save a draft update as a revision.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @param int $pageId
 | 
			
		||||
     * @return JsonResponse
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function saveDraft(Request $request, $pageId)
 | 
			
		||||
    public function saveDraft(Request $request, int $pageId)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getById('page', $pageId, true);
 | 
			
		||||
        $page = $this->pageRepo->getById($pageId);
 | 
			
		||||
        $this->checkOwnablePermission('page-update', $page);
 | 
			
		||||
 | 
			
		||||
        if (!$this->isSignedIn()) {
 | 
			
		||||
            return response()->json([
 | 
			
		||||
                'status' => 'error',
 | 
			
		||||
                'message' => trans('errors.guests_cannot_save_drafts'),
 | 
			
		||||
            ], 500);
 | 
			
		||||
            return $this->jsonError(trans('errors.guests_cannot_save_drafts'), 500);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
 | 
			
		||||
| 
						 | 
				
			
			@ -303,211 +251,98 @@ class PageController extends Controller
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Redirect from a special link url which
 | 
			
		||||
     * uses the page id rather than the name.
 | 
			
		||||
     * @param int $pageId
 | 
			
		||||
     * @return RedirectResponse|Redirector
 | 
			
		||||
     * Redirect from a special link url which uses the page id rather than the name.
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function redirectFromLink($pageId)
 | 
			
		||||
    public function redirectFromLink(int $pageId)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getById('page', $pageId);
 | 
			
		||||
        $page = $this->pageRepo->getById($pageId);
 | 
			
		||||
        return redirect($page->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the deletion page for the specified page.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $pageSlug
 | 
			
		||||
     * @return View
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function showDelete($bookSlug, $pageSlug)
 | 
			
		||||
    public function showDelete(string $bookSlug, string $pageSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
 | 
			
		||||
        $this->checkOwnablePermission('page-delete', $page);
 | 
			
		||||
        $this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
 | 
			
		||||
        return view('pages.delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
 | 
			
		||||
        return view('pages.delete', [
 | 
			
		||||
            'book' => $page->book,
 | 
			
		||||
            'page' => $page,
 | 
			
		||||
            'current' => $page
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the deletion page for the specified page.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param int $pageId
 | 
			
		||||
     * @return View
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function showDeleteDraft($bookSlug, $pageId)
 | 
			
		||||
    public function showDeleteDraft(string $bookSlug, int $pageId)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getById('page', $pageId, true);
 | 
			
		||||
        $page = $this->pageRepo->getById($pageId);
 | 
			
		||||
        $this->checkOwnablePermission('page-update', $page);
 | 
			
		||||
        $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
 | 
			
		||||
        return view('pages.delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
 | 
			
		||||
        return view('pages.delete', [
 | 
			
		||||
            'book' => $page->book,
 | 
			
		||||
            'page' => $page,
 | 
			
		||||
            'current' => $page
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove the specified page from storage.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $pageSlug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @internal param int $id
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     * @throws NotifyException
 | 
			
		||||
     */
 | 
			
		||||
    public function destroy($bookSlug, $pageSlug)
 | 
			
		||||
    public function destroy(string $bookSlug, string $pageSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
 | 
			
		||||
        $book = $page->book;
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
 | 
			
		||||
        $this->checkOwnablePermission('page-delete', $page);
 | 
			
		||||
        $this->pageRepo->destroyPage($page);
 | 
			
		||||
 | 
			
		||||
        $book = $page->book;
 | 
			
		||||
        $this->pageRepo->destroy($page);
 | 
			
		||||
        Activity::addMessage('page_delete', $page->name, $book->id);
 | 
			
		||||
        $this->showSuccessNotification( trans('entities.pages_delete_success'));
 | 
			
		||||
 | 
			
		||||
        $this->showSuccessNotification(trans('entities.pages_delete_success'));
 | 
			
		||||
        return redirect($book->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove the specified draft page from storage.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param int $pageId
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     * @throws NotifyException
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function destroyDraft($bookSlug, $pageId)
 | 
			
		||||
    public function destroyDraft(string $bookSlug, int $pageId)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getById('page', $pageId, true);
 | 
			
		||||
        $page = $this->pageRepo->getById($pageId);
 | 
			
		||||
        $book = $page->book;
 | 
			
		||||
        $chapter = $page->chapter;
 | 
			
		||||
        $this->checkOwnablePermission('page-update', $page);
 | 
			
		||||
        $this->showSuccessNotification( trans('entities.pages_delete_draft_success'));
 | 
			
		||||
        $this->pageRepo->destroyPage($page);
 | 
			
		||||
 | 
			
		||||
        $this->pageRepo->destroy($page);
 | 
			
		||||
 | 
			
		||||
        $this->showSuccessNotification(trans('entities.pages_delete_draft_success'));
 | 
			
		||||
 | 
			
		||||
        if ($chapter && userCan('view', $chapter)) {
 | 
			
		||||
            return redirect($chapter->getUrl());
 | 
			
		||||
        }
 | 
			
		||||
        return redirect($book->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Shows the last revisions for this page.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $pageSlug
 | 
			
		||||
     * @return View
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function showRevisions($bookSlug, $pageSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
 | 
			
		||||
        $this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
 | 
			
		||||
        return view('pages.revisions', ['page' => $page, 'current' => $page]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Shows a preview of a single revision
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $pageSlug
 | 
			
		||||
     * @param int $revisionId
 | 
			
		||||
     * @return View
 | 
			
		||||
     */
 | 
			
		||||
    public function showRevision($bookSlug, $pageSlug, $revisionId)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
 | 
			
		||||
        $revision = $page->revisions()->where('id', '=', $revisionId)->first();
 | 
			
		||||
        if ($revision === null) {
 | 
			
		||||
            abort(404);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $page->fill($revision->toArray());
 | 
			
		||||
        $this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
 | 
			
		||||
 | 
			
		||||
        return view('pages.revision', [
 | 
			
		||||
            'page' => $page,
 | 
			
		||||
            'book' => $page->book,
 | 
			
		||||
            'diff' => null,
 | 
			
		||||
            'revision' => $revision
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Shows the changes of a single revision
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $pageSlug
 | 
			
		||||
     * @param int $revisionId
 | 
			
		||||
     * @return View
 | 
			
		||||
     */
 | 
			
		||||
    public function showRevisionChanges($bookSlug, $pageSlug, $revisionId)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
 | 
			
		||||
        $revision = $page->revisions()->where('id', '=', $revisionId)->first();
 | 
			
		||||
        if ($revision === null) {
 | 
			
		||||
            abort(404);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $prev = $revision->getPrevious();
 | 
			
		||||
        $prevContent = ($prev === null) ? '' : $prev->html;
 | 
			
		||||
        $diff = (new Htmldiff)->diff($prevContent, $revision->html);
 | 
			
		||||
 | 
			
		||||
        $page->fill($revision->toArray());
 | 
			
		||||
        $this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
 | 
			
		||||
 | 
			
		||||
        return view('pages.revision', [
 | 
			
		||||
            'page' => $page,
 | 
			
		||||
            'book' => $page->book,
 | 
			
		||||
            'diff' => $diff,
 | 
			
		||||
            'revision' => $revision
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Restores a page using the content of the specified revision.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $pageSlug
 | 
			
		||||
     * @param int $revisionId
 | 
			
		||||
     * @return RedirectResponse|Redirector
 | 
			
		||||
     */
 | 
			
		||||
    public function restoreRevision($bookSlug, $pageSlug, $revisionId)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
 | 
			
		||||
        $this->checkOwnablePermission('page-update', $page);
 | 
			
		||||
        $page = $this->pageRepo->restorePageRevision($page, $page->book, $revisionId);
 | 
			
		||||
        Activity::add($page, 'page_restore', $page->book->id);
 | 
			
		||||
        return redirect($page->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Deletes a revision using the id of the specified revision.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $pageSlug
 | 
			
		||||
     * @param int $revId
 | 
			
		||||
     * @return RedirectResponse|Redirector
 | 
			
		||||
     *@throws BadRequestException
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function destroyRevision($bookSlug, $pageSlug, $revId)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
 | 
			
		||||
        $this->checkOwnablePermission('page-delete', $page);
 | 
			
		||||
 | 
			
		||||
        $revision = $page->revisions()->where('id', '=', $revId)->first();
 | 
			
		||||
        if ($revision === null) {
 | 
			
		||||
            throw new NotFoundException("Revision #{$revId} not found");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Get the current revision for the page
 | 
			
		||||
        $currentRevision = $page->getCurrentRevision();
 | 
			
		||||
 | 
			
		||||
        // Check if its the latest revision, cannot delete latest revision.
 | 
			
		||||
        if (intval($currentRevision->id) === intval($revId)) {
 | 
			
		||||
            $this->showErrorNotification( trans('entities.revision_cannot_delete_latest'));
 | 
			
		||||
            return response()->view('pages.revisions', ['page' => $page, 'book' => $page->book, 'current' => $page], 400);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $revision->delete();
 | 
			
		||||
        $this->showSuccessNotification( trans('entities.revision_delete_success'));
 | 
			
		||||
        return redirect($page->getUrl('/revisions'));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show a listing of recently created pages
 | 
			
		||||
     * @return Factory|View
 | 
			
		||||
     * Show a listing of recently created pages.
 | 
			
		||||
     */
 | 
			
		||||
    public function showRecentlyUpdated()
 | 
			
		||||
    {
 | 
			
		||||
        // TODO - Still exist?
 | 
			
		||||
        $pages = $this->pageRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(url('/pages/recently-updated'));
 | 
			
		||||
        $pages = Page::visible()->orderBy('updated_at', 'desc')
 | 
			
		||||
            ->paginate(20)
 | 
			
		||||
            ->setPath(url('/pages/recently-updated'));
 | 
			
		||||
 | 
			
		||||
        return view('pages.detailed-listing', [
 | 
			
		||||
            'title' => trans('entities.recently_updated_pages'),
 | 
			
		||||
            'pages' => $pages
 | 
			
		||||
| 
						 | 
				
			
			@ -516,14 +351,11 @@ class PageController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the view to choose a new parent to move a page into.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $pageSlug
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function showMove($bookSlug, $pageSlug)
 | 
			
		||||
    public function showMove(string $bookSlug, string $pageSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
 | 
			
		||||
        $this->checkOwnablePermission('page-update', $page);
 | 
			
		||||
        $this->checkOwnablePermission('page-delete', $page);
 | 
			
		||||
        return view('pages.move', [
 | 
			
		||||
| 
						 | 
				
			
			@ -533,17 +365,13 @@ class PageController extends Controller
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Does the action of moving the location of a page
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $pageSlug
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     * Does the action of moving the location of a page.
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function move(Request $request, string $bookSlug, string $pageSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
 | 
			
		||||
        $this->checkOwnablePermission('page-update', $page);
 | 
			
		||||
        $this->checkOwnablePermission('page-delete', $page);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -552,37 +380,29 @@ class PageController extends Controller
 | 
			
		|||
            return redirect($page->getUrl());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $stringExploded = explode(':', $entitySelection);
 | 
			
		||||
        $entityType = $stringExploded[0];
 | 
			
		||||
        $entityId = intval($stringExploded[1]);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            $parent = $this->pageRepo->getById($entityType, $entityId);
 | 
			
		||||
        } catch (Exception $e) {
 | 
			
		||||
            session()->flash(trans('entities.selected_book_chapter_not_found'));
 | 
			
		||||
            $parent = $this->pageRepo->move($page, $entitySelection);
 | 
			
		||||
        } catch (Exception $exception) {
 | 
			
		||||
            if ($exception instanceof  PermissionsException) {
 | 
			
		||||
                $this->showPermissionError();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
 | 
			
		||||
            return redirect()->back();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->checkOwnablePermission('page-create', $parent);
 | 
			
		||||
 | 
			
		||||
        $this->pageRepo->changePageParent($page, $parent);
 | 
			
		||||
        Activity::add($page, 'page_move', $page->book->id);
 | 
			
		||||
        $this->showSuccessNotification( trans('entities.pages_move_success', ['parentName' => $parent->name]));
 | 
			
		||||
 | 
			
		||||
        $this->showSuccessNotification(trans('entities.pages_move_success', ['parentName' => $parent->name]));
 | 
			
		||||
        return redirect($page->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the view to copy a page.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $pageSlug
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function showCopy($bookSlug, $pageSlug)
 | 
			
		||||
    public function showCopy(string $bookSlug, string $pageSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
 | 
			
		||||
        $this->checkOwnablePermission('page-view', $page);
 | 
			
		||||
        session()->flashInput(['name' => $page->name]);
 | 
			
		||||
        return view('pages.copy', [
 | 
			
		||||
| 
						 | 
				
			
			@ -591,79 +411,65 @@ class PageController extends Controller
 | 
			
		|||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a copy of a page within the requested target destination.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $pageSlug
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function copy(Request $request, string $bookSlug, string $pageSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
 | 
			
		||||
        $this->checkOwnablePermission('page-view', $page);
 | 
			
		||||
 | 
			
		||||
        $entitySelection = $request->get('entity_selection', null);
 | 
			
		||||
        if ($entitySelection === null || $entitySelection === '') {
 | 
			
		||||
            $parent = $page->chapter ? $page->chapter : $page->book;
 | 
			
		||||
        } else {
 | 
			
		||||
            $stringExploded = explode(':', $entitySelection);
 | 
			
		||||
            $entityType = $stringExploded[0];
 | 
			
		||||
            $entityId = intval($stringExploded[1]);
 | 
			
		||||
        $entitySelection = $request->get('entity_selection', null) ?? null;
 | 
			
		||||
        $newName = $request->get('name', null);
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                $parent = $this->pageRepo->getById($entityType, $entityId);
 | 
			
		||||
            } catch (Exception $e) {
 | 
			
		||||
                $this->showErrorNotification(trans('entities.selected_book_chapter_not_found'));
 | 
			
		||||
                return redirect()->back();
 | 
			
		||||
        try {
 | 
			
		||||
            $pageCopy = $this->pageRepo->copy($page, $entitySelection, $newName);
 | 
			
		||||
        } catch (Exception $exception) {
 | 
			
		||||
            if ($exception instanceof  PermissionsException) {
 | 
			
		||||
                $this->showPermissionError();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
 | 
			
		||||
            return redirect()->back();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->checkOwnablePermission('page-create', $parent);
 | 
			
		||||
 | 
			
		||||
        $pageCopy = $this->pageRepo->copyPage($page, $parent, $request->get('name', ''));
 | 
			
		||||
 | 
			
		||||
        Activity::add($pageCopy, 'page_create', $pageCopy->book->id);
 | 
			
		||||
        $this->showSuccessNotification( trans('entities.pages_copy_success'));
 | 
			
		||||
 | 
			
		||||
        $this->showSuccessNotification(trans('entities.pages_copy_success'));
 | 
			
		||||
        return redirect($pageCopy->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the Permissions view.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $pageSlug
 | 
			
		||||
     * @return Factory|View
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function showPermissions($bookSlug, $pageSlug)
 | 
			
		||||
    public function showPermissions(string $bookSlug, string $pageSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
 | 
			
		||||
        $this->checkOwnablePermission('restrictions-manage', $page);
 | 
			
		||||
        $roles = $this->userRepo->getRestrictableRoles();
 | 
			
		||||
        return view('pages.permissions', [
 | 
			
		||||
            'page'  => $page,
 | 
			
		||||
            'roles' => $roles
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set the permissions for this page.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $pageSlug
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @return RedirectResponse|Redirector
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function permissions(Request $request, string $bookSlug, string $pageSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
 | 
			
		||||
        $this->checkOwnablePermission('restrictions-manage', $page);
 | 
			
		||||
        $this->pageRepo->updateEntityPermissionsFromRequest($request, $page);
 | 
			
		||||
        $this->showSuccessNotification( trans('entities.pages_permissions_success'));
 | 
			
		||||
 | 
			
		||||
        $restricted = $request->get('restricted') === 'true';
 | 
			
		||||
        $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
 | 
			
		||||
        $this->pageRepo->updatePermissions($page, $restricted, $permissions);
 | 
			
		||||
 | 
			
		||||
        $this->showSuccessNotification(trans('entities.pages_permissions_success'));
 | 
			
		||||
        return redirect($page->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,21 +3,15 @@
 | 
			
		|||
namespace BookStack\Http\Controllers;
 | 
			
		||||
 | 
			
		||||
use BookStack\Entities\ExportService;
 | 
			
		||||
use BookStack\Entities\Managers\PageContent;
 | 
			
		||||
use BookStack\Entities\Repos\PageRepo;
 | 
			
		||||
use BookStack\Exceptions\NotFoundException;
 | 
			
		||||
use Illuminate\Http\Response;
 | 
			
		||||
use Throwable;
 | 
			
		||||
 | 
			
		||||
class PageExportController extends Controller
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @var PageRepo
 | 
			
		||||
     */
 | 
			
		||||
    protected $pageRepo;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var ExportService
 | 
			
		||||
     */
 | 
			
		||||
    protected $pageRepo;
 | 
			
		||||
    protected $exportService;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			@ -35,46 +29,37 @@ class PageExportController extends Controller
 | 
			
		|||
    /**
 | 
			
		||||
     * Exports a page to a PDF.
 | 
			
		||||
     * https://github.com/barryvdh/laravel-dompdf
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $pageSlug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function pdf(string $bookSlug, string $pageSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
 | 
			
		||||
        $page->html = $this->pageRepo->renderPage($page);
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
 | 
			
		||||
        $page->html = (new PageContent($page))->render();
 | 
			
		||||
        $pdfContent = $this->exportService->pageToPdf($page);
 | 
			
		||||
        return $this->downloadResponse($pdfContent, $pageSlug . '.pdf');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Export a page to a self-contained HTML file.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $pageSlug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function html(string $bookSlug, string $pageSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
 | 
			
		||||
        $page->html = $this->pageRepo->renderPage($page);
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
 | 
			
		||||
        $page->html = (new PageContent($page))->render();
 | 
			
		||||
        $containedHtml = $this->exportService->pageToContainedHtml($page);
 | 
			
		||||
        return $this->downloadResponse($containedHtml, $pageSlug . '.html');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Export a page to a simple plaintext .txt file.
 | 
			
		||||
     * @param string $bookSlug
 | 
			
		||||
     * @param string $pageSlug
 | 
			
		||||
     * @return Response
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function plainText(string $bookSlug, string $pageSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
 | 
			
		||||
        $pageText = $this->exportService->pageToPlainText($page);
 | 
			
		||||
        return $this->downloadResponse($pageText, $pageSlug . '.txt');
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,128 @@
 | 
			
		|||
<?php namespace BookStack\Http\Controllers;
 | 
			
		||||
 | 
			
		||||
use BookStack\Entities\Repos\PageRepo;
 | 
			
		||||
use BookStack\Exceptions\NotFoundException;
 | 
			
		||||
use BookStack\Facades\Activity;
 | 
			
		||||
use GatherContent\Htmldiff\Htmldiff;
 | 
			
		||||
 | 
			
		||||
class PageRevisionController extends Controller
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    protected $pageRepo;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * PageRevisionController constructor.
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(PageRepo $pageRepo)
 | 
			
		||||
    {
 | 
			
		||||
        $this->pageRepo = $pageRepo;
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Shows the last revisions for this page.
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function index(string $bookSlug, string $pageSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
 | 
			
		||||
        $this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
 | 
			
		||||
        return view('pages.revisions', [
 | 
			
		||||
            'page' => $page,
 | 
			
		||||
            'current' => $page
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Shows a preview of a single revision.
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function show(string $bookSlug, string $pageSlug, int $revisionId)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
 | 
			
		||||
        $revision = $page->revisions()->where('id', '=', $revisionId)->first();
 | 
			
		||||
        if ($revision === null) {
 | 
			
		||||
            throw new NotFoundException();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $page->fill($revision->toArray());
 | 
			
		||||
 | 
			
		||||
        $this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
 | 
			
		||||
        return view('pages.revision', [
 | 
			
		||||
            'page' => $page,
 | 
			
		||||
            'book' => $page->book,
 | 
			
		||||
            'diff' => null,
 | 
			
		||||
            'revision' => $revision
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Shows the changes of a single revision.
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function changes(string $bookSlug, string $pageSlug, int $revisionId)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
 | 
			
		||||
        $revision = $page->revisions()->where('id', '=', $revisionId)->first();
 | 
			
		||||
        if ($revision === null) {
 | 
			
		||||
            throw new NotFoundException();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $prev = $revision->getPrevious();
 | 
			
		||||
        $prevContent = $prev->html ?? '';
 | 
			
		||||
        $diff = (new Htmldiff)->diff($prevContent, $revision->html);
 | 
			
		||||
 | 
			
		||||
        $page->fill($revision->toArray());
 | 
			
		||||
        $this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
 | 
			
		||||
 | 
			
		||||
        return view('pages.revision', [
 | 
			
		||||
            'page' => $page,
 | 
			
		||||
            'book' => $page->book,
 | 
			
		||||
            'diff' => $diff,
 | 
			
		||||
            'revision' => $revision
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Restores a page using the content of the specified revision.
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function restore(string $bookSlug, string $pageSlug, int $revisionId)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
 | 
			
		||||
        $this->checkOwnablePermission('page-update', $page);
 | 
			
		||||
 | 
			
		||||
        $page = $this->pageRepo->restoreRevision($page, $revisionId);
 | 
			
		||||
 | 
			
		||||
        Activity::add($page, 'page_restore', $page->book->id);
 | 
			
		||||
        return redirect($page->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Deletes a revision using the id of the specified revision.
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function destroy(string $bookSlug, string $pageSlug, int $revId)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
 | 
			
		||||
        $this->checkOwnablePermission('page-delete', $page);
 | 
			
		||||
 | 
			
		||||
        $revision = $page->revisions()->where('id', '=', $revId)->first();
 | 
			
		||||
        if ($revision === null) {
 | 
			
		||||
            throw new NotFoundException("Revision #{$revId} not found");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Get the current revision for the page
 | 
			
		||||
        $currentRevision = $page->getCurrentRevision();
 | 
			
		||||
 | 
			
		||||
        // Check if its the latest revision, cannot delete latest revision.
 | 
			
		||||
        if (intval($currentRevision->id) === intval($revId)) {
 | 
			
		||||
            $this->showErrorNotification(trans('entities.revision_cannot_delete_latest'));
 | 
			
		||||
            return redirect($page->getUrl('/revisions'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $revision->delete();
 | 
			
		||||
        $this->showSuccessNotification(trans('entities.revision_delete_success'));
 | 
			
		||||
        return redirect($page->getUrl('/revisions'));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -11,8 +11,7 @@ class PageTemplateController extends Controller
 | 
			
		|||
    protected $pageRepo;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * PageTemplateController constructor.
 | 
			
		||||
     * @param $pageRepo
 | 
			
		||||
     * PageTemplateController constructor
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(PageRepo $pageRepo)
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -22,14 +21,12 @@ class PageTemplateController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch a list of templates from the system.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
 | 
			
		||||
     */
 | 
			
		||||
    public function list(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $request->get('page', 1);
 | 
			
		||||
        $search = $request->get('search', '');
 | 
			
		||||
        $templates = $this->pageRepo->getPageTemplates(10, $page, $search);
 | 
			
		||||
        $templates = $this->pageRepo->getTemplates(10, $page, $search);
 | 
			
		||||
 | 
			
		||||
        if ($search) {
 | 
			
		||||
            $templates->appends(['search' => $search]);
 | 
			
		||||
| 
						 | 
				
			
			@ -42,13 +39,11 @@ class PageTemplateController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the content of a template.
 | 
			
		||||
     * @param $templateId
 | 
			
		||||
     * @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function get($templateId)
 | 
			
		||||
    public function get(int $templateId)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getById('page', $templateId);
 | 
			
		||||
        $page = $this->pageRepo->getById($templateId);
 | 
			
		||||
 | 
			
		||||
        if (!$page->template) {
 | 
			
		||||
            throw new NotFoundException();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -53,7 +53,7 @@ class PermissionController extends Controller
 | 
			
		|||
        ]);
 | 
			
		||||
 | 
			
		||||
        $this->permissionsRepo->saveNewRole($request->all());
 | 
			
		||||
        $this->showSuccessNotification( trans('settings.role_create_success'));
 | 
			
		||||
        $this->showSuccessNotification(trans('settings.role_create_success'));
 | 
			
		||||
        return redirect('/settings/roles');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -90,7 +90,7 @@ class PermissionController extends Controller
 | 
			
		|||
        ]);
 | 
			
		||||
 | 
			
		||||
        $this->permissionsRepo->updateRole($id, $request->all());
 | 
			
		||||
        $this->showSuccessNotification( trans('settings.role_update_success'));
 | 
			
		||||
        $this->showSuccessNotification(trans('settings.role_update_success'));
 | 
			
		||||
        return redirect('/settings/roles');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -124,11 +124,11 @@ class PermissionController extends Controller
 | 
			
		|||
        try {
 | 
			
		||||
            $this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id'));
 | 
			
		||||
        } catch (PermissionsException $e) {
 | 
			
		||||
            $this->showErrorNotification( $e->getMessage());
 | 
			
		||||
            $this->showErrorNotification($e->getMessage());
 | 
			
		||||
            return redirect()->back();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->showSuccessNotification( trans('settings.role_delete_success'));
 | 
			
		||||
        $this->showSuccessNotification(trans('settings.role_delete_success'));
 | 
			
		||||
        return redirect('/settings/roles');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,35 +1,27 @@
 | 
			
		|||
<?php namespace BookStack\Http\Controllers;
 | 
			
		||||
 | 
			
		||||
use BookStack\Actions\ViewService;
 | 
			
		||||
use BookStack\Entities\EntityContextManager;
 | 
			
		||||
use BookStack\Entities\Repos\EntityRepo;
 | 
			
		||||
use BookStack\Entities\Book;
 | 
			
		||||
use BookStack\Entities\Bookshelf;
 | 
			
		||||
use BookStack\Entities\Entity;
 | 
			
		||||
use BookStack\Entities\Managers\EntityContext;
 | 
			
		||||
use BookStack\Entities\SearchService;
 | 
			
		||||
use BookStack\Exceptions\NotFoundException;
 | 
			
		||||
use Illuminate\Contracts\View\Factory;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
use Illuminate\View\View;
 | 
			
		||||
 | 
			
		||||
class SearchController extends Controller
 | 
			
		||||
{
 | 
			
		||||
    protected $entityRepo;
 | 
			
		||||
    protected $viewService;
 | 
			
		||||
    protected $searchService;
 | 
			
		||||
    protected $entityContextManager;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * SearchController constructor.
 | 
			
		||||
     * @param EntityRepo $entityRepo
 | 
			
		||||
     * @param ViewService $viewService
 | 
			
		||||
     * @param SearchService $searchService
 | 
			
		||||
     * @param EntityContextManager $entityContextManager
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        EntityRepo $entityRepo,
 | 
			
		||||
        ViewService $viewService,
 | 
			
		||||
        SearchService $searchService,
 | 
			
		||||
        EntityContextManager $entityContextManager
 | 
			
		||||
        EntityContext $entityContextManager
 | 
			
		||||
    ) {
 | 
			
		||||
        $this->entityRepo = $entityRepo;
 | 
			
		||||
        $this->viewService = $viewService;
 | 
			
		||||
        $this->searchService = $searchService;
 | 
			
		||||
        $this->entityContextManager = $entityContextManager;
 | 
			
		||||
| 
						 | 
				
			
			@ -38,9 +30,6 @@ class SearchController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Searches all entities.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @return View
 | 
			
		||||
     * @internal param string $searchTerm
 | 
			
		||||
     */
 | 
			
		||||
    public function search(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -64,12 +53,8 @@ class SearchController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Searches all entities within a book.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @param integer $bookId
 | 
			
		||||
     * @return View
 | 
			
		||||
     * @internal param string $searchTerm
 | 
			
		||||
     */
 | 
			
		||||
    public function searchBook(Request $request, $bookId)
 | 
			
		||||
    public function searchBook(Request $request, int $bookId)
 | 
			
		||||
    {
 | 
			
		||||
        $term = $request->get('term', '');
 | 
			
		||||
        $results = $this->searchService->searchBook($bookId, $term);
 | 
			
		||||
| 
						 | 
				
			
			@ -78,12 +63,8 @@ class SearchController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Searches all entities within a chapter.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @param integer $chapterId
 | 
			
		||||
     * @return View
 | 
			
		||||
     * @internal param string $searchTerm
 | 
			
		||||
     */
 | 
			
		||||
    public function searchChapter(Request $request, $chapterId)
 | 
			
		||||
    public function searchChapter(Request $request, int $chapterId)
 | 
			
		||||
    {
 | 
			
		||||
        $term = $request->get('term', '');
 | 
			
		||||
        $results = $this->searchService->searchChapter($chapterId, $term);
 | 
			
		||||
| 
						 | 
				
			
			@ -93,8 +74,6 @@ class SearchController extends Controller
 | 
			
		|||
    /**
 | 
			
		||||
     * Search for a list of entities and return a partial HTML response of matching entities.
 | 
			
		||||
     * Returns the most popular entities if no search is provided.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @return mixed
 | 
			
		||||
     */
 | 
			
		||||
    public function searchEntitiesAjax(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -115,15 +94,13 @@ class SearchController extends Controller
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Search siblings items in the system.
 | 
			
		||||
     * @param Request $request
 | 
			
		||||
     * @return Factory|View|mixed
 | 
			
		||||
     */
 | 
			
		||||
    public function searchSiblings(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        $type = $request->get('entity_type', null);
 | 
			
		||||
        $id = $request->get('entity_id', null);
 | 
			
		||||
 | 
			
		||||
        $entity = $this->entityRepo->getById($type, $id);
 | 
			
		||||
        $entity = Entity::getEntityInstance($type)->newQuery()->visible()->find($id);
 | 
			
		||||
        if (!$entity) {
 | 
			
		||||
            return $this->jsonError(trans('errors.entity_not_found'), 404);
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -132,12 +109,12 @@ class SearchController extends Controller
 | 
			
		|||
 | 
			
		||||
        // Page in chapter
 | 
			
		||||
        if ($entity->isA('page') && $entity->chapter) {
 | 
			
		||||
            $entities = $this->entityRepo->getChapterChildren($entity->chapter);
 | 
			
		||||
            $entities = $entity->chapter->visiblePages();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Page in book or chapter
 | 
			
		||||
        if (($entity->isA('page') && !$entity->chapter) || $entity->isA('chapter')) {
 | 
			
		||||
            $entities = $this->entityRepo->getBookDirectChildren($entity->book);
 | 
			
		||||
            $entities = $entity->book->getDirectChildren();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Book
 | 
			
		||||
| 
						 | 
				
			
			@ -145,15 +122,15 @@ class SearchController extends Controller
 | 
			
		|||
        if ($entity->isA('book')) {
 | 
			
		||||
            $contextShelf = $this->entityContextManager->getContextualShelfForBook($entity);
 | 
			
		||||
            if ($contextShelf) {
 | 
			
		||||
                $entities = $this->entityRepo->getBookshelfChildren($contextShelf);
 | 
			
		||||
                $entities = $contextShelf->visibleBooks()->get();
 | 
			
		||||
            } else {
 | 
			
		||||
                $entities = $this->entityRepo->getAll('book');
 | 
			
		||||
                $entities = Book::visible()->get();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Shelve
 | 
			
		||||
        if ($entity->isA('bookshelf')) {
 | 
			
		||||
            $entities = $this->entityRepo->getAll('bookshelf');
 | 
			
		||||
            $entities = Bookshelf::visible()->get();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return view('partials.entity-list-basic', ['entities' => $entities, 'style' => 'compact']);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -76,7 +76,7 @@ class SettingController extends Controller
 | 
			
		|||
            setting()->remove('app-logo');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->showSuccessNotification( trans('settings.settings_save_success'));
 | 
			
		||||
        $this->showSuccessNotification(trans('settings.settings_save_success'));
 | 
			
		||||
        return redirect('/settings');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -111,14 +111,14 @@ class SettingController extends Controller
 | 
			
		|||
        $imagesToDelete = $imageService->deleteUnusedImages($checkRevisions, $dryRun);
 | 
			
		||||
        $deleteCount = count($imagesToDelete);
 | 
			
		||||
        if ($deleteCount === 0) {
 | 
			
		||||
            $this->showWarningNotification( trans('settings.maint_image_cleanup_nothing_found'));
 | 
			
		||||
            $this->showWarningNotification(trans('settings.maint_image_cleanup_nothing_found'));
 | 
			
		||||
            return redirect('/settings/maintenance')->withInput();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($dryRun) {
 | 
			
		||||
            session()->flash('cleanup-images-warning', trans('settings.maint_image_cleanup_warning', ['count' => $deleteCount]));
 | 
			
		||||
        } else {
 | 
			
		||||
            $this->showSuccessNotification( trans('settings.maint_image_cleanup_success', ['count' => $deleteCount]));
 | 
			
		||||
            $this->showSuccessNotification(trans('settings.maint_image_cleanup_success', ['count' => $deleteCount]));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return redirect('/settings/maintenance#image-cleanup')->withInput();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -202,7 +202,7 @@ class UserController extends Controller
 | 
			
		|||
        }
 | 
			
		||||
 | 
			
		||||
        $user->save();
 | 
			
		||||
        $this->showSuccessNotification( trans('settings.users_edit_success'));
 | 
			
		||||
        $this->showSuccessNotification(trans('settings.users_edit_success'));
 | 
			
		||||
 | 
			
		||||
        $redirectUrl = userCan('users-manage') ? '/settings/users' : ('/settings/users/' . $user->id);
 | 
			
		||||
        return redirect($redirectUrl);
 | 
			
		||||
| 
						 | 
				
			
			@ -236,17 +236,17 @@ class UserController extends Controller
 | 
			
		|||
        $user = $this->userRepo->getById($id);
 | 
			
		||||
 | 
			
		||||
        if ($this->userRepo->isOnlyAdmin($user)) {
 | 
			
		||||
            $this->showErrorNotification( trans('errors.users_cannot_delete_only_admin'));
 | 
			
		||||
            $this->showErrorNotification(trans('errors.users_cannot_delete_only_admin'));
 | 
			
		||||
            return redirect($user->getEditUrl());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($user->system_name === 'public') {
 | 
			
		||||
            $this->showErrorNotification( trans('errors.users_cannot_delete_guest'));
 | 
			
		||||
            $this->showErrorNotification(trans('errors.users_cannot_delete_guest'));
 | 
			
		||||
            return redirect($user->getEditUrl());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->userRepo->destroy($user);
 | 
			
		||||
        $this->showSuccessNotification( trans('settings.users_delete_success'));
 | 
			
		||||
        $this->showSuccessNotification(trans('settings.users_delete_success'));
 | 
			
		||||
 | 
			
		||||
        return redirect('/settings/users');
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -261,7 +261,7 @@ class UserController extends Controller
 | 
			
		|||
        $user = $this->userRepo->getById($id);
 | 
			
		||||
 | 
			
		||||
        $userActivity = $this->userRepo->getActivity($user);
 | 
			
		||||
        $recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5, 0);
 | 
			
		||||
        $recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5);
 | 
			
		||||
        $assetCounts = $this->userRepo->getAssetCounts($user);
 | 
			
		||||
 | 
			
		||||
        return view('users.profile', [
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,5 +24,4 @@ class GlobalViewData
 | 
			
		|||
 | 
			
		||||
        return $next($request);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -58,7 +58,7 @@
 | 
			
		|||
        <h1 id="{{$bookChild->getType()}}-{{$bookChild->id}}">{{ $bookChild->name }}</h1>
 | 
			
		||||
 | 
			
		||||
        @if($bookChild->isA('chapter'))
 | 
			
		||||
            <p>{{ $bookChild->text }}</p>
 | 
			
		||||
            <p>{{ $bookChild->description }}</p>
 | 
			
		||||
 | 
			
		||||
            @if(count($bookChild->pages) > 0)
 | 
			
		||||
                @foreach($bookChild->pages as $page)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,7 +22,7 @@
 | 
			
		|||
                <div class="card mb-xl">
 | 
			
		||||
                    <h3 class="card-title">{{ trans('entities.pages_popular') }}</h3>
 | 
			
		||||
                    <div class="px-m">
 | 
			
		||||
                        @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, 'page'), 'style' => 'compact'])
 | 
			
		||||
                        @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, ['page']), 'style' => 'compact'])
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -30,7 +30,7 @@
 | 
			
		|||
                <div class="card mb-xl">
 | 
			
		||||
                    <h3 class="card-title">{{ trans('entities.books_popular') }}</h3>
 | 
			
		||||
                    <div class="px-m">
 | 
			
		||||
                        @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, 'book'), 'style' => 'compact'])
 | 
			
		||||
                        @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, ['book']), 'style' => 'compact'])
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -38,7 +38,7 @@
 | 
			
		|||
                <div class="card mb-xl">
 | 
			
		||||
                    <h3 class="card-title">{{ trans('entities.chapters_popular') }}</h3>
 | 
			
		||||
                    <div class="px-m">
 | 
			
		||||
                        @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, 'chapter'), 'style' => 'compact'])
 | 
			
		||||
                        @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, ['chapter']), 'style' => 'compact'])
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,7 +19,7 @@
 | 
			
		|||
                <a href="#" permissions-table-toggle-all class="text-small ml-m text-primary">{{ trans('common.toggle_all') }}</a>
 | 
			
		||||
            </th>
 | 
			
		||||
        </tr>
 | 
			
		||||
        @foreach($roles as $role)
 | 
			
		||||
        @foreach(\BookStack\Auth\Role::restrictable() as $role)
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td width="33%" class="pt-m">
 | 
			
		||||
                    {{ $role->display_name }}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,10 +14,10 @@
 | 
			
		|||
    <div class="form-group">
 | 
			
		||||
        <label for="books">{{ trans('entities.shelves_books') }}</label>
 | 
			
		||||
        <input type="hidden" id="books-input" name="books"
 | 
			
		||||
               value="{{ isset($shelf) ? $shelf->books->implode('id', ',') : '' }}">
 | 
			
		||||
               value="{{ isset($shelf) ? $shelf->visibleBooks->implode('id', ',') : '' }}">
 | 
			
		||||
        <div class="scroll-box" shelf-sort-assigned-books data-instruction="{{ trans('entities.shelves_drag_books') }}">
 | 
			
		||||
            @if (isset($shelfBooks) && count($shelfBooks) > 0)
 | 
			
		||||
                @foreach ($shelfBooks as $book)
 | 
			
		||||
            @if (count($shelf->visibleBooks ?? []) > 0)
 | 
			
		||||
                @foreach ($shelf->visibleBooks as $book)
 | 
			
		||||
                    <div data-id="{{ $book->id }}" class="scroll-box-item">
 | 
			
		||||
                        <a href="{{ $book->getUrl() }}" class="text-book">@icon('book'){{ $book->name }}</a>
 | 
			
		||||
                    </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,9 +12,9 @@
 | 
			
		|||
        <h1 class="break-text">{{$shelf->name}}</h1>
 | 
			
		||||
        <div class="book-content">
 | 
			
		||||
            <p class="text-muted">{!! nl2br(e($shelf->description)) !!}</p>
 | 
			
		||||
            @if(count($books) > 0)
 | 
			
		||||
            @if(count($shelf->visibleBooks) > 0)
 | 
			
		||||
                <div class="entity-list">
 | 
			
		||||
                    @foreach($books as $book)
 | 
			
		||||
                    @foreach($shelf->visibleBooks as $book)
 | 
			
		||||
                        @include('books.list-item', ['book' => $book])
 | 
			
		||||
                    @endforeach
 | 
			
		||||
                </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,9 +9,7 @@ Route::group(['middleware' => 'auth'], function () {
 | 
			
		|||
    Route::get('/uploads/images/{path}', 'Images\ImageController@showImage')
 | 
			
		||||
        ->where('path', '.*$');
 | 
			
		||||
 | 
			
		||||
    Route::group(['prefix' => 'pages'], function() {
 | 
			
		||||
        Route::get('/recently-updated', 'PageController@showRecentlyUpdated');
 | 
			
		||||
    });
 | 
			
		||||
    Route::get('/pages/recently-updated', 'PageController@showRecentlyUpdated');
 | 
			
		||||
 | 
			
		||||
    // Shelves
 | 
			
		||||
    Route::get('/create-shelf', 'BookshelfController@create');
 | 
			
		||||
| 
						 | 
				
			
			@ -40,13 +38,13 @@ Route::group(['middleware' => 'auth'], function () {
 | 
			
		|||
        Route::get('/{slug}/edit', 'BookController@edit');
 | 
			
		||||
        Route::put('/{slug}', 'BookController@update');
 | 
			
		||||
        Route::delete('/{id}', 'BookController@destroy');
 | 
			
		||||
        Route::get('/{slug}/sort-item', 'BookController@sortItem');
 | 
			
		||||
        Route::get('/{slug}/sort-item', 'BookSortController@showItem');
 | 
			
		||||
        Route::get('/{slug}', 'BookController@show');
 | 
			
		||||
        Route::get('/{bookSlug}/permissions', 'BookController@showPermissions');
 | 
			
		||||
        Route::put('/{bookSlug}/permissions', 'BookController@permissions');
 | 
			
		||||
        Route::get('/{slug}/delete', 'BookController@showDelete');
 | 
			
		||||
        Route::get('/{bookSlug}/sort', 'BookController@sort');
 | 
			
		||||
        Route::put('/{bookSlug}/sort', 'BookController@saveSort');
 | 
			
		||||
        Route::get('/{bookSlug}/sort', 'BookSortController@show');
 | 
			
		||||
        Route::put('/{bookSlug}/sort', 'BookSortController@update');
 | 
			
		||||
        Route::get('/{bookSlug}/export/html', 'BookExportController@html');
 | 
			
		||||
        Route::get('/{bookSlug}/export/pdf', 'BookExportController@pdf');
 | 
			
		||||
        Route::get('/{bookSlug}/export/plaintext', 'BookExportController@plainText');
 | 
			
		||||
| 
						 | 
				
			
			@ -74,11 +72,11 @@ Route::group(['middleware' => 'auth'], function () {
 | 
			
		|||
        Route::delete('/{bookSlug}/draft/{pageId}', 'PageController@destroyDraft');
 | 
			
		||||
 | 
			
		||||
        // Revisions
 | 
			
		||||
        Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageController@showRevisions');
 | 
			
		||||
        Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}', 'PageController@showRevision');
 | 
			
		||||
        Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/changes', 'PageController@showRevisionChanges');
 | 
			
		||||
        Route::put('/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', 'PageController@restoreRevision');
 | 
			
		||||
        Route::delete('/{bookSlug}/page/{pageSlug}/revisions/{revId}/delete', 'PageController@destroyRevision');
 | 
			
		||||
        Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageRevisionController@index');
 | 
			
		||||
        Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}', 'PageRevisionController@show');
 | 
			
		||||
        Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/changes', 'PageRevisionController@changes');
 | 
			
		||||
        Route::put('/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', 'PageRevisionController@restore');
 | 
			
		||||
        Route::delete('/{bookSlug}/page/{pageSlug}/revisions/{revId}/delete', 'PageRevisionController@destroy');
 | 
			
		||||
 | 
			
		||||
        // Chapters
 | 
			
		||||
        Route::get('/{bookSlug}/chapter/{chapterSlug}/create-page', 'PageController@create');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,6 @@
 | 
			
		|||
 | 
			
		||||
use BookStack\Auth\Permissions\JointPermission;
 | 
			
		||||
use BookStack\Entities\Page;
 | 
			
		||||
use BookStack\Entities\Repos\EntityRepo;
 | 
			
		||||
use BookStack\Auth\User;
 | 
			
		||||
use BookStack\Entities\Repos\PageRepo;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -56,7 +55,7 @@ class CommandsTest extends TestCase
 | 
			
		|||
        $this->asEditor();
 | 
			
		||||
        $pageRepo = app(PageRepo::class);
 | 
			
		||||
        $page = Page::first();
 | 
			
		||||
        $pageRepo->updatePage($page, $page->book_id, ['name' => 'updated page', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
 | 
			
		||||
        $pageRepo->update($page, ['name' => 'updated page', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
 | 
			
		||||
        $pageRepo->updatePageDraft($page, ['name' => 'updated page', 'html' => '<p>new content in draft</p>', 'summary' => 'page revision testing']);
 | 
			
		||||
 | 
			
		||||
        $this->assertDatabaseHas('page_revisions', [
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,6 @@
 | 
			
		|||
use BookStack\Entities\Book;
 | 
			
		||||
use BookStack\Entities\Chapter;
 | 
			
		||||
use BookStack\Entities\Page;
 | 
			
		||||
use BookStack\Entities\Repos\EntityRepo;
 | 
			
		||||
use BookStack\Auth\UserRepo;
 | 
			
		||||
use BookStack\Entities\Repos\PageRepo;
 | 
			
		||||
use Carbon\Carbon;
 | 
			
		||||
| 
						 | 
				
			
			@ -192,7 +191,7 @@ class EntityTest extends BrowserKitTest
 | 
			
		|||
        $entities = $this->createEntityChainBelongingToUser($creator, $updater);
 | 
			
		||||
        $this->actingAs($creator);
 | 
			
		||||
        app(UserRepo::class)->destroy($creator);
 | 
			
		||||
        app(PageRepo::class)->savePageRevision($entities['page']);
 | 
			
		||||
        app(PageRepo::class)->update($entities['page'], ['html' => '<p>hello!</p>>']);
 | 
			
		||||
 | 
			
		||||
        $this->checkEntitiesViewable($entities);
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -205,7 +204,7 @@ class EntityTest extends BrowserKitTest
 | 
			
		|||
        $entities = $this->createEntityChainBelongingToUser($creator, $updater);
 | 
			
		||||
        $this->actingAs($updater);
 | 
			
		||||
        app(UserRepo::class)->destroy($updater);
 | 
			
		||||
        app(PageRepo::class)->savePageRevision($entities['page']);
 | 
			
		||||
        app(PageRepo::class)->update($entities['page'], ['html' => '<p>Hello there!</p>']);
 | 
			
		||||
 | 
			
		||||
        $this->checkEntitiesViewable($entities);
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -273,8 +272,7 @@ class EntityTest extends BrowserKitTest
 | 
			
		|||
 | 
			
		||||
    public function test_slug_multi_byte_lower_casing()
 | 
			
		||||
    {
 | 
			
		||||
        $entityRepo = app(EntityRepo::class);
 | 
			
		||||
        $book = $entityRepo->createFromInput('book', [
 | 
			
		||||
        $book = $this->newBook([
 | 
			
		||||
            'name' => 'КНИГА'
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -284,8 +282,7 @@ class EntityTest extends BrowserKitTest
 | 
			
		|||
 | 
			
		||||
    public function test_slug_format()
 | 
			
		||||
    {
 | 
			
		||||
        $entityRepo = app(EntityRepo::class);
 | 
			
		||||
        $book = $entityRepo->createFromInput('book', [
 | 
			
		||||
        $book = $this->newBook([
 | 
			
		||||
            'name' => 'PartA / PartB / PartC'
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,7 @@
 | 
			
		|||
<?php namespace Tests;
 | 
			
		||||
 | 
			
		||||
use BookStack\Entities\Managers\PageContent;
 | 
			
		||||
use BookStack\Entities\Page;
 | 
			
		||||
use BookStack\Entities\Repos\EntityRepo;
 | 
			
		||||
use BookStack\Entities\Repos\PageRepo;
 | 
			
		||||
 | 
			
		||||
class PageContentTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
| 
						 | 
				
			
			@ -242,4 +241,66 @@ class PageContentTest extends TestCase
 | 
			
		|||
        $updatedPage = Page::where('id', '=', $page->id)->first();
 | 
			
		||||
        $this->assertEquals(substr_count($updatedPage->html, "bkmrk-test\""), 1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_get_page_nav_sets_correct_properties()
 | 
			
		||||
    {
 | 
			
		||||
        $content = '<h1 id="testa">Hello</h1><h2 id="testb">There</h2><h3 id="testc">Donkey</h3>';
 | 
			
		||||
        $pageContent = new PageContent(new Page(['html' => $content]));
 | 
			
		||||
        $navMap = $pageContent->getNavigation($content);
 | 
			
		||||
 | 
			
		||||
        $this->assertCount(3, $navMap);
 | 
			
		||||
        $this->assertArrayMapIncludes([
 | 
			
		||||
            'nodeName' => 'h1',
 | 
			
		||||
            'link' => '#testa',
 | 
			
		||||
            'text' => 'Hello',
 | 
			
		||||
            'level' => 1,
 | 
			
		||||
        ], $navMap[0]);
 | 
			
		||||
        $this->assertArrayMapIncludes([
 | 
			
		||||
            'nodeName' => 'h2',
 | 
			
		||||
            'link' => '#testb',
 | 
			
		||||
            'text' => 'There',
 | 
			
		||||
            'level' => 2,
 | 
			
		||||
        ], $navMap[1]);
 | 
			
		||||
        $this->assertArrayMapIncludes([
 | 
			
		||||
            'nodeName' => 'h3',
 | 
			
		||||
            'link' => '#testc',
 | 
			
		||||
            'text' => 'Donkey',
 | 
			
		||||
            'level' => 3,
 | 
			
		||||
        ], $navMap[2]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_get_page_nav_does_not_show_empty_titles()
 | 
			
		||||
    {
 | 
			
		||||
        $content = '<h1 id="testa">Hello</h1><h2 id="testb"> </h2><h3 id="testc"></h3>';
 | 
			
		||||
        $pageContent = new PageContent(new Page(['html' => $content]));
 | 
			
		||||
        $navMap = $pageContent->getNavigation($content);
 | 
			
		||||
 | 
			
		||||
        $this->assertCount(1, $navMap);
 | 
			
		||||
        $this->assertArrayMapIncludes([
 | 
			
		||||
            'nodeName' => 'h1',
 | 
			
		||||
            'link' => '#testa',
 | 
			
		||||
            'text' => 'Hello'
 | 
			
		||||
        ], $navMap[0]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_get_page_nav_shifts_headers_if_only_smaller_ones_are_used()
 | 
			
		||||
    {
 | 
			
		||||
        $content = '<h4 id="testa">Hello</h4><h5 id="testb">There</h5><h6 id="testc">Donkey</h6>';
 | 
			
		||||
        $pageContent = new PageContent(new Page(['html' => $content]));
 | 
			
		||||
        $navMap = $pageContent->getNavigation($content);
 | 
			
		||||
 | 
			
		||||
        $this->assertCount(3, $navMap);
 | 
			
		||||
        $this->assertArrayMapIncludes([
 | 
			
		||||
            'nodeName' => 'h4',
 | 
			
		||||
            'level' => 1,
 | 
			
		||||
        ], $navMap[0]);
 | 
			
		||||
        $this->assertArrayMapIncludes([
 | 
			
		||||
            'nodeName' => 'h5',
 | 
			
		||||
            'level' => 2,
 | 
			
		||||
        ], $navMap[1]);
 | 
			
		||||
        $this->assertArrayMapIncludes([
 | 
			
		||||
            'nodeName' => 'h6',
 | 
			
		||||
            'level' => 3,
 | 
			
		||||
        ], $navMap[2]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,14 @@
 | 
			
		|||
<?php namespace Tests;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
use BookStack\Entities\Repos\PageRepo;
 | 
			
		||||
 | 
			
		||||
class PageDraftTest extends BrowserKitTest
 | 
			
		||||
{
 | 
			
		||||
    protected $page;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var PageRepo
 | 
			
		||||
     */
 | 
			
		||||
    protected $pageRepo;
 | 
			
		||||
 | 
			
		||||
    public function setUp(): void
 | 
			
		||||
| 
						 | 
				
			
			@ -85,11 +88,11 @@ class PageDraftTest extends BrowserKitTest
 | 
			
		|||
        $newUser = $this->getEditor();
 | 
			
		||||
 | 
			
		||||
        $this->actingAs($newUser)->visit('/')
 | 
			
		||||
            ->visit($book->getUrl() . '/create-page')
 | 
			
		||||
            ->visit($chapter->getUrl() . '/create-page')
 | 
			
		||||
            ->visit($book->getUrl('/create-page'))
 | 
			
		||||
            ->visit($chapter->getUrl('/create-page'))
 | 
			
		||||
            ->visit($book->getUrl())
 | 
			
		||||
            ->seeInElement('.book-contents', 'New Page');
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        $this->asAdmin()
 | 
			
		||||
            ->visit($book->getUrl())
 | 
			
		||||
            ->dontSeeInElement('.book-contents', 'New Page')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,7 +12,7 @@ class PageRevisionTest extends TestCase
 | 
			
		|||
 | 
			
		||||
        $pageRepo = app(PageRepo::class);
 | 
			
		||||
        $page = Page::first();
 | 
			
		||||
        $pageRepo->updatePage($page, $page->book_id, ['name' => 'updated page', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
 | 
			
		||||
        $pageRepo->update($page, ['name' => 'updated page', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
 | 
			
		||||
        $pageRevision = $page->revisions->last();
 | 
			
		||||
 | 
			
		||||
        $revisionView = $this->get($page->getUrl() . '/revisions/' . $pageRevision->id);
 | 
			
		||||
| 
						 | 
				
			
			@ -30,8 +30,8 @@ class PageRevisionTest extends TestCase
 | 
			
		|||
 | 
			
		||||
        $pageRepo = app(PageRepo::class);
 | 
			
		||||
        $page = Page::first();
 | 
			
		||||
        $pageRepo->updatePage($page, $page->book_id, ['name' => 'updated page abc123', 'html' => '<p>new contente def456</p>', 'summary' => 'initial page revision testing']);
 | 
			
		||||
        $pageRepo->updatePage($page, $page->book_id, ['name' => 'updated page again', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
 | 
			
		||||
        $pageRepo->update($page, ['name' => 'updated page abc123', 'html' => '<p>new contente def456</p>', 'summary' => 'initial page revision testing']);
 | 
			
		||||
        $pageRepo->update($page, ['name' => 'updated page again', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
 | 
			
		||||
        $page =  Page::find($page->id);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -98,7 +98,7 @@ class PageRevisionTest extends TestCase
 | 
			
		|||
        $beforeRevisionCount = $page->revisions->count();
 | 
			
		||||
        $currentRevision = $page->getCurrentRevision();
 | 
			
		||||
        $resp = $this->asEditor()->delete($currentRevision->getUrl('/delete/'));
 | 
			
		||||
        $resp->assertStatus(400);
 | 
			
		||||
        $resp->assertRedirect($page->getUrl('/revisions'));
 | 
			
		||||
 | 
			
		||||
        $page = Page::find($page->id);
 | 
			
		||||
        $afterRevisionCount = $page->revisions->count();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,5 @@
 | 
			
		|||
<?php namespace Tests;
 | 
			
		||||
 | 
			
		||||
use BookStack\Auth\Role;
 | 
			
		||||
use BookStack\Entities\Book;
 | 
			
		||||
use BookStack\Entities\Chapter;
 | 
			
		||||
use BookStack\Entities\Page;
 | 
			
		||||
| 
						 | 
				
			
			@ -20,7 +19,7 @@ class SortTest extends TestCase
 | 
			
		|||
    {
 | 
			
		||||
        $this->asAdmin();
 | 
			
		||||
        $pageRepo = app(PageRepo::class);
 | 
			
		||||
        $draft = $pageRepo->getDraftPage($this->book);
 | 
			
		||||
        $draft = $pageRepo->getNewDraftPage($this->book);
 | 
			
		||||
 | 
			
		||||
        $resp = $this->get($this->book->getUrl());
 | 
			
		||||
        $resp->assertSee($draft->name);
 | 
			
		||||
| 
						 | 
				
			
			@ -214,7 +213,6 @@ class SortTest extends TestCase
 | 
			
		|||
            'entity_selection' => 'book:' . $newBook->id,
 | 
			
		||||
            'name' => 'My copied test page'
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $pageCopy = Page::where('name', '=', 'My copied test page')->first();
 | 
			
		||||
 | 
			
		||||
        $movePageResp->assertRedirect($pageCopy->getUrl());
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,14 +5,13 @@ use BookStack\Entities\Bookshelf;
 | 
			
		|||
use BookStack\Entities\Chapter;
 | 
			
		||||
use BookStack\Entities\Entity;
 | 
			
		||||
use BookStack\Auth\User;
 | 
			
		||||
use BookStack\Entities\Repos\EntityRepo;
 | 
			
		||||
use BookStack\Entities\Page;
 | 
			
		||||
 | 
			
		||||
class RestrictionsTest extends BrowserKitTest
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var \BookStack\Auth\User
 | 
			
		||||
     * @var User
 | 
			
		||||
     */
 | 
			
		||||
    protected $user;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -327,7 +326,7 @@ class RestrictionsTest extends BrowserKitTest
 | 
			
		|||
 | 
			
		||||
    public function test_page_view_restriction()
 | 
			
		||||
    {
 | 
			
		||||
        $page = \BookStack\Entities\Page::first();
 | 
			
		||||
        $page = Page::first();
 | 
			
		||||
 | 
			
		||||
        $pageUrl = $page->getUrl();
 | 
			
		||||
        $this->actingAs($this->user)
 | 
			
		||||
| 
						 | 
				
			
			@ -367,7 +366,7 @@ class RestrictionsTest extends BrowserKitTest
 | 
			
		|||
 | 
			
		||||
    public function test_page_delete_restriction()
 | 
			
		||||
    {
 | 
			
		||||
        $page = \BookStack\Entities\Page::first();
 | 
			
		||||
        $page = Page::first();
 | 
			
		||||
 | 
			
		||||
        $pageUrl = $page->getUrl();
 | 
			
		||||
        $this->actingAs($this->user)
 | 
			
		||||
| 
						 | 
				
			
			@ -438,7 +437,7 @@ class RestrictionsTest extends BrowserKitTest
 | 
			
		|||
 | 
			
		||||
    public function test_page_restriction_form()
 | 
			
		||||
    {
 | 
			
		||||
        $page = \BookStack\Entities\Page::first();
 | 
			
		||||
        $page = Page::first();
 | 
			
		||||
        $this->asAdmin()->visit($page->getUrl() . '/permissions')
 | 
			
		||||
            ->see('Page Permissions')
 | 
			
		||||
            ->check('restricted')
 | 
			
		||||
| 
						 | 
				
			
			@ -665,10 +664,8 @@ class RestrictionsTest extends BrowserKitTest
 | 
			
		|||
        $this->setEntityRestrictions($firstBook, ['view', 'update']);
 | 
			
		||||
        $this->setEntityRestrictions($secondBook, ['view']);
 | 
			
		||||
 | 
			
		||||
        $firstBookChapter = $this->app[EntityRepo::class]->createFromInput('chapter',
 | 
			
		||||
                ['name' => 'first book chapter'], $firstBook);
 | 
			
		||||
        $secondBookChapter = $this->app[EntityRepo::class]->createFromInput('chapter',
 | 
			
		||||
                ['name' => 'second book chapter'], $secondBook);
 | 
			
		||||
        $firstBookChapter = $this->newChapter(['name' => 'first book chapter'], $firstBook);
 | 
			
		||||
        $secondBookChapter = $this->newChapter(['name' => 'second book chapter'], $secondBook);
 | 
			
		||||
 | 
			
		||||
        // Create request data
 | 
			
		||||
        $reqData = [
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,14 @@
 | 
			
		|||
<?php namespace Tests;
 | 
			
		||||
 | 
			
		||||
use BookStack\Auth\User;
 | 
			
		||||
use BookStack\Entities\Book;
 | 
			
		||||
use BookStack\Entities\Bookshelf;
 | 
			
		||||
use BookStack\Entities\Chapter;
 | 
			
		||||
use BookStack\Entities\Entity;
 | 
			
		||||
use BookStack\Entities\Page;
 | 
			
		||||
use BookStack\Entities\Repos\EntityRepo;
 | 
			
		||||
use BookStack\Entities\Repos\BookRepo;
 | 
			
		||||
use BookStack\Entities\Repos\BookshelfRepo;
 | 
			
		||||
use BookStack\Entities\Repos\ChapterRepo;
 | 
			
		||||
use BookStack\Auth\Permissions\PermissionsRepo;
 | 
			
		||||
use BookStack\Auth\Role;
 | 
			
		||||
use BookStack\Auth\Permissions\PermissionService;
 | 
			
		||||
| 
						 | 
				
			
			@ -11,6 +16,8 @@ use BookStack\Entities\Repos\PageRepo;
 | 
			
		|||
use BookStack\Settings\SettingService;
 | 
			
		||||
use BookStack\Uploads\HttpFetcher;
 | 
			
		||||
use Illuminate\Support\Env;
 | 
			
		||||
use Mockery;
 | 
			
		||||
use Throwable;
 | 
			
		||||
 | 
			
		||||
trait SharedTestHelpers
 | 
			
		||||
{
 | 
			
		||||
| 
						 | 
				
			
			@ -68,7 +75,7 @@ trait SharedTestHelpers
 | 
			
		|||
     */
 | 
			
		||||
    protected function getViewer($attributes = [])
 | 
			
		||||
    {
 | 
			
		||||
        $user = \BookStack\Auth\Role::getRole('viewer')->users()->first();
 | 
			
		||||
        $user = Role::getRole('viewer')->users()->first();
 | 
			
		||||
        if (!empty($attributes)) $user->forceFill($attributes)->save();
 | 
			
		||||
        return $user;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -76,7 +83,7 @@ trait SharedTestHelpers
 | 
			
		|||
    /**
 | 
			
		||||
     * Regenerate the permission for an entity.
 | 
			
		||||
     * @param Entity $entity
 | 
			
		||||
     * @throws \Throwable
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    protected function regenEntityPermissions(Entity $entity)
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -87,10 +94,10 @@ trait SharedTestHelpers
 | 
			
		|||
    /**
 | 
			
		||||
     * Create and return a new bookshelf.
 | 
			
		||||
     * @param array $input
 | 
			
		||||
     * @return \BookStack\Entities\Bookshelf
 | 
			
		||||
     * @return Bookshelf
 | 
			
		||||
     */
 | 
			
		||||
    public function newShelf($input = ['name' => 'test shelf', 'description' => 'My new test shelf']) {
 | 
			
		||||
        return app(EntityRepo::class)->createFromInput('bookshelf', $input);
 | 
			
		||||
        return app(BookshelfRepo::class)->create($input, []);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			@ -99,30 +106,30 @@ trait SharedTestHelpers
 | 
			
		|||
     * @return Book
 | 
			
		||||
     */
 | 
			
		||||
    public function newBook($input = ['name' => 'test book', 'description' => 'My new test book']) {
 | 
			
		||||
        return app(EntityRepo::class)->createFromInput('book', $input);
 | 
			
		||||
        return app(BookRepo::class)->create($input);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create and return a new test chapter
 | 
			
		||||
     * @param array $input
 | 
			
		||||
     * @param Book $book
 | 
			
		||||
     * @return \BookStack\Entities\Chapter
 | 
			
		||||
     * @return Chapter
 | 
			
		||||
     */
 | 
			
		||||
    public function newChapter($input = ['name' => 'test chapter', 'description' => 'My new test chapter'], Book $book) {
 | 
			
		||||
        return app(EntityRepo::class)->createFromInput('chapter', $input, $book);
 | 
			
		||||
        return app(ChapterRepo::class)->create($input, $book);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create and return a new test page
 | 
			
		||||
     * @param array $input
 | 
			
		||||
     * @return Page
 | 
			
		||||
     * @throws \Throwable
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function newPage($input = ['name' => 'test page', 'html' => 'My new test page']) {
 | 
			
		||||
        $book = Book::first();
 | 
			
		||||
        $pageRepo = app(PageRepo::class);
 | 
			
		||||
        $draftPage = $pageRepo->getDraftPage($book);
 | 
			
		||||
        return $pageRepo->publishPageDraft($draftPage, $input);
 | 
			
		||||
        $draftPage = $pageRepo->getNewDraftPage($book);
 | 
			
		||||
        return $pageRepo->publishDraft($draftPage, $input);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			@ -167,10 +174,10 @@ trait SharedTestHelpers
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Give the given user some permissions.
 | 
			
		||||
     * @param \BookStack\Auth\User $user
 | 
			
		||||
     * @param User $user
 | 
			
		||||
     * @param array $permissions
 | 
			
		||||
     */
 | 
			
		||||
    protected function giveUserPermissions(\BookStack\Auth\User $user, $permissions = [])
 | 
			
		||||
    protected function giveUserPermissions(User $user, $permissions = [])
 | 
			
		||||
    {
 | 
			
		||||
        $newRole = $this->createNewRole($permissions);
 | 
			
		||||
        $user->attachRole($newRole);
 | 
			
		||||
| 
						 | 
				
			
			@ -198,7 +205,7 @@ trait SharedTestHelpers
 | 
			
		|||
     */
 | 
			
		||||
    protected function mockHttpFetch($returnData, int $times = 1)
 | 
			
		||||
    {
 | 
			
		||||
        $mockHttp = \Mockery::mock(HttpFetcher::class);
 | 
			
		||||
        $mockHttp = Mockery::mock(HttpFetcher::class);
 | 
			
		||||
        $this->app[HttpFetcher::class] = $mockHttp;
 | 
			
		||||
        $mockHttp->shouldReceive('fetch')
 | 
			
		||||
            ->times($times)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,78 +0,0 @@
 | 
			
		|||
<?php
 | 
			
		||||
namespace Tests;
 | 
			
		||||
 | 
			
		||||
use BookStack\Entities\Repos\PageRepo;
 | 
			
		||||
 | 
			
		||||
class PageRepoTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @var PageRepo $pageRepo
 | 
			
		||||
     */
 | 
			
		||||
    protected $pageRepo;
 | 
			
		||||
 | 
			
		||||
    protected function setUp(): void
 | 
			
		||||
    {
 | 
			
		||||
        parent::setUp();
 | 
			
		||||
        $this->pageRepo = app()->make(PageRepo::class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_get_page_nav_sets_correct_properties()
 | 
			
		||||
    {
 | 
			
		||||
        $content = '<h1 id="testa">Hello</h1><h2 id="testb">There</h2><h3 id="testc">Donkey</h3>';
 | 
			
		||||
        $navMap = $this->pageRepo->getPageNav($content);
 | 
			
		||||
 | 
			
		||||
        $this->assertCount(3, $navMap);
 | 
			
		||||
        $this->assertArrayMapIncludes([
 | 
			
		||||
            'nodeName' => 'h1',
 | 
			
		||||
            'link' => '#testa',
 | 
			
		||||
            'text' => 'Hello',
 | 
			
		||||
            'level' => 1,
 | 
			
		||||
        ], $navMap[0]);
 | 
			
		||||
        $this->assertArrayMapIncludes([
 | 
			
		||||
            'nodeName' => 'h2',
 | 
			
		||||
            'link' => '#testb',
 | 
			
		||||
            'text' => 'There',
 | 
			
		||||
            'level' => 2,
 | 
			
		||||
        ], $navMap[1]);
 | 
			
		||||
        $this->assertArrayMapIncludes([
 | 
			
		||||
            'nodeName' => 'h3',
 | 
			
		||||
            'link' => '#testc',
 | 
			
		||||
            'text' => 'Donkey',
 | 
			
		||||
            'level' => 3,
 | 
			
		||||
        ], $navMap[2]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_get_page_nav_does_not_show_empty_titles()
 | 
			
		||||
    {
 | 
			
		||||
        $content = '<h1 id="testa">Hello</h1><h2 id="testb"> </h2><h3 id="testc"></h3>';
 | 
			
		||||
        $navMap = $this->pageRepo->getPageNav($content);
 | 
			
		||||
 | 
			
		||||
        $this->assertCount(1, $navMap);
 | 
			
		||||
        $this->assertArrayMapIncludes([
 | 
			
		||||
            'nodeName' => 'h1',
 | 
			
		||||
            'link' => '#testa',
 | 
			
		||||
            'text' => 'Hello'
 | 
			
		||||
        ], $navMap[0]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_get_page_nav_shifts_headers_if_only_smaller_ones_are_used()
 | 
			
		||||
    {
 | 
			
		||||
        $content = '<h4 id="testa">Hello</h4><h5 id="testb">There</h5><h6 id="testc">Donkey</h6>';
 | 
			
		||||
        $navMap = $this->pageRepo->getPageNav($content);
 | 
			
		||||
 | 
			
		||||
        $this->assertCount(3, $navMap);
 | 
			
		||||
        $this->assertArrayMapIncludes([
 | 
			
		||||
            'nodeName' => 'h4',
 | 
			
		||||
            'level' => 1,
 | 
			
		||||
        ], $navMap[0]);
 | 
			
		||||
        $this->assertArrayMapIncludes([
 | 
			
		||||
            'nodeName' => 'h5',
 | 
			
		||||
            'level' => 2,
 | 
			
		||||
        ], $navMap[1]);
 | 
			
		||||
        $this->assertArrayMapIncludes([
 | 
			
		||||
            'nodeName' => 'h6',
 | 
			
		||||
            'level' => 3,
 | 
			
		||||
        ], $navMap[2]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -223,7 +223,7 @@ class AttachmentTest extends TestCase
 | 
			
		|||
    {
 | 
			
		||||
        $admin = $this->getAdmin();
 | 
			
		||||
        $viewer = $this->getViewer();
 | 
			
		||||
        $page = Page::first();
 | 
			
		||||
        $page = Page::first(); /** @var Page $page */
 | 
			
		||||
 | 
			
		||||
        $this->actingAs($admin);
 | 
			
		||||
        $fileName = 'permission_test.txt';
 | 
			
		||||
| 
						 | 
				
			
			@ -233,7 +233,7 @@ class AttachmentTest extends TestCase
 | 
			
		|||
        $page->restricted = true;
 | 
			
		||||
        $page->permissions()->delete();
 | 
			
		||||
        $page->save();
 | 
			
		||||
        $this->app[PermissionService::class]->buildJointPermissionsForEntity($page);
 | 
			
		||||
        $page->rebuildPermissions();
 | 
			
		||||
        $page->load('jointPermissions');
 | 
			
		||||
 | 
			
		||||
        $this->actingAs($viewer);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -367,7 +367,7 @@ class ImageTest extends TestCase
 | 
			
		|||
        $image = Image::where('type', '=', 'gallery')->first();
 | 
			
		||||
 | 
			
		||||
        $pageRepo = app(PageRepo::class);
 | 
			
		||||
        $pageRepo->updatePage($page, $page->book_id, [
 | 
			
		||||
        $pageRepo->update($page, [
 | 
			
		||||
            'name' => $page->name,
 | 
			
		||||
            'html' => $page->html . "<img src=\"{$image->url}\">",
 | 
			
		||||
            'summary' => ''
 | 
			
		||||
| 
						 | 
				
			
			@ -379,7 +379,7 @@ class ImageTest extends TestCase
 | 
			
		|||
        $this->assertCount(0, $toDelete);
 | 
			
		||||
 | 
			
		||||
        // Save a revision of our page without the image;
 | 
			
		||||
        $pageRepo->updatePage($page, $page->book_id, [
 | 
			
		||||
        $pageRepo->update($page, [
 | 
			
		||||
            'name' => $page->name,
 | 
			
		||||
            'html' => "<p>Hello</p>",
 | 
			
		||||
            'summary' => ''
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue