namespace BookStack\Actions;
use BookStack\Auth\User;
+use BookStack\Entities\Entity;
use BookStack\Model;
/**
- * @property string key
- * @property \User user
- * @property \Entity entity
- * @property string extra
+ * @property string $key
+ * @property User $user
+ * @property Entity $entity
+ * @property string $extra
+ * @property string $entity_type
+ * @property int $entity_id
+ * @property int $user_id
+ * @property int $book_id
*/
class Activity extends Model
{
<?php namespace BookStack\Actions;
use BookStack\Auth\Permissions\PermissionService;
+use BookStack\Entities\Book;
use BookStack\Entities\Entity;
-use Session;
class ActivityService
{
/**
* ActivityService constructor.
- * @param \BookStack\Actions\Activity $activity
+ * @param Activity $activity
* @param PermissionService $permissionService
*/
public function __construct(Activity $activity, PermissionService $permissionService)
/**
* Add activity data to database.
- * @param Entity $entity
- * @param $activityKey
+ * @param \BookStack\Entities\Entity $entity
+ * @param string $activityKey
* @param int $bookId
- * @param bool $extra
*/
- public function add(Entity $entity, $activityKey, $bookId = 0, $extra = false)
+ public function add(Entity $entity, string $activityKey, int $bookId = null)
{
- $activity = $this->activity->newInstance();
- $activity->user_id = $this->user->id;
- $activity->book_id = $bookId;
- $activity->key = strtolower($activityKey);
- if ($extra !== false) {
- $activity->extra = $extra;
- }
+ $activity = $this->newActivityForUser($activityKey, $bookId);
$entity->activity()->save($activity);
$this->setNotification($activityKey);
}
/**
- * Adds a activity history with a message & without binding to a entity.
- * @param $activityKey
+ * Adds a activity history with a message, without binding to a entity.
+ * @param string $activityKey
+ * @param string $message
* @param int $bookId
- * @param bool|false $extra
*/
- public function addMessage($activityKey, $bookId = 0, $extra = false)
+ public function addMessage(string $activityKey, string $message, int $bookId = null)
{
- $this->activity->user_id = $this->user->id;
- $this->activity->book_id = $bookId;
- $this->activity->key = strtolower($activityKey);
- if ($extra !== false) {
- $this->activity->extra = $extra;
- }
- $this->activity->save();
+ $this->newActivityForUser($activityKey, $bookId)->forceFill([
+ 'extra' => $message
+ ])->save();
+
$this->setNotification($activityKey);
}
+ /**
+ * Get a new activity instance for the current user.
+ * @param string $key
+ * @param int|null $bookId
+ * @return Activity
+ */
+ protected function newActivityForUser(string $key, int $bookId = null)
+ {
+ return $this->activity->newInstance()->forceFill([
+ 'key' => strtolower($key),
+ 'user_id' => $this->user->id,
+ 'book_id' => $bookId ?? 0,
+ ]);
+ }
/**
* 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)
{
+ // TODO - Rewrite to db query.
$activities = $entity->activity;
foreach ($activities as $activity) {
$activity->extra = $entity->name;
{
$activityList = $this->permissionService
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
- ->orderBy('created_at', 'desc')->with('user', 'entity')->skip($count * $page)->take($count)->get();
+ ->orderBy('created_at', 'desc')
+ ->with('user', 'entity')
+ ->skip($count * $page)
+ ->take($count)
+ ->get();
return $this->filterSimilar($activityList);
}
/**
* 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
$notificationTextKey = 'activities.' . $activityKey . '_notification';
if (trans()->has($notificationTextKey)) {
$message = trans($notificationTextKey);
- Session::flash('success', $message);
+ session()->flash('success', $message);
}
}
}
<?php namespace BookStack\Actions;
use BookStack\Auth\Permissions\PermissionService;
+use BookStack\Entities\Book;
use BookStack\Entities\Entity;
use BookStack\Entities\EntityProvider;
+use DB;
use Illuminate\Support\Collection;
class ViewService
/**
* ViewService constructor.
- * @param \BookStack\Actions\View $view
- * @param \BookStack\Auth\Permissions\PermissionService $permissionService
+ * @param View $view
+ * @param PermissionService $permissionService
* @param EntityProvider $entityProvider
*/
public function __construct(View $view, PermissionService $permissionService, EntityProvider $entityProvider)
/**
* Add a view to the given entity.
- * @param Entity $entity
+ * @param \BookStack\Entities\Entity $entity
* @return int
*/
public function add(Entity $entity)
}
// Otherwise create new view count
- $entity->views()->save($this->view->create([
+ $entity->views()->save($this->view->newInstance([
'user_id' => $user->id,
'views' => 1
]));
* @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
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type', $action)
- ->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
+ ->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
->groupBy('viewable_id', 'viewable_type')
->orderBy('view_count', 'desc');
* @param Collection $books
* @param array $roles
* @param bool $deleteOld
- * @throws \Throwable
*/
protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false)
{
}
/**
- * 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);
+ $this->clean();
+ 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);
+ });
});
- }
- });
- $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;
+ /**
+ * 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);
+ });
+ });
}
/**
if (strtolower($entityType) === 'page') {
// Prevent drafts being visible to others.
$query = $query->where(function ($query) {
- $query->where('draft', '=', false);
- if ($this->currentUser()) {
- $query->orWhere(function ($query) {
- $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id);
+ $query->where('draft', '=', false)
+ ->orWhere(function ($query) {
+ $query->where('draft', '=', true)
+ ->where('created_by', '=', $this->currentUser()->id);
});
- }
});
}
*/
public static function getRole($roleName)
{
- return static::where('name', '=', $roleName)->first();
+ return static::query()->where('name', '=', $roleName)->first();
}
/**
*/
public static function getSystemRole($roleName)
{
- return static::where('system_name', '=', $roleName)->first();
+ return static::query()->where('system_name', '=', $roleName)->first();
}
/**
*/
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();
}
}
*/
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;
}
/**
<?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;
}
/**
* 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)
{
/**
* Checks if the give user is the only admin.
- * @param \BookStack\Auth\User $user
+ * @param User $user
* @return bool
*/
public function isOnlyAdmin(User $user)
* 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)
{
/**
* 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)
/**
* Get the latest activity for a user.
- * @param \BookStack\Auth\User $user
+ * @param User $user
* @param int $count
* @param int $page
* @return array
/**
* 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(),
];
}
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.
$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;
}
}
'locale' => env('APP_LANG', 'en'),
// Locales available
- 'locales' => ['en', 'ar', 'de', 'de_informal', 'es', 'es_AR', 'fr', 'hu', 'nl', 'pt_BR', 'sk', 'cs', 'sv', 'kr', 'ja', 'pl', 'it', 'ru', 'uk', 'zh_CN', 'zh_TW'],
+ 'locales' => ['en', 'ar', 'de', 'de_informal', 'es', 'es_AR', 'fr', 'hu', 'nl', 'pt_BR', 'sk', 'cs', 'sv', 'kr', 'ja', 'pl', 'it', 'ru', 'uk', 'zh_CN', 'zh_TW', 'tr'],
// Application Fallback Locale
'fallback_locale' => 'en',
'Setting' => BookStack\Facades\Setting::class,
'Views' => BookStack\Facades\Views::class,
'Images' => BookStack\Facades\Images::class,
+ 'Permissions' => BookStack\Facades\Permissions::class,
],
<?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 extends Entity
+/**
+ * Class Book
+ * @property string $description
+ * @property int $image_id
+ * @property Image|null $cover
+ * @package BookStack\Entities
+ */
+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
try {
$cover = $this->cover ? url($this->cover->getThumb($width, $height, false)) : $default;
- } catch (\Exception $err) {
+ } catch (Exception $err) {
$cover = $default;
}
return $cover;
/**
* 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()
{
/**
* Get the direct child pages of this book.
- * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ * @return HasMany
*/
public function directPages()
{
/**
* Get all chapters within this book.
- * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ * @return HasMany
*/
public function chapters()
{
/**
* Get the shelves this book is contained within.
- * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
+ * @return BelongsToMany
*/
public function shelves()
{
}
/**
- * Get an excerpt of this book's description to the specified length or less.
- * @param int $length
- * @return string
+ * Get the direct child items within this book.
+ * @return Collection
*/
- public function getExcerpt(int $length = 100)
+ public function getDirectChildren(): Collection
{
- $description = $this->description;
- return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
+ $pages = $this->directPages()->visible()->get();
+ $chapters = $this->chapters()->visible()->get();
+ return $pages->contact($chapters)->sortBy('priority')->sortByDesc('draft');
}
/**
- * Return a generalised, common raw query that can be 'unioned' across entities.
+ * Get an excerpt of this book's description to the specified length or less.
+ * @param int $length
* @return string
*/
- public function entityRawQuery()
+ public function getExcerpt(int $length = 100)
{
- 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";
+ $description = $this->description;
+ return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
}
}
--- /dev/null
+<?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
+ */
+ public function book(): BelongsTo
+ {
+ 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;
+ }
+}
<?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';
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.
->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
/**
* 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
}
/**
- * Return a generalised, common raw query that can be 'unioned' across entities.
- * @return string
+ * Check if this shelf contains the given book.
+ * @param Book $book
+ * @return bool
*/
- public function entityRawQuery()
+ public function contains(Book $book): bool
{
- 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";
+ return $this->books()->where('id', '=', $book->id)->count() > 0;
}
/**
- * Check if this shelf contains the given book.
+ * Add a book to the end of this shelf.
* @param Book $book
- * @return bool
*/
- public function contains(Book $book): bool
+ public function appendBook(Book $book)
{
- return $this->books()->where('id', '=', $book->id)->count() > 0;
+ if ($this->contains($book)) {
+ return;
+ }
+
+ $maxOrder = $this->books()->max('order');
+ $this->books()->attach($book->id, ['order' => $maxOrder + 1]);
}
}
<?php namespace BookStack\Entities;
+use BookStack\Entities\Managers\EntityContext;
use Illuminate\View\View;
class BreadcrumbsViewComposer
/**
* BreadcrumbsViewComposer constructor.
- * @param EntityContextManager $entityContextManager
+ * @param EntityContext $entityContextManager
*/
- public function __construct(EntityContextManager $entityContextManager)
+ public function __construct(EntityContext $entityContextManager)
{
$this->entityContextManager = $entityContextManager;
}
<?php namespace BookStack\Entities;
-class Chapter extends Entity
+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 book this chapter is within.
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
- */
- public function book()
- {
- return $this->belongsTo(Book::class);
- }
-
/**
* Get the pages that this chapter contains.
* @param string $dir
}
/**
- * Return a generalised, common raw query that can be 'unioned' across entities.
- * @return string
+ * Check if this chapter has any child pages.
+ * @return bool
*/
- public function entityRawQuery()
+ public function hasChildren()
{
- 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";
+ return count($this->pages) > 0;
}
/**
- * Check if this chapter has any child pages.
- * @return bool
+ * Get the visible pages in this chapter.
*/
- public function hasChildren()
+ public function getVisiblePages(): Collection
{
- return count($this->pages) > 0;
+ return $this->pages()->visible()
+ ->orderBy('draft', 'desc')
+ ->orderBy('priority', 'asc')
+ ->get();
}
}
use BookStack\Actions\View;
use BookStack\Auth\Permissions\EntityPermission;
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;
/**
* The base class for book-like items such as pages, chapters & books.
* This is not a database model in itself but extended.
*
- * @property integer $id
+ * @property int $id
* @property string $name
* @property string $slug
* @property Carbon $created_at
* @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
*/
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 scopeVisible(Builder $query)
+ {
+ 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 getMorphClass()
+ public function scopeWithLastView(Builder $query)
{
- return 'BookStack\\Entity';
+ $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]);
}
/**
/**
* Gets the activity objects for this entity.
- * @return \Illuminate\Database\Eloquent\Relations\MorphMany
+ * @return MorphMany
*/
public function activity()
{
- return $this->morphMany(Activity::class, 'entity')->orderBy('created_at', 'desc');
+ return $this->morphMany(Activity::class, 'entity')
+ ->orderBy('created_at', 'desc');
}
/**
return $this->morphMany(View::class, 'viewable');
}
- public function viewCountQuery()
- {
- return $this->views()->selectRaw('viewable_id, sum(views) as view_count')->groupBy('viewable_id');
- }
-
/**
* Get the Tag models that have been user assigned to this entity.
- * @return \Illuminate\Database\Eloquent\Relations\MorphMany
+ * @return MorphMany
*/
public function tags()
{
/**
* Get the related search terms.
- * @return \Illuminate\Database\Eloquent\Relations\MorphMany
+ * @return MorphMany
*/
public function searchTerms()
{
/**
* Get the entity jointPermissions this is connected to.
- * @return \Illuminate\Database\Eloquent\Relations\MorphMany
+ * @return MorphMany
*/
public function jointPermissions()
{
}
/**
- * Return a generalised, common raw query that can be 'unioned' across entities.
+ * Get the url of this entity
+ * @param $path
* @return string
*/
- public function entityRawQuery()
+ public function getUrl($path = '/')
{
- return '';
+ return $path;
}
/**
- * Get the url of this entity
- * @param $path
- * @return string
+ * Rebuild the permissions for this entity.
*/
- public function getUrl($path = '/')
+ public function rebuildPermissions()
{
- return $path;
+ /** @noinspection PhpUnhandledExceptionInspection */
+ 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.
+ */
+ public function refreshSlug(): string
+ {
+ $generator = new SlugGenerator($this);
+ $this->slug = $generator->generate();
+ return $this->slug;
}
}
/**
* EntityProvider constructor.
- * @param Bookshelf $bookshelf
- * @param Book $book
- * @param Chapter $chapter
- * @param Page $page
- * @param PageRevision $pageRevision
*/
public function __construct(
Bookshelf $bookshelf,
/**
* 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,
/**
* 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];
/**
* 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);
<?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();
/**
* 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,
/**
* 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
/**
* 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();
/**
* 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
}
/**
- * 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);
/**
* 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);
/**
* 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";
/**
* 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')) {
--- /dev/null
+<?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;
+}
--- /dev/null
+<?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;
+ }
+}
-<?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;
}
/**
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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);
+ }
+ }
+}
<?php namespace BookStack\Entities;
use BookStack\Uploads\Attachment;
-
-class Page extends Entity
+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'];
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);
}
/**
return $array;
}
- /**
- * Get the book this page sits in.
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
- */
- public function book()
- {
- return $this->belongsTo(Book::class);
- }
-
/**
* 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()
{
*/
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()
{
$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()
{
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'];
/**
* 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;
}
--- /dev/null
+<?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();
+ }
+}
-<?php
-
-
-namespace BookStack\Entities\Repos;
-
+<?php namespace BookStack\Entities\Repos;
+use BookStack\Actions\TagRepo;
use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
+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 getBySlug(string $slug): Book
+ public function __construct(BaseRepo $baseRepo, TagRepo $tagRepo, ImageRepo $imageRepo)
{
- /** @var Book $book */
- $book = $this->getEntityBySlug('book', $slug);
- return $book;
+ $this->baseRepo = $baseRepo;
+ $this->tagRepo = $tagRepo;
+ $this->imageRepo = $imageRepo;
}
/**
- * Append a Book to a BookShelf.
- * @param Bookshelf $shelf
- * @param Book $book
+ * Get all books in a paginated format.
*/
- public function appendBookToShelf(Bookshelf $shelf, Book $book)
+ public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator
{
- if ($shelf->contains($book)) {
- return;
- }
+ return Book::visible()->orderBy($sort, $order)->paginate($count);
+ }
- $maxOrder = $shelf->books()->max('order');
- $shelf->books()->attach($book->id, ['order' => $maxOrder + 1]);
+ /**
+ * 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();
}
/**
- * Destroy the provided book and all its child entities.
- * @param Book $book
- * @throws NotifyException
- * @throws \Throwable
+ * Get the most popular books in the system.
*/
- public function destroyBook(Book $book)
+ public function getPopular(int $count = 20): Collection
{
- foreach ($book->pages as $page) {
- $this->destroyPage($page);
- }
+ return Book::visible()->withViewCount()
+ ->having('view_count', '>', 0)
+ ->orderBy('view_count', 'desc')
+ ->take($count)->get();
+ }
- foreach ($book->chapters as $chapter) {
- $this->destroyChapter($chapter);
+ /**
+ * 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
+ {
+ $book = Book::visible()->where('slug', '=', $slug)->first();
+
+ if ($book === null) {
+ throw new NotFoundException(trans('errors.book_not_found'));
}
- $this->destroyEntityCommonRelations($book);
- $book->delete();
+ return $book;
+ }
+
+ /**
+ * Create a new book in the system
+ */
+ public function create(array $input): Book
+ {
+ $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);
}
-}
\ No newline at end of file
+ /**
+ * 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);
+ }
+}
--- /dev/null
+<?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);
+ }
+}
--- /dev/null
+<?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;
+ }
+}
+++ /dev/null
-<?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\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 DOMNode;
-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;
- }
-
- /**
- * Check if a slug already exists in the database.
- * @param string $type
- * @param string $slug
- * @param bool|integer $currentId
- * @param bool|integer $bookId
- * @return bool
- */
- protected function slugExists($type, $slug, $currentId = false, $bookId = false)
- {
- $query = $this->entityProvider->get($type)->where('slug', '=', $slug);
- if (strtolower($type) === 'page' || strtolower($type) === 'chapter') {
- $query = $query->where('book_id', '=', $bookId);
- }
- if ($currentId) {
- $query = $query->where('id', '!=', $currentId);
- }
- return $query->count() > 0;
- }
-
- /**
- * 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')) {
- foreach ($request->get('restrictions') as $roleId => $restrictions) {
- foreach ($restrictions as $action => $value) {
- $entity->permissions()->create([
- 'role_id' => $roleId,
- 'action' => strtolower($action)
- ]);
- }
- }
- }
-
- $entity->save();
- $this->permissionService->buildJointPermissionsForEntity($entity);
- }
-
-
- /**
- * Create a new entity from request input.
- * Used for books and chapters.
- * @param string $type
- * @param array $input
- * @param Book|null $book
- * @return Entity
- * @throws Throwable
- */
- public function createFromInput(string $type, array $input = [], Book $book = null)
- {
- $entityModel = $this->entityProvider->get($type)->newInstance($input);
- $entityModel->slug = $this->findSuitableSlug($type, $entityModel->name, false, $book ? $book->id : false);
- $entityModel->created_by = user()->id;
- $entityModel->updated_by = user()->id;
-
- if ($book) {
- $entityModel->book_id = $book->id;
- }
-
- $entityModel->save();
-
- if (isset($input['tags'])) {
- $this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
- }
-
- $this->permissionService->buildJointPermissionsForEntity($entityModel);
- $this->searchService->indexEntity($entityModel);
- return $entityModel;
- }
-
- /**
- * Update entity details from request input.
- * Used for books and chapters
- * @param string $type
- * @param Entity $entityModel
- * @param array $input
- * @return Entity
- * @throws Throwable
- */
- public function updateFromInput(string $type, Entity $entityModel, array $input = [])
- {
- if ($entityModel->name !== $input['name']) {
- $entityModel->slug = $this->findSuitableSlug($type, $input['name'], $entityModel->id);
- }
-
- $entityModel->fill($input);
- $entityModel->updated_by = user()->id;
- $entityModel->save();
-
- if (isset($input['tags'])) {
- $this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
- }
-
- $this->permissionService->buildJointPermissionsForEntity($entityModel);
- $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.
- * @param string $type
- * @param integer $newBookId
- * @param Entity $entity
- * @param bool $rebuildPermissions
- * @return Entity
- */
- public function changeBook($type, $newBookId, Entity $entity, $rebuildPermissions = false)
- {
- $entity->book_id = $newBookId;
- // Update related activity
- foreach ($entity->activity as $activity) {
- $activity->book_id = $newBookId;
- $activity->save();
- }
- $entity->slug = $this->findSuitableSlug($type, $entity->name, $entity->id, $newBookId);
- $entity->save();
-
- // Update all child pages if a chapter
- if (strtolower($type) === 'chapter') {
- foreach ($entity->pages as $page) {
- $this->changeBook('page', $newBookId, $page, false);
- }
- }
-
- // Update permissions if applicable
- if ($rebuildPermissions) {
- $entity->load('book');
- $this->permissionService->buildJointPermissionsForEntity($entity->book);
- }
-
- return $entity;
- }
-
- /**
- * Alias method to update the book jointPermissions in the PermissionService.
- * @param Book $book
- */
- public function buildJointPermissionsForBook(Book $book)
- {
- $this->permissionService->buildJointPermissionsForEntity($book);
- }
-
- /**
- * Format a name as a url slug.
- * @param $name
- * @return string
- */
- protected function nameToSlug($name)
- {
- $slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', mb_strtolower($name));
- $slug = preg_replace('/\s{2,}/', ' ', $slug);
- $slug = str_replace(' ', '-', $slug);
- if ($slug === "") {
- $slug = substr(md5(rand(1, 500)), 0, 5);
- }
- return $slug;
- }
-
- /**
- * 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;
-
- foreach ($shelfBooks as $book) {
- if (!userCan('restrictions-manage', $book)) {
- continue;
- }
- $book->permissions()->delete();
- $book->restricted = $bookshelf->restricted;
- $book->permissions()->createMany($shelfPermissions);
- $book->save();
- $this->permissionService->buildJointPermissionsForEntity($book);
- $updatedBookCount++;
- }
-
- return $updatedBookCount;
- }
-}
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
{
- // Hold the old details to compare later
- $oldHtml = $page->html;
- $oldName = $page->name;
+ $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');
- // Prevent slug being updated if no name change
- if ($page->name !== $input['name']) {
- $page->slug = $this->findSuitableSlug('page', $input['name'], $page->id, $book_id);
+ 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();
}
- // Save page tags if present
- if (isset($input['tags'])) {
- $this->tagRepo->saveTagsToEntity($page, $input['tags']);
+ 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;
+
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);
+ $pageContent = new PageContent($page);
+ $pageContent->setNewHTML($input['html']);
+ $page->revision_count++;
+
if (setting('app-editor') !== 'markdown') {
$page->markdown = '';
}
- $page->updated_by = $userId;
- $page->revision_count++;
+
$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;
$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;
$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
+ * Save a page update draft.
*/
- protected function formatHtml(string $htmlText)
+ public function updatePageDraft(Page $page, array $input)
{
- 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);
+ // If the page itself is a draft simply update that
+ if ($page->draft) {
+ $page->fill($input);
+ if (isset($input['html'])) {
+ $content = new PageContent($page);
+ $content->setNewHTML($input['html']);
+ }
+ $page->save();
+ return $page;
}
- // Generate inner html as a string
- $html = '';
- foreach ($childNodes as $childNode) {
- $html .= $doc->saveHTML($childNode);
+ // Otherwise save the data to a revision
+ $draft = $this->getPageRevisionToUpdate($page);
+ $draft->fill($input);
+ if (setting('app-editor') !== 'markdown') {
+ $draft->markdown = '';
}
- return $html;
+ $draft->save();
+ return $draft;
}
/**
- * 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
+ * Destroy a page from the system.
+ * @throws NotifyException
*/
- protected function setUniqueId($element, array &$idMap)
+ public function destroy(Page $page)
{
- 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;
+ $trashCan = new TrashCan();
+ $trashCan->destroyPage($page);
}
/**
- * Get the plain text version of a page's content.
- * @param \BookStack\Entities\Page $page
- * @return string
+ * Restores a revision's content back into a page.
*/
- protected function pageToPlainText(Page $page) : string
+ public function restoreRevision(Page $page, int $revisionId): Page
{
- $html = $this->renderPage($page, true);
- return strip_tags($html);
- }
+ $page->revision_count++;
+ $this->savePageRevision($page);
- /**
- * 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;
+ $revision = $page->revisions()->where('id', '=', $revisionId)->first();
+ $page->fill($revision->toArray());
+ $content = new PageContent($page);
+ $content->setNewHTML($page->html);
$page->updated_by = user()->id;
- $page->draft = true;
-
- if ($chapter) {
- $page->chapter_id = $chapter->id;
- }
+ $page->refreshSlug();
+ $page->save();
- $book->pages()->save($page);
- $page = $this->entityProvider->page->find($page->id);
- $this->permissionService->buildJointPermissionsForEntity($page);
+ $page->indexForSearch();
return $page;
}
/**
- * Save a page update draft.
- * @param Page $page
- * @param array $data
- * @return PageRevision|Page
+ * 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 updatePageDraft(Page $page, array $data = [])
+ public function move(Page $page, string $parentIdentifier): Book
{
- // 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->save();
- return $page;
+ $parent = $this->findParentByIdentifier($parentIdentifier);
+ if ($parent === null) {
+ throw new MoveOperationException('Book or chapter to move page into not found');
}
- // 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';
+ if (!userCan('page-create', $parent)) {
+ throw new PermissionsException('User does not have permission to create a page within the new parent');
}
- $draft->fill($data);
- if (setting('app-editor') !== 'markdown') {
- $draft->markdown = '';
- }
-
- $draft->save();
- return $draft;
+ $page->changeBook($parent instanceof Book ? $parent->id : $parent->book->id);
+ $page->rebuildPermissions();
+ return $parent;
}
/**
- * 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
+ * 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 publishPageDraft(Page $draftPage, array $input)
+ public function copy(Page $page, string $parentIdentifier = null, string $newName = null): Page
{
- $draftPage->fill($input);
-
- // Save page tags if present
- if (isset($input['tags'])) {
- $this->tagRepo->saveTagsToEntity($draftPage, $input['tags']);
+ $parent = $parentIdentifier ? $this->findParentByIdentifier($parentIdentifier) : $page->parent();
+ if ($parent === null) {
+ throw new MoveOperationException('Book or chapter to move page into not found');
}
- if (isset($input['template']) && userCan('templates-manage')) {
- $draftPage->template = ($input['template'] === 'true');
+ if (!userCan('page-create', $parent)) {
+ throw new PermissionsException('User does not have permission to create a page within the new parent');
}
- $draftPage->slug = $this->findSuitableSlug('page', $draftPage->name, false, $draftPage->book->id);
- $draftPage->html = $this->formatHtml($input['html']);
- $draftPage->text = $this->pageToPlainText($draftPage);
- $draftPage->draft = false;
- $draftPage->revision_count = 1;
-
- $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');
- }
+ $copyPage = $this->getNewDraftPage($parent);
+ $pageData = $page->getAttributes();
- /**
- * 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();
- }
+ // Update name
+ if (!empty($newName)) {
+ $pageData['name'] = $newName;
+ }
- /**
- * 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;
+ // Copy tags from previous page if set
+ if ($page->tags) {
+ $pageData['tags'] = [];
+ foreach ($page->tags as $tag) {
+ $pageData['tags'][] = ['name' => $tag->name, 'value' => $tag->value];
+ }
}
- return $message . "\n" . trans('entities.pages_draft_edited_notification');
+
+ return $this->publishDraft($copyPage, $pageData);
}
/**
- * A query to check for active update drafts on a particular page.
- * @param Page $page
- * @param int $minRange
- * @return mixed
+ * Find a page parent entity via a identifier string in the format:
+ * {type}:{id}
+ * Example: (book:5)
+ * @throws MoveOperationException
*/
- protected function activePageEditingQuery(Page $page, int $minRange = null)
+ protected function findParentByIdentifier(string $identifier): ?Entity
{
- $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');
+ $stringExploded = explode(':', $identifier);
+ $entityType = $stringExploded[0];
+ $entityId = intval($stringExploded[1]);
- if ($minRange !== null) {
- $query = $query->where('updated_at', '>=', Carbon::now()->subMinutes($minRange));
+ if ($entityType !== 'book' && $entityType !== 'chapter') {
+ throw new MoveOperationException('Pages can only be in books or chapters');
}
- return $query;
+ $parentClass = $entityType === 'book' ? Book::class : Chapter::class;
+ return $parentClass::visible()->where('id', '=', $entityId)->first();
}
/**
- * 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
+ * Update the permissions of a page.
*/
- public function isPageEditingActive(Page $page, int $minRange = null)
+ public function updatePermissions(Page $page, bool $restricted, Collection $permissions = null)
{
- $draftSearch = $this->activePageEditingQuery($page, $minRange);
- return $draftSearch->count() > 0;
+ $this->baseRepo->updatePermissions($page, $restricted, $permissions);
}
/**
- * Get a notification message concerning the editing activity on a particular page.
- * @param Page $page
- * @param int $minRange
- * @return string
+ * Change the page's parent to the given entity.
*/
- public function getPageEditingActiveMessage(Page $page, int $minRange = null)
+ protected function changeParent(Page $page, Entity $parent)
{
- $pageDraftEdits = $this->activePageEditingQuery($page, $minRange)->get();
+ $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);
+ }
- $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]);
+ $page->load('book');
+ $book->rebuildPermissions();
}
/**
- * Parse the headers on the page to get a navigation menu
- * @param string $pageContent
- * @return array
+ * Get a page revision to update for the given page.
+ * Checks for an existing revisions before providing a fresh one.
*/
- public function getPageNav(string $pageContent)
+ protected function getPageRevisionToUpdate(Page $page): PageRevision
{
- 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 [];
+ $drafts = $this->getUserDraftQuery($page)->get();
+ if ($drafts->count() > 0) {
+ return $drafts->first();
}
- $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();
+ $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;
}
/**
- * Restores a revision's content back into a page.
- * @param Page $page
- * @param Book $book
- * @param int $revisionId
- * @return Page
- * @throws \Exception
+ * Delete old revisions, for the given page, from the system.
*/
- public function restorePageRevision(Page $page, Book $book, int $revisionId)
+ protected function deleteOldRevisions(Page $page)
{
- $page->revision_count++;
- $this->savePageRevision($page);
- $revision = $page->revisions()->where('id', '=', $revisionId)->first();
- $page->fill($revision->toArray());
- $page->slug = $this->findSuitableSlug('page', $page->name, $page->id, $book->id);
- $page->text = $this->pageToPlainText($page);
- $page->updated_by = user()->id;
- $page->save();
- $this->searchService->indexEntity($page);
- return $page;
- }
+ $revisionLimit = config('app.revision_limit');
+ if ($revisionLimit === false) {
+ return;
+ }
- /**
- * Change the page's parent to the given entity.
- * @param Page $page
- * @param Entity $parent
- * @throws \Throwable
- */
- public function changePageParent(Page $page, Entity $parent)
- {
- $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, $page);
+ $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();
}
- $page->load('book');
- $this->permissionService->buildJointPermissionsForEntity($book);
}
/**
- * 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
+ * Get a new priority for a page
*/
- public function copyPage(Page $page, Entity $newParent, string $newName = '')
+ protected function getNewPriority(Page $page): int
{
- $newBook = $newParent->isA('book') ? $newParent : $newParent->book;
- $newChapter = $newParent->isA('chapter') ? $newParent : null;
- $copyPage = $this->getDraftPage($newBook, $newChapter);
- $pageData = $page->getAttributes();
-
- // Update name
- if (!empty($newName)) {
- $pageData['name'] = $newName;
+ if ($page->parent() instanceof Chapter) {
+ $lastPage = $page->parent()->pages('desc')->first();
+ return $lastPage ? $lastPage->priority + 1 : 0;
}
- // Copy tags from previous page if set
- if ($page->tags) {
- $pageData['tags'] = [];
- foreach ($page->tags as $tag) {
- $pageData['tags'][] = ['name' => $tag->name, 'value' => $tag->value];
- }
- }
-
- // Set priority
- if ($newParent->isA('chapter')) {
- $pageData['priority'] = $this->getNewChapterPriority($newParent);
- } else {
- $pageData['priority'] = $this->getNewBookPriority($newParent);
- }
-
- return $this->publishPageDraft($copyPage, $pageData);
+ return (new BookContents($page->book))->getLastPriority() + 1;
}
/**
- * Get pages that have been marked as templates.
- * @param int $count
- * @param int $page
- * @param string $search
- * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
+ * Get the query to find the user's draft copies of the given page.
*/
- public function getPageTemplates(int $count = 10, int $page = 1, string $search = '')
+ protected function getUserDraftQuery(Page $page)
{
- $query = $this->entityQuery('page')
- ->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;
+ return PageRevision::query()->where('created_by', '=', user()->id)
+ ->where('type', 'update_draft')
+ ->where('page_id', '=', $page->id)
+ ->orderBy('created_at', 'desc');
}
}
--- /dev/null
+<?php namespace BookStack\Entities;
+
+class SlugGenerator
+{
+
+ protected $entity;
+
+ /**
+ * SlugGenerator constructor.
+ * @param $entity
+ */
+ public function __construct(Entity $entity)
+ {
+ $this->entity = $entity;
+ }
+
+ /**
+ * Generate a fresh slug for the given entity.
+ * The slug will generated so it does not conflict within the same parent item.
+ */
+ public function generate(): string
+ {
+ $slug = $this->formatNameAsSlug($this->entity->name);
+ while ($this->slugInUse($slug)) {
+ $slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
+ }
+ return $slug;
+ }
+
+ /**
+ * Format a name as a url slug.
+ */
+ protected function formatNameAsSlug(string $name): string
+ {
+ $slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', mb_strtolower($name));
+ $slug = preg_replace('/\s{2,}/', ' ', $slug);
+ $slug = str_replace(' ', '-', $slug);
+ if ($slug === "") {
+ $slug = substr(md5(rand(1, 500)), 0, 5);
+ }
+ return $slug;
+ }
+
+ /**
+ * Check if a slug is already in-use for this
+ * type of model within the same parent.
+ */
+ protected function slugInUse(string $slug): bool
+ {
+ $query = $this->entity->newQuery()->where('slug', '=', $slug);
+
+ if ($this->entity instanceof BookChild) {
+ $query->where('book_id', '=', $this->entity->book_id);
+ }
+
+ if ($this->entity->id) {
+ $query->where('id', '!=', $this->entity->id);
+ }
+
+ return $query->count() > 0;
+ }
+}
--- /dev/null
+<?php namespace BookStack\Exceptions;
+
+use Exception;
+
+class MoveOperationException extends Exception
+{
+
+}
--- /dev/null
+<?php namespace BookStack\Exceptions;
+
+use Exception;
+
+class SortOperationException extends Exception
+{
+
+}
--- /dev/null
+<?php namespace BookStack\Facades;
+
+use Illuminate\Support\Facades\Facade;
+
+class Permissions extends Facade
+{
+ /**
+ * Get the registered name of the component.
+ *
+ * @return string
+ */
+ protected static function getFacadeAccessor()
+ {
+ return 'permissions';
+ }
+}
<?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)
{
]);
$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);
/**
* 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)
{
]);
$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);
/**
* 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)
{
]);
$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);
/**
* Attach a link to a page.
- * @param Request $request
- * @return mixed
+ * @throws ValidationException
+ * @throws NotFoundException
*/
public function attachLink(Request $request)
{
]);
$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);
/**
* 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');
/**
* 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'));
}
* 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);
$userId = $this->emailConfirmationService->checkTokenAndGetUserId($token);
} catch (Exception $exception) {
if ($exception instanceof UserTokenNotFoundException) {
- session()->flash('error', 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);
- session()->flash('error', trans('errors.email_confirmation_expired'));
+ $this->showErrorNotification(trans('errors.email_confirmation_expired'));
return redirect('/register/confirm');
}
$user->save();
auth()->login($user);
- session()->flash('success', trans('auth.email_confirm_success'));
+ $this->showSuccessNotification(trans('auth.email_confirm_success'));
$this->emailConfirmationService->deleteByUser($user);
return redirect('/');
try {
$this->emailConfirmationService->sendConfirmation($user);
} catch (Exception $e) {
- session()->flash('error', trans('auth.email_confirm_send_error'));
+ $this->showErrorNotification(trans('auth.email_confirm_send_error'));
return redirect('/register/confirm');
}
- session()->flash('success', trans('auth.email_confirm_resent'));
+ $this->showSuccessNotification(trans('auth.email_confirm_resent'));
return redirect('/register/confirm');
}
}
if ($response === Password::RESET_LINK_SENT) {
$message = trans('auth.reset_password_sent_success', ['email' => $request->get('email')]);
- session()->flash('success', $message);
+ $this->showSuccessNotification($message);
return back()->with('status', trans($response));
}
try {
$this->emailConfirmationService->sendConfirmation($newUser);
} catch (Exception $e) {
- session()->flash('error', trans('auth.email_confirm_send_error'));
+ $this->showErrorNotification(trans('auth.email_confirm_send_error'));
}
return redirect('/register/confirm');
}
auth()->login($newUser);
- session()->flash('success', trans('auth.register_success'));
+ $this->showSuccessNotification(trans('auth.register_success'));
return redirect($this->redirectPath());
}
protected function sendResetResponse(Request $request, $response)
{
$message = trans('auth.reset_password_success');
- session()->flash('success', $message);
+ $this->showSuccessNotification($message);
return redirect($this->redirectPath())
->with('status', trans($response));
}
$user->save();
auth()->login($user);
- session()->flash('success', 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('/');
}
if ($exception instanceof UserTokenExpiredException) {
- session()->flash('error', trans('errors.invite_token_expired'));
+ $this->showErrorNotification(trans('errors.invite_token_expired'));
return redirect('/password/email');
}
<?php namespace BookStack\Http\Controllers;
use Activity;
-use BookStack\Auth\UserRepo;
-use BookStack\Entities\Book;
-use BookStack\Entities\EntityContextManager;
+use BookStack\Entities\Managers\BookContents;
+use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Managers\EntityContext;
use BookStack\Entities\Repos\BookRepo;
-use BookStack\Entities\Repos\EntityRepo;
-use BookStack\Entities\ExportService;
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;
{
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()
{
- $view = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books'));
- $sort = setting()->getUser($this->currentUser, 'books_sort', 'name');
- $order = setting()->getUser($this->currentUser, 'books_sort_order', 'asc');
- $sortOptions = [
- 'name' => trans('common.sort_name'),
- 'created_at' => trans('common.sort_created_at'),
- 'updated_at' => trans('common.sort_updated_at'),
- ];
-
- $books = $this->bookRepo->getAllPaginated('book', 18, $sort, $order);
- $recents = $this->signedIn ? $this->bookRepo->getRecentlyViewed('book', 4, 0) : false;
- $popular = $this->bookRepo->getPopular('book', 4, 0);
- $new = $this->bookRepo->getRecentlyCreated('book', 4, 0);
+ $view = setting()->getForCurrentUser('books_view_type', config('app.views.books'));
+ $sort = setting()->getForCurrentUser('books_sort', 'name');
+ $order = setting()->getForCurrentUser('books_sort_order', 'asc');
+
+ $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();
'view' => $view,
'sort' => $sort,
'order' => $order,
- 'sortOptions' => $sortOptions,
]);
}
/**
* 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
/**
* Store a newly created book in storage.
- *
- * @param Request $request
- * @param string $shelfSlug
- * @return Response
- * @throws NotFoundException
* @throws ImageUploadException
* @throws ValidationException
*/
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
- 'image' => $this->imageRepo->getImageValidationRules(),
+ 'image' => $this->getImageValidationRules(),
]);
$bookshelf = null;
if ($shelfSlug !== null) {
- $bookshelf = $this->bookRepo->getEntityBySlug('bookshelf', $shelfSlug);
+ $bookshelf = Bookshelf::visible()->where('slug', '=', $shelfSlug)->firstOrFail();
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
}
- $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) {
- $this->bookRepo->appendBookToShelf($bookshelf, $book);
+ $bookshelf->appendBook($book);
Activity::add($bookshelf, 'bookshelf_update');
}
/**
* 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')) {
/**
* Show the form for editing the specified book.
- * @param string $slug
- * @return Response
- * @throws NotFoundException
*/
public function edit(string $slug)
{
/**
* Update the specified book in storage.
- * @param Request $request
- * @param string $slug
- * @return Response
* @throws ImageUploadException
- * @throws NotFoundException
* @throws ValidationException
+ * @throws Throwable
*/
public function update(Request $request, string $slug)
{
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
- 'image' => $this->imageRepo->getImageValidationRules(),
+ 'image' => $this->getImageValidationRules(),
]);
- $book = $this->bookRepo->updateFromInput('book', $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)
{
}
/**
- * 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 getSortItem(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($mapItem->type, $mapItem->book, $model);
- }
- 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) {
- $this->bookRepo->buildJointPermissionsForBook($book);
- 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
*/
{
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-delete', $book);
- Activity::addMessage('book_delete', 0, $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);
- session()->flash('success', 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();
- }
+ $restricted = $request->get('restricted') === 'true';
+ $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
+ $this->bookRepo->updatePermissions($book, $restricted, $permissions);
- if ($request->has('image_reset')) {
- $this->imageRepo->destroyImage($book->cover);
- $book->image_id = 0;
- $book->save();
- }
+ $this->showSuccessNotification(trans('entities.books_permissions_updated'));
+ return redirect($book->getUrl());
}
}
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)
{
/**
* Export a book as a PDF file.
- * @param string $bookSlug
- * @return mixed
- * @throws NotFoundException
* @throws Throwable
*/
public function pdf(string $bookSlug)
/**
* Export a book as a contained HTML file.
- * @param string $bookSlug
- * @return mixed
- * @throws NotFoundException
* @throws Throwable
*/
public function html(string $bookSlug)
/**
* Export a book as a plain text file.
- * @param $bookSlug
- * @return mixed
- * @throws NotFoundException
*/
public function plainText(string $bookSlug)
{
--- /dev/null
+<?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());
+ }
+}
<?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();
/**
* Display a listing of the book.
- * @return Response
*/
public function index()
{
- $view = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
- $sort = setting()->getUser($this->currentUser, 'bookshelves_sort', 'name');
- $order = setting()->getUser($this->currentUser, 'bookshelves_sort_order', 'asc');
+ $view = setting()->getForCurrentUser('bookshelves_view_type', config('app.views.bookshelves', 'grid'));
+ $sort = setting()->getForCurrentUser('bookshelves_sort', 'name');
+ $order = setting()->getForCurrentUser('bookshelves_sort_order', 'asc');
$sortOptions = [
'name' => trans('common.sort_name'),
'created_at' => trans('common.sort_created_at'),
'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->signedIn ? $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'));
/**
* 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)
{
$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',
'image' => $this->imageRepo->getImageValidationRules(),
]);
- $shelf = $this->entityRepo->updateFromInput('bookshelf', $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()]));
/**
* 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', 0, $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);
- session()->flash('success', 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);
- session()->flash('success', 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();
- }
- }
}
<?php namespace BookStack\Http\Controllers;
use Activity;
-use BookStack\Auth\UserRepo;
-use BookStack\Entities\Repos\EntityRepo;
-use BookStack\Entities\ExportService;
+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)
{
'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,
/**
* 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', $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', $book->id, $chapter->name);
- $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
/**
* 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);
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) {
- session()->flash('error', 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, true);
- Activity::add($chapter, 'chapter_move', $chapter->book->id);
- session()->flash('success', 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);
- session()->flash('success', 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());
}
}
-<?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');
}
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);
}
/**
* 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',
/**
* 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')]);
}
namespace BookStack\Http\Controllers;
-use BookStack\Auth\User;
use BookStack\Ownable;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
{
use DispatchesJobs, ValidatesRequests;
- /**
- * @var User static
- */
- protected $currentUser;
- /**
- * @var bool
- */
- protected $signedIn;
-
/**
* Controller constructor.
*/
public function __construct()
{
- $this->middleware(function ($request, $next) {
-
- // Get a user instance for the current user
- $user = user();
-
- // Share variables with controllers
- $this->currentUser = $user;
- $this->signedIn = auth()->check();
-
- // Share variables with views
- view()->share('signedIn', $this->signedIn);
- view()->share('currentUser', $user);
+ //
+ }
- return $next($request);
- });
+ /**
+ * Check if the current user is signed in.
+ */
+ protected function isSignedIn(): bool
+ {
+ return auth()->check();
}
/**
* Stops the application and shows a permission error if
* the application is in demo mode.
*/
- protected function preventAccessForDemoUsers()
+ protected function preventAccessInDemoMode()
{
if (config('app.env') === 'demo') {
$this->showPermissionError();
$response = response()->json(['error' => trans('errors.permissionJson')], 403);
} else {
$response = redirect('/');
- session()->flash('error', trans('errors.permission'));
+ $this->showErrorNotification(trans('errors.permission'));
}
throw new HttpResponseException($response);
protected function checkPermissionOrCurrentUser(string $permissionName, int $userId)
{
return $this->checkPermissionOr($permissionName, function () use ($userId) {
- return $userId === $this->currentUser->id;
+ return $userId === user()->id;
});
}
*/
protected function jsonError($messageText = "", $statusCode = 500)
{
- return response()->json(['message' => $messageText], $statusCode);
+ return response()->json(['message' => $messageText, 'status' => 'error'], $statusCode);
}
/**
'Content-Disposition' => 'attachment; filename="' . $fileName . '"'
]);
}
+
+ /**
+ * Show a positive, successful notification to the user on next view load.
+ * @param string $message
+ */
+ protected function showSuccessNotification(string $message)
+ {
+ session()->flash('success', $message);
+ }
+
+ /**
+ * Show a warning notification to the user on next view load.
+ * @param string $message
+ */
+ protected function showWarningNotification(string $message)
+ {
+ session()->flash('warning', $message);
+ }
+
+ /**
+ * Show an error notification to the user on next view load.
+ * @param string $message
+ */
+ protected function showErrorNotification(string $message)
+ {
+ 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';
+ }
}
<?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.
public function index()
{
$activity = Activity::latest(10);
- $draftPages = $this->signedIn ? $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->signedIn ? 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');
// Add required list ordering & sorting for books & shelves views.
if ($homepageOption === 'bookshelves' || $homepageOption === 'books') {
$key = $homepageOption;
- $view = setting()->getUser($this->currentUser, $key . '_view_type', config('app.views.' . $key));
- $sort = setting()->getUser($this->currentUser, $key . '_sort', 'name');
- $order = setting()->getUser($this->currentUser, $key . '_sort_order', 'asc');
+ $view = setting()->getForCurrentUser($key . '_view_type', config('app.views.' . $key));
+ $sort = setting()->getForCurrentUser($key . '_sort', 'name');
+ $order = setting()->getForCurrentUser($key . '_sort_order', 'asc');
$sortOptions = [
'name' => trans('common.sort_name'),
}
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);
}
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]));
}
<?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;
/**
* 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);
}
/**
<?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;
{
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->signedIn) {
- $draft = $this->pageRepo->getDraftPage($book, $chapter);
+ if ($this->isSignedIn()) {
+ $draft = $this->pageRepo->getNewDraftPage($parent);
return redirect($draft->getUrl());
}
/**
* 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->signedIn;
- $templates = $this->pageRepo->getPageTemplates(10);
+ $draftsEnabled = $this->isSignedIn();
+ $templates = $this->pageRepo->getTemplates(10);
return view('pages.edit', [
'page' => $draft,
/**
* 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;
-
- $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);
+ $page = $this->pageRepo->publishDraft($draftPage, $request->all());
+ Activity::add($page, 'page_create', $draftPage->book->id);
- 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']);
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,
/**
* 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, $this->currentUser->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) {
- session()->flash('warning', implode("\n", $warnings));
+ $this->showWarningNotification(implode("\n", $warnings));
}
- $draftsEnabled = $this->signedIn;
- $templates = $this->pageRepo->getPageTemplates(10);
-
+ $templates = $this->pageRepo->getTemplates(10);
+ $draftsEnabled = $this->isSignedIn();
+ $this->setPageTitle(trans('entities.pages_editing_named', ['pageName' => $page->getShortName()]));
return view('pages.edit', [
'page' => $page,
'book' => $page->book,
/**
* 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->signedIn) {
- return response()->json([
- 'status' => 'error',
- 'message' => trans('errors.guests_cannot_save_drafts'),
- ], 500);
+ if (!$this->isSignedIn()) {
+ return $this->jsonError(trans('errors.guests_cannot_save_drafts'), 500);
}
$draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
}
/**
- * 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);
- Activity::addMessage('page_delete', $book->id, $page->name);
- session()->flash('success', trans('entities.pages_delete_success'));
+ $book = $page->book;
+ $this->pageRepo->destroy($page);
+ Activity::addMessage('page_delete', $page->name, $book->id);
+
+ $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);
- session()->flash('success', trans('entities.pages_delete_draft_success'));
- $this->pageRepo->destroyPage($page);
- 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]);
- }
+ $this->pageRepo->destroy($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);
- }
+ $this->showSuccessNotification(trans('entities.pages_delete_draft_success'));
- $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");
+ if ($chapter && userCan('view', $chapter)) {
+ return redirect($chapter->getUrl());
}
-
- // 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)) {
- session()->flash('error', trans('entities.revision_cannot_delete_latest'));
- return response()->view('pages.revisions', ['page' => $page, 'book' => $page->book, 'current' => $page], 400);
- }
-
- $revision->delete();
- session()->flash('success', trans('entities.revision_delete_success'));
- return redirect($page->getUrl('/revisions'));
+ return redirect($book->getUrl());
}
/**
- * 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
/**
* 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', [
}
/**
- * 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);
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);
- session()->flash('success', 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', [
]);
}
+
/**
* 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]);
-
- try {
- $parent = $this->pageRepo->getById($entityType, $entityId);
- } catch (Exception $e) {
- session()->flash(trans('entities.selected_book_chapter_not_found'));
- return redirect()->back();
- }
- }
+ $entitySelection = $request->get('entity_selection', null) ?? null;
+ $newName = $request->get('name', null);
- $this->checkOwnablePermission('page-create', $parent);
+ try {
+ $pageCopy = $this->pageRepo->copy($page, $entitySelection, $newName);
+ } catch (Exception $exception) {
+ if ($exception instanceof PermissionsException) {
+ $this->showPermissionError();
+ }
- $pageCopy = $this->pageRepo->copyPage($page, $parent, $request->get('name', ''));
+ $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
+ return redirect()->back();
+ }
Activity::add($pageCopy, 'page_create', $pageCopy->book->id);
- session()->flash('success', 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);
- session()->flash('success', 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());
}
}
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;
/**
/**
* 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');
}
--- /dev/null
+<?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'));
+ }
+}
protected $pageRepo;
/**
- * PageTemplateController constructor.
- * @param $pageRepo
+ * PageTemplateController constructor
*/
public function __construct(PageRepo $pageRepo)
{
/**
* 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]);
/**
* 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();
]);
$this->permissionsRepo->saveNewRole($request->all());
- session()->flash('success', trans('settings.role_create_success'));
+ $this->showSuccessNotification(trans('settings.role_create_success'));
return redirect('/settings/roles');
}
]);
$this->permissionsRepo->updateRole($id, $request->all());
- session()->flash('success', trans('settings.role_update_success'));
+ $this->showSuccessNotification(trans('settings.role_update_success'));
return redirect('/settings/roles');
}
try {
$this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id'));
} catch (PermissionsException $e) {
- session()->flash('error', $e->getMessage());
+ $this->showErrorNotification($e->getMessage());
return redirect()->back();
}
- session()->flash('success', trans('settings.role_delete_success'));
+ $this->showSuccessNotification(trans('settings.role_delete_success'));
return redirect('/settings/roles');
}
}
<?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;
/**
* Searches all entities.
- * @param Request $request
- * @return View
- * @internal param string $searchTerm
*/
public function search(Request $request)
{
/**
* 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);
/**
* 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);
/**
* 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)
{
/**
* 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);
}
// 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
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']);
*/
public function update(Request $request)
{
- $this->preventAccessForDemoUsers();
+ $this->preventAccessInDemoMode();
$this->checkPermission('settings-manage');
$this->validate($request, [
'app_logo' => $this->imageRepo->getImageValidationRules(),
setting()->remove('app-logo');
}
- session()->flash('success', trans('settings.settings_save_success'));
+ $this->showSuccessNotification(trans('settings.settings_save_success'));
return redirect('/settings');
}
$imagesToDelete = $imageService->deleteUnusedImages($checkRevisions, $dryRun);
$deleteCount = count($imagesToDelete);
if ($deleteCount === 0) {
- session()->flash('warning', 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 {
- session()->flash('success', 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();
*/
public function update(Request $request, $id)
{
- $this->preventAccessForDemoUsers();
+ $this->preventAccessInDemoMode();
$this->checkPermissionOrCurrentUser('users-manage', $id);
$this->validate($request, [
}
// External auth id updates
- if ($this->currentUser->can('users-manage') && $request->filled('external_auth_id')) {
+ if (user()->can('users-manage') && $request->filled('external_auth_id')) {
$user->external_auth_id = $request->get('external_auth_id');
}
}
$user->save();
- session()->flash('success', 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);
*/
public function destroy($id)
{
- $this->preventAccessForDemoUsers();
+ $this->preventAccessInDemoMode();
$this->checkPermissionOrCurrentUser('users-manage', $id);
$user = $this->userRepo->getById($id);
if ($this->userRepo->isOnlyAdmin($user)) {
- session()->flash('error', 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') {
- session()->flash('error', trans('errors.users_cannot_delete_guest'));
+ $this->showErrorNotification(trans('errors.users_cannot_delete_guest'));
return redirect($user->getEditUrl());
}
$this->userRepo->destroy($user);
- session()->flash('success', trans('settings.users_delete_success'));
+ $this->showSuccessNotification(trans('settings.users_delete_success'));
return redirect('/settings/users');
}
$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', [
\Illuminate\Routing\Middleware\ThrottleRequests::class,
\BookStack\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
- \BookStack\Http\Middleware\Localization::class
+ \BookStack\Http\Middleware\Localization::class,
+ \BookStack\Http\Middleware\GlobalViewData::class,
],
'api' => [
'throttle:60,1',
--- /dev/null
+<?php namespace BookStack\Http\Middleware;
+
+use Closure;
+use Illuminate\Http\Request;
+
+/**
+ * Class GlobalViewData
+ * Sets up data that is accessible to any view rendered by the web routes.
+ */
+class GlobalViewData
+{
+
+ /**
+ * Handle an incoming request.
+ *
+ * @param Request $request
+ * @param Closure $next
+ * @return mixed
+ */
+ public function handle(Request $request, Closure $next)
+ {
+ view()->share('signedIn', auth()->check());
+ view()->share('currentUser', user());
+
+ return $next($request);
+ }
+}
'uk' => 'uk_UA',
'zh_CN' => 'zh_CN',
'zh_TW' => 'zh_TW',
+ 'tr' => 'tr_TR',
];
/**
use BookStack\Actions\ActivityService;
use BookStack\Actions\ViewService;
+use BookStack\Auth\Permissions\PermissionService;
use BookStack\Settings\SettingService;
use BookStack\Uploads\ImageService;
use Illuminate\Support\ServiceProvider;
*/
public function register()
{
- $this->app->bind('activity', function () {
+ $this->app->singleton('activity', function () {
return $this->app->make(ActivityService::class);
});
- $this->app->bind('views', function () {
+ $this->app->singleton('views', function () {
return $this->app->make(ViewService::class);
});
- $this->app->bind('setting', function () {
+ $this->app->singleton('setting', function () {
return $this->app->make(SettingService::class);
});
- $this->app->bind('images', function () {
+ $this->app->singleton('images', function () {
return $this->app->make(ImageService::class);
});
+
+ $this->app->singleton('permissions', function () {
+ return $this->app->make(PermissionService::class);
+ });
}
}
WORKDIR /app
RUN apt-get update -y \
- && apt-get install -y libtidy-dev libpng-dev libldap2-dev libxml++2.6-dev wait-for-it \
+ && apt-get install -y git zip unzip libtidy-dev libpng-dev libldap2-dev libxml++2.6-dev wait-for-it \
&& docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu \
&& docker-php-ext-install pdo pdo_mysql tidy dom xml mbstring gd ldap \
&& a2enmod rewrite \
<server name="DB_CONNECTION" value="mysql_testing"/>
<server name="BCRYPT_ROUNDS" value="4"/>
<server name="MAIL_DRIVER" value="array"/>
- <server name="LOG_CHANNEL" value="null"/>
+ <server name="LOG_CHANNEL" value="single"/>
<server name="AUTH_METHOD" value="standard"/>
<server name="DISABLE_EXTERNAL_SERVICES" value="true"/>
<server name="AVATAR_URL" value=""/>
this.displayDoc = this.display.contentDocument;
this.init();
});
+
+ window.$events.emitPublic(elem, 'editor-markdown::setup', {
+ markdownIt: this.markdown,
+ displayEl: this.display,
+ codeMirrorInstance: this.cm,
+ });
}
init() {
handleAction(event) {
let actionElem = event.target.closest('[action]');
+
if (event.target.matches('a[href^="#"]')) {
const id = event.target.href.split('#')[1];
scrollAndHighlightElement(document.querySelector('#' + id));
}
+
if (actionElem === null) return;
event.preventDefault();
this.loadPlugins();
this.tinyMceConfig = this.getTinyMceConfig();
+ window.$events.emitPublic(elem, 'editor-tinymce::pre-init', {config: this.tinyMceConfig});
window.tinymce.init(this.tinyMceConfig);
}
// Paste image-uploads
editor.on('paste', event => editorPaste(event, editor, context));
+ // Custom handler hook
+ window.$events.emitPublic(context.elem, 'editor-tinymce::setup', {editor});
}
};
}
+/**
+ * Used in the function below to store references of clean-up functions.
+ * Used to ensure only one transitionend function exists at any time.
+ * @type {WeakMap<object, any>}
+ */
+const animateStylesCleanupMap = new WeakMap();
+
/**
* Fade out the given element.
* @param {Element} element
* @param {Function|null} onComplete
*/
export function fadeOut(element, animTime = 400, onComplete = null) {
+ cleanupExistingElementAnimation(element);
animateStyles(element, {
opacity: ['1', '0']
}, animTime, () => {
* @param {Number} animTime
*/
export function slideUp(element, animTime = 400) {
+ cleanupExistingElementAnimation(element);
const currentHeight = element.getBoundingClientRect().height;
const computedStyles = getComputedStyle(element);
const currentPaddingTop = computedStyles.getPropertyValue('padding-top');
* @param {Number} animTime - Animation time in ms
*/
export function slideDown(element, animTime = 400) {
+ cleanupExistingElementAnimation(element);
element.style.display = 'block';
const targetHeight = element.getBoundingClientRect().height;
const computedStyles = getComputedStyle(element);
animateStyles(element, animStyles, animTime);
}
-/**
- * Used in the function below to store references of clean-up functions.
- * Used to ensure only one transitionend function exists at any time.
- * @type {WeakMap<object, any>}
- */
-const animateStylesCleanupMap = new WeakMap();
-
/**
* Animate the css styles of an element using FLIP animation techniques.
* Styles must be an object where the keys are style properties, camelcase, and the values
}
element.style.transition = null;
element.removeEventListener('transitionend', cleanup);
+ animateStylesCleanupMap.delete(element);
if (onComplete) onComplete();
};
setTimeout(() => {
- requestAnimationFrame(() => {
- element.style.transition = `all ease-in-out ${animTime}ms`;
- for (let style of styleNames) {
- element.style[style] = styles[style][1];
- }
+ element.style.transition = `all ease-in-out ${animTime}ms`;
+ for (let style of styleNames) {
+ element.style[style] = styles[style][1];
+ }
- if (animateStylesCleanupMap.has(element)) {
- const oldCleanup = animateStylesCleanupMap.get(element);
- element.removeEventListener('transitionend', oldCleanup);
- }
+ element.addEventListener('transitionend', cleanup);
+ animateStylesCleanupMap.set(element, cleanup);
+ }, 15);
+}
- element.addEventListener('transitionend', cleanup);
- animateStylesCleanupMap.set(element, cleanup);
- });
- }, 10);
+/**
+ * Run the active cleanup action for the given element.
+ * @param {Element} element
+ */
+function cleanupExistingElementAnimation(element) {
+ if (animateStylesCleanupMap.has(element)) {
+ const oldCleanup = animateStylesCleanupMap.get(element);
+ oldCleanup();
+ }
}
\ No newline at end of file
value: content,
mode: mode,
lineNumbers: true,
+ lineWrapping: false,
theme: getTheme(),
readOnly: true
});
value: content,
mode: getMode(lang),
lineNumbers: true,
+ lineWrapping: false,
theme: getTheme(),
readOnly: true
});
value: content,
mode: getMode(modeSuggestion),
lineNumbers: true,
- theme: getTheme(),
- lineWrapping: true
+ lineWrapping: false,
+ theme: getTheme()
});
}
}
/**
- * Get a CodeMirror instace to use for the markdown editor.
+ * Get a CodeMirror instance to use for the markdown editor.
* @param {HTMLElement} elem
* @returns {*}
*/
function markdownEditor(elem) {
- let content = elem.textContent;
-
- return CodeMirror(function (elt) {
- elem.parentNode.insertBefore(elt, elem);
- elem.style.display = 'none';
- }, {
+ const content = elem.textContent;
+ const config = {
value: content,
mode: "markdown",
lineNumbers: true,
- theme: getTheme(),
lineWrapping: true,
+ theme: getTheme(),
scrollPastEnd: true,
- });
+ };
+
+ window.$events.emitPublic(elem, 'editor-markdown-cm::pre-init', {config});
+
+ return CodeMirror(function (elt) {
+ elem.parentNode.insertBefore(elt, elem);
+ elem.style.display = 'none';
+ }, config);
}
/**
this.stack = [];
}
+ /**
+ * Emit a custom event for any handlers to pick-up.
+ * @param {String} eventName
+ * @param {*} eventData
+ * @returns {Events}
+ */
emit(eventName, eventData) {
this.stack.push({name: eventName, data: eventData});
if (typeof this.listeners[eventName] === 'undefined') return this;
return this;
}
+ /**
+ * Listen to a custom event and run the given callback when that event occurs.
+ * @param {String} eventName
+ * @param {Function} callback
+ * @returns {Events}
+ */
listen(eventName, callback) {
if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = [];
this.listeners[eventName].push(callback);
return this;
}
+
+ /**
+ * Emit an event for public use.
+ * Sends the event via the native DOM event handling system.
+ * @param {Element} targetElement
+ * @param {String} eventName
+ * @param {Object} eventData
+ */
+ emitPublic(targetElement, eventName, eventData) {
+ const event = new CustomEvent(eventName, {
+ detail: eventData,
+ bubbles: true
+ });
+ targetElement.dispatchEvent(event);
+ }
}
export default Events;
\ No newline at end of file
'ru' => 'Русский',
'uk' => 'Українська',
'zh_CN' => '简体中文',
- 'zh_TW' => '繁體中文',
- 'hu' => 'Magyar'
+ 'zh_TW' => '繁體中文',
+ 'hu' => 'Magyar',
+ 'tr' => 'Türkçe',
]
//!////////////////////////////////
];
'digits' => ':attribute debe ser de :digits dígitos.',
'digits_between' => ':attribute debe ser un valor entre :min y :max dígios.',
'email' => ':attribute debe ser una dirección álida.',
+ 'ends_with' => 'El :attribute debe terminar con uno de los siguientes: :values',
'filled' => 'El campo :attribute es requerido.',
+ 'gt' => [
+ 'numeric' => 'El :attribute debe ser mayor que :value.',
+ 'file' => 'El :attribute debe ser mayor que :value kilobytes.',
+ 'string' => 'El :attribute debe ser mayor que :value caracteres.',
+ 'array' => 'El :attribute debe tener más de :value objetos.',
+ ],
+ 'gte' => [
+ 'numeric' => 'El :attribute debe ser mayor o igual a :value.',
+ 'file' => 'El :attribute debe ser mayor o igual a :value kilobytes.',
+ 'string' => 'El :attribute debe ser mayor o igual a :value caracteres.',
+ 'array' => 'El :attribute debe tener :value objetos o más.',
+ ],
'exists' => 'El :attribute seleccionado es inválido.',
'image' => 'El :attribute debe ser una imagen.',
'in' => 'El selected :attribute es inválio.',
'image_extension' => 'El :attribute debe tener una extensión de imagen válida y soportada.',
'integer' => 'El :attribute debe ser un entero.',
'ip' => 'El :attribute debe ser una dirección IP álida.',
+ 'ipv4' => 'El :attribute debe ser una dirección IPv4 válida.',
+ 'ipv6' => 'El :attribute debe ser una dirección IPv6 válida.',
+ 'json' => 'El :attribute debe ser una cadena JSON válida.',
+ 'lt' => [
+ 'numeric' => 'El :attribute debe ser menor que :value.',
+ 'file' => 'El :attribute debe ser menor que :value kilobytes.',
+ 'string' => 'El :attribute debe ser menor que :value caracteres.',
+ 'array' => 'El :attribute debe tener menos de :value objetos.',
+ ],
+ 'lte' => [
+ 'numeric' => 'El :attribute debe ser menor o igual a :value.',
+ 'file' => 'El :attribute debe ser menor o igual a :value kilobytes.',
+ 'string' => 'El :attribute debe ser menor o igual a :value caracteres.',
+ 'array' => 'El :attribute no debe tener más de :value objetos.',
+ ],
'max' => [
'numeric' => ':attribute no puede ser mayor que :max.',
'file' => ':attribute no puede ser mayor que :max kilobytes.',
'array' => ':attribute debe tener como mínimo :min items.',
],
'no_double_extension' => 'El :attribute debe tener una única extensión de archivo.',
- 'not_in' => ':attribute seleccionado es inválio.',
+ 'not_in' => ':attribute seleccionado es inválido.',
+ 'not_regex' => 'El formato de :attribute es inválido.',
'numeric' => ':attribute debe ser numérico.',
'regex' => ':attribute con formato inválido',
'required' => ':attribute es requerido.',
'password' => 'Mot de passe',
'password_confirm' => 'Confirmez le mot de passe',
'password_hint' => 'Doit faire plus de 7 caractères',
- 'forgot_password' => 'Mot de passe oublié ?',
+ 'forgot_password' => 'Mot de passe oublié ?',
'remember_me' => 'Se souvenir de moi',
- 'ldap_email_hint' => "Merci d'entrer une adresse e-mail pour ce compte",
+ 'ldap_email_hint' => "Merci d'entrer une adresse e-mail pour ce compte.",
'create_account' => 'Créer un compte',
- 'already_have_account' => 'Vous avez déjà un compte?',
- 'dont_have_account' => 'Vous n\'avez pas de compte?',
- 'social_login' => 'Social Login',
- 'social_registration' => 'Enregistrement Social',
- 'social_registration_text' => "S'inscrire et se connecter avec un réseau social",
+ 'already_have_account' => 'Vous avez déjà un compte ?',
+ 'dont_have_account' => 'Vous n\'avez pas de compte ?',
+ 'social_login' => 'Connexion avec un réseau social',
+ 'social_registration' => 'Inscription avec un réseau social',
+ 'social_registration_text' => "S'inscrire et se connecter avec un réseau social.",
- 'register_thanks' => 'Merci pour votre enregistrement',
+ 'register_thanks' => 'Merci pour votre inscription !',
'register_confirm' => 'Vérifiez vos e-mails et cliquez sur le lien de confirmation pour rejoindre :appName.',
- 'registrations_disabled' => "L'inscription est désactivée pour le moment",
+ 'registrations_disabled' => "Les inscriptions sont désactivées pour le moment",
'registration_email_domain_invalid' => 'Cette adresse e-mail ne peut pas accéder à l\'application',
'register_success' => 'Merci pour votre inscription. Vous êtes maintenant inscrit(e) et connecté(e)',
/**
* Password Reset
*/
- 'reset_password' => 'Reset Password',
- 'reset_password_send_instructions' => 'Entrez votre adresse e-mail ci-dessous et un e-mail avec un lien de réinitialisation de mot de passe vous sera envoyé',
+ 'reset_password' => 'Réinitialiser le mot de passe',
+ 'reset_password_send_instructions' => 'Entrez votre adresse e-mail ci-dessous et un e-mail avec un lien de réinitialisation de mot de passe vous sera envoyé.',
'reset_password_send_button' => 'Envoyer un lien de réinitialisation',
'reset_password_sent_success' => 'Un lien de réinitialisation a été envoyé à :email.',
'reset_password_success' => 'Votre mot de passe a été réinitialisé avec succès.',
'email_reset_subject' => 'Réinitialisez votre mot de passe pour :appName',
- 'email_reset_text' => 'Vous recevez cet e-mail parce que nous avons reçu une demande de réinitialisation pour votre compte',
+ 'email_reset_text' => 'Vous recevez cet e-mail parce que nous avons reçu une demande de réinitialisation pour votre compte.',
'email_reset_not_requested' => 'Si vous n\'avez pas effectué cette demande, vous pouvez ignorer cet e-mail.',
* Email Confirmation
*/
'email_confirm_subject' => 'Confirmez votre adresse e-mail pour :appName',
- 'email_confirm_greeting' => 'Merci d\'avoir rejoint :appName !',
- 'email_confirm_text' => 'Merci de confirmer en cliquant sur le lien ci-dessous :',
+ 'email_confirm_greeting' => 'Merci d\'avoir rejoint :appName !',
+ 'email_confirm_text' => 'Merci de confirmer en cliquant sur le lien ci-dessous :',
'email_confirm_action' => 'Confirmez votre adresse e-mail',
'email_confirm_send_error' => 'La confirmation par e-mail est requise mais le système n\'a pas pu envoyer l\'e-mail. Contactez l\'administrateur système.',
- 'email_confirm_success' => 'Votre adresse e-mail a été confirmée !',
+ 'email_confirm_success' => 'Votre adresse e-mail a été confirmée !',
'email_confirm_resent' => 'L\'e-mail de confirmation a été ré-envoyé. Vérifiez votre boîte de récéption.',
'email_not_confirmed' => 'Adresse e-mail non confirmée',
'email_not_confirmed_click_link' => 'Merci de cliquer sur le lien dans l\'e-mail qui vous a été envoyé après l\'enregistrement.',
'email_not_confirmed_resend' => 'Si vous ne retrouvez plus l\'e-mail, vous pouvez renvoyer un e-mail de confirmation en utilisant le formulaire ci-dessous.',
'email_not_confirmed_resend_button' => 'Renvoyez l\'e-mail de confirmation',
+
+ // User Invite
+ 'user_invite_email_subject' => 'Vous avez été invité(e) à rejoindre :appName !',
+ 'user_invite_email_greeting' => 'Un compte vous a été créé sur :appName.',
+ 'user_invite_email_text' => 'Cliquez sur le bouton ci-dessous pour renseigner le mot de passe et récupérer l\'accès :',
+ 'user_invite_email_action' => 'Renseignez le mot de passe de votre compte',
+ 'user_invite_page_welcome' => 'Bienvenue dans :appName !',
+ 'user_invite_page_text' => 'Pour finaliser votre compte et recevoir l\'accès, vous devez renseigner le mot de passe qui sera utilisé pour la connexion à :appName les prochaines fois.',
+ 'user_invite_page_confirm_button' => 'Confirmez le mot de passe',
+ 'user_invite_success' => 'Mot de passe renseigné, vous avez maintenant accès à :appName !'
];
'description' => 'Description',
'role' => 'Rôle',
'cover_image' => 'Image de couverture',
- 'cover_image_description' => 'Cette image doit être environ 440x250px.',
+ 'cover_image_description' => 'Cette image doit faire environ 440x250 px.',
/**
* Actions
/**
* Sort Options
*/
+ 'sort_options' => 'Options de tri',
+ 'sort_direction_toggle' => 'Inverser la direction du tri',
+ 'sort_ascending' => 'Tri ascendant',
+ 'sort_descending' => 'Tri descendant',
'sort_name' => 'Nom',
'sort_created_at' => 'Date de création',
'sort_updated_at' => 'Date de mise à jour',
'grid_view' => 'Vue en grille',
'list_view' => 'Vue en liste',
'default' => 'Défaut',
+ 'breadcrumb' => 'Fil d\'Ariane',
/**
* Header
*/
+ 'profile_menu' => 'Menu du profil',
'view_profile' => 'Voir le profil',
'edit_profile' => 'Modifier le profil',
+ // Layout tabs
+ 'tab_info' => 'Info',
+ 'tab_content' => 'Contenu',
+
/**
* Email Content
*/
- 'email_action_help' => 'Si vous rencontrez des problèmes pour cliquer sur le bouton ":actionText", copiez et collez l\'adresse ci-dessous dans votre navigateur :',
+ 'email_action_help' => 'Si vous rencontrez des problèmes pour cliquer sur le bouton ":actionText", copiez et collez l\'adresse ci-dessous dans votre navigateur :',
'email_rights' => 'Tous droits réservés',
];
'shelves_delete' => 'Supprimer l\'étagère',
'shelves_delete_named' => 'Supprimer l\'étagère :name',
'shelves_delete_explain' => "Ceci va supprimer l\'étagère nommée \':bookName\'. Les livres contenus dans cette étagère ne seront pas supprimés.",
- 'shelves_delete_confirmation' => 'Êtes-vous sûr(e) de vouloir supprimer cette étagère ?',
+ 'shelves_delete_confirmation' => 'Êtes-vous sûr(e) de vouloir supprimer cette étagère ?',
'shelves_permissions' => 'Permissions de l\'étagère',
'shelves_permissions_updated' => 'Permissions de l\'étagère mises à jour',
'shelves_permissions_active' => 'Permissions de l\'étagère activées',
'books_delete' => 'Supprimer un livre',
'books_delete_named' => 'Supprimer le livre :bookName',
'books_delete_explain' => 'Ceci va supprimer le livre nommé \':bookName\', tous les chapitres et pages seront supprimés.',
- 'books_delete_confirmation' => 'Êtes-vous sûr(e) de vouloir supprimer ce livre ?',
+ 'books_delete_confirmation' => 'Êtes-vous sûr(e) de vouloir supprimer ce livre ?',
'books_edit' => 'Modifier le livre',
'books_edit_named' => 'Modifier le livre :bookName',
'books_form_book_name' => 'Nom du livre',
'chapters_delete' => 'Supprimer le chapitre',
'chapters_delete_named' => 'Supprimer le chapitre :chapterName',
'chapters_delete_explain' => 'Ceci va supprimer le chapitre \':chapterName\', toutes les pages seront déplacées dans le livre parent.',
- 'chapters_delete_confirm' => 'Etes-vous sûr(e) de vouloir supprimer ce chapitre ?',
+ 'chapters_delete_confirm' => 'Etes-vous sûr(e) de vouloir supprimer ce chapitre ?',
'chapters_edit' => 'Modifier le chapitre',
'chapters_edit_named' => 'Modifier le chapitre :chapterName',
'chapters_save' => 'Enregistrer le chapitre',
'pages_delete_draft' => 'Supprimer le brouillon',
'pages_delete_success' => 'Page supprimée',
'pages_delete_draft_success' => 'Brouillon supprimé',
- 'pages_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cette page ?',
- 'pages_delete_draft_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer ce brouillon ?',
+ 'pages_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cette page ?',
+ 'pages_delete_draft_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer ce brouillon ?',
'pages_editing_named' => 'Modification de la page :pageName',
'pages_edit_toggle_header' => 'Afficher/cacher l\'en-tête',
'pages_edit_save_draft' => 'Enregistrer le brouillon',
'start_b' => ':userName a commencé à éditer cette page',
'time_a' => 'depuis la dernière sauvegarde',
'time_b' => 'dans les :minCount dernières minutes',
- 'message' => ':start :time. Attention à ne pas écraser les mises à jour de quelqu\'un d\'autre !',
+ 'message' => ':start :time. Attention à ne pas écraser les mises à jour de quelqu\'un d\'autre !',
],
'pages_draft_discarded' => 'Brouillon écarté, la page est dans sa version actuelle.',
'pages_specific' => 'Page Spécifique',
+ 'pages_is_template' => 'Modèle de page',
/**
* Editor sidebar
'attachments_file_uploaded' => 'Fichier ajouté avec succès',
'attachments_file_updated' => 'Fichier mis à jour avec succès',
'attachments_link_attached' => 'Lien attaché à la page avec succès',
+ 'templates' => 'Modèles',
+ 'templates_set_as_template' => 'La page est un modèle',
+ 'templates_explain_set_as_template' => 'You can set this page as a template so its contents be utilized when creating other pages. Other users will be able to use this template if they have view permissions for this page.',
+ 'templates_replace_content' => 'Remplacer le contenu de la page',
+ 'templates_append_content' => 'Ajouter après le contenu de la page',
+ 'templates_prepend_content' => 'Ajouter devant le contenu de la page',
/**
* Profile View
'comments' => 'Commentaires',
'comment_add' => 'Ajouter un commentaire',
'comment_placeholder' => 'Entrez vos commentaires ici',
- 'comment_count' => '{0} Pas de commentaires|{1} 1 Commentaire|[2,*] :count Commentaires',
+ 'comment_count' => '{0} Pas de commentaires|{1} Un commentaire|[2,*] :count commentaires',
'comment_save' => 'Enregistrer le commentaire',
- 'comment_saving' => 'Enregistrement du commentaire...',
- 'comment_deleting' => 'Suppression du commentaire...',
+ 'comment_saving' => 'Enregistrement du commentaire…',
+ 'comment_deleting' => 'Suppression du commentaire…',
'comment_new' => 'Nouveau commentaire',
'comment_created' => 'commenté :createDiff',
'comment_updated' => 'Mis à jour :updateDiff par :username',
'comment_deleted_success' => 'Commentaire supprimé',
'comment_created_success' => 'Commentaire ajouté',
'comment_updated_success' => 'Commentaire mis à jour',
- 'comment_delete_confirm' => 'Etes-vous sûr de vouloir supprimer ce commentaire ?',
+ 'comment_delete_confirm' => 'Etes-vous sûr de vouloir supprimer ce commentaire ?',
'comment_in_reply_to' => 'En réponse à :commentId',
/**
* Revision
*/
- 'revision_delete_confirm' => 'Êtes-vous sûr de vouloir supprimer cette révision?',
+ 'revision_delete_confirm' => 'Êtes-vous sûr de vouloir supprimer cette révision ?',
+ 'revision_restore_confirm' => 'Êtes-vous sûr de vouloir restaurer cette révision ? Le contenu courant de la page va être remplacé.',
'revision_delete_success' => 'Révision supprimée',
'revision_cannot_delete_latest' => 'Impossible de supprimer la dernière révision.'
];
'ldap_extension_not_installed' => 'L\'extension LDAP PHP n\'est pas installée',
'ldap_cannot_connect' => 'Impossible de se connecter au serveur LDAP, la connexion initiale a échoué',
'social_no_action_defined' => 'Pas d\'action définie',
- 'social_login_bad_response' => "Erreur pendant la tentative de connexion à :socialAccount : \n:error",
+ 'social_login_bad_response' => "Erreur pendant la tentative de connexion à :socialAccount : \n:error",
'social_account_in_use' => 'Ce compte :socialAccount est déjà utilisé. Essayez de vous connecter via :socialAccount.',
'social_account_email_in_use' => 'L\'email :email est déjà utilisé. Si vous avez déjà un compte :socialAccount, vous pouvez le joindre à votre profil existant.',
'social_account_existing' => 'Ce compte :socialAccount est déjà rattaché à votre profil.',
'social_account_register_instructions' => 'Si vous n\'avez pas encore de compte, vous pouvez le lier avec l\'option :socialAccount.',
'social_driver_not_found' => 'Pilote de compte social absent',
'social_driver_not_configured' => 'Vos préférences pour le compte :socialAccount sont incorrectes.',
+ 'invite_token_expired' => 'Le lien de cette invitation a expiré. Vous pouvez essayer de réinitiliser votre mot de passe.',
// System
'path_not_writable' => 'Impossible d\'écrire dans :filePath. Assurez-vous d\'avoir les droits d\'écriture sur le serveur',
'role_cannot_be_edited' => 'Ce rôle ne peut pas être modifié',
'role_system_cannot_be_deleted' => 'Ceci est un rôle du système et ne peut pas être supprimé',
'role_registration_default_cannot_delete' => 'Ce rôle ne peut pas être supprimé tant qu\'il est le rôle par défaut',
+ 'role_cannot_remove_only_admin' => 'Ceci est le seul compte administrateur. Assignez un nouvel administrateur avant de le supprimer ici.',
// Error pages
'404_page_not_found' => 'Page non trouvée',
'password' => 'Les mots de passe doivent faire au moins 6 caractères et correspondre à la confirmation.',
'user' => "Nous n'avons pas trouvé d'utilisateur avec cette adresse.",
'token' => 'Le jeton de réinitialisation est invalide.',
- 'sent' => 'Nous vous avons envoyé un lien de réinitialisation de mot de passe !',
- 'reset' => 'Votre mot de passe a été réinitialisé !',
+ 'sent' => 'Nous vous avons envoyé un lien de réinitialisation de mot de passe !',
+ 'reset' => 'Votre mot de passe a été réinitialisé !',
];
'app_features_security' => 'Fonctionnalités et sécurité',
'app_name' => 'Nom de l\'application',
'app_name_desc' => 'Ce nom est affiché dans l\'en-tête et les e-mails.',
- 'app_name_header' => 'Afficher le nom dans l\'en-tête ?',
+ 'app_name_header' => 'Afficher le nom dans l\'en-tête ?',
'app_public_access' => 'Accès public',
'app_public_access_desc' => 'L\'activation de cette option permettra aux visiteurs, qui ne sont pas connectés, d\'accéder au contenu de votre instance BookStack.',
'app_public_access_desc_guest' => 'L\'accès pour les visiteurs publics peut être contrôlé par l\'utilisateur "Guest".',
'app_public_access_toggle' => 'Autoriser l\'accès public',
- 'app_public_viewing' => 'Accepter le visionnage public des pages ?',
- 'app_secure_images' => 'Activer l\'ajout d\'image sécurisé ?',
+ 'app_public_viewing' => 'Accepter le visionnage public des pages ?',
+ 'app_secure_images' => 'Activer l\'ajout d\'image sécurisé ?',
'app_secure_images_toggle' => 'Activer l\'ajout d\'image sécurisé',
'app_secure_images_desc' => 'Pour des questions de performances, toutes les images sont publiques. Cette option ajoute une chaîne aléatoire difficile à deviner dans les URLs des images.',
'app_editor' => 'Editeur des pages',
'reg_enable_desc' => 'Lorsque l\'inscription est activée, l\'utilisateur pourra s\'enregistrer en tant qu\'utilisateur de l\'application. Lors de l\'inscription, ils se voient attribuer un rôle par défaut.',
'reg_default_role' => 'Rôle par défaut lors de l\'inscription',
'reg_email_confirmation' => 'Confirmation de l\'e-mail',
- 'reg_email_confirmation_toggle' => 'Obliger la confirmation par e-mail ?',
+ 'reg_email_confirmation_toggle' => 'Obliger la confirmation par e-mail ?',
'reg_confirm_email_desc' => 'Si la restriction de domaine est activée, la confirmation sera automatiquement obligatoire et cette valeur sera ignorée.',
'reg_confirm_restrict_domain' => 'Restreindre l\'inscription à un domaine',
'reg_confirm_restrict_domain_desc' => 'Entrez une liste de domaines acceptés lors de l\'inscription, séparés par une virgule. Les utilisateurs recevront un e-mail de confirmation à cette adresse. <br> Les utilisateurs pourront changer leur adresse après inscription s\'ils le souhaitent.',
'maint_image_cleanup_desc' => "Scan le contenu des pages et des révisions pour vérifier les images et les dessins en cours d'utilisation et lesquels sont redondant. Veuillez à faire une sauvegarde de la base de données et des images avant de lancer ceci.",
'maint_image_cleanup_ignore_revisions' => 'Ignorer les images dans les révisions',
'maint_image_cleanup_run' => 'Lancer le nettoyage',
- 'maint_image_cleanup_warning' => ':count images potentiellement inutilisées trouvées. Etes-vous sûr de vouloir supprimer ces images ?',
- 'maint_image_cleanup_success' => ':count images potentiellement inutilisées trouvées et supprimées !',
- 'maint_image_cleanup_nothing_found' => 'Aucune image inutilisée trouvée, rien à supprimer !',
+ 'maint_image_cleanup_warning' => ':count images potentiellement inutilisées trouvées. Etes-vous sûr de vouloir supprimer ces images ?',
+ 'maint_image_cleanup_success' => ':count images potentiellement inutilisées trouvées et supprimées !',
+ 'maint_image_cleanup_nothing_found' => 'Aucune image inutilisée trouvée, rien à supprimer !',
/**
* Role settings
'role_delete_confirm' => 'Ceci va supprimer le rôle \':roleName\'.',
'role_delete_users_assigned' => 'Ce rôle a :userCount utilisateurs assignés. Vous pouvez choisir un rôle de remplacement pour ces utilisateurs.',
'role_delete_no_migration' => "Ne pas assigner de nouveau rôle",
- 'role_delete_sure' => 'Êtes-vous sûr de vouloir supprimer ce rôle ?',
+ 'role_delete_sure' => 'Êtes-vous sûr de vouloir supprimer ce rôle ?',
'role_delete_success' => 'Le rôle a été supprimé avec succès',
'role_edit' => 'Modifier le rôle',
'role_details' => 'Détails du rôle',
'role_manage_users' => 'Gérer les utilisateurs',
'role_manage_roles' => 'Gérer les rôles et permissions',
'role_manage_entity_permissions' => 'Gérer les permissions sur les livres, chapitres et pages',
- 'role_manage_own_entity_permissions' => 'Gérer les permissions de ses propres livres, chapitres, et pages',
+ 'role_manage_own_entity_permissions' => 'Gérer les permissions de ses propres livres, chapitres et pages',
'role_manage_settings' => 'Gérer les préférences de l\'application',
'role_asset' => 'Permissions des ressources',
'role_asset_desc' => 'Ces permissions contrôlent l\'accès par défaut des ressources dans le système. Les permissions dans les livres, les chapitres et les pages ignoreront ces permissions',
'users_delete' => 'Supprimer un utilisateur',
'users_delete_named' => 'Supprimer l\'utilisateur :userName',
'users_delete_warning' => 'Ceci va supprimer \':userName\' du système.',
- 'users_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cet utilisateur ?',
+ 'users_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cet utilisateur ?',
'users_delete_success' => 'Utilisateurs supprimés avec succès',
'users_edit' => 'Modifier l\'utilisateur',
'users_edit_profile' => 'Modifier le profil',
'users_edit_success' => 'Utilisateur mis à jour avec succès',
'users_avatar' => 'Avatar de l\'utilisateur',
- 'users_avatar_desc' => 'Cette image doit être un carré d\'environ 256px.',
+ 'users_avatar_desc' => 'Cette image doit être un carré d\'environ 256 px.',
'users_preferred_language' => 'Langue préférée',
'users_preferred_language_desc' => 'Cette option changera la langue utilisée pour l\'interface utilisateur de l\'application. Ceci n\'affectera aucun contenu créé par l\'utilisateur.',
'users_social_accounts' => 'Comptes sociaux',
'digits' => ':attribute doit être de longueur :digits.',
'digits_between' => ':attribute doit avoir une longueur entre :min et :max.',
'email' => ':attribute doit être une adresse e-mail valide.',
+ 'ends_with' => ':attribute doit se terminer par une des valeurs suivantes : :values',
'filled' => ':attribute est un champ requis.',
+ 'gt' => [
+ 'numeric' => ':attribute doit être plus grand que :value.',
+ 'file' => ':attribute doit être plus grand que :value kilobytes.',
+ 'string' => ':attribute doit être plus grand que :value caractères.',
+ 'array' => ':attribute doit avoir plus que :value éléments.',
+ ],
+ 'gte' => [
+ 'numeric' => ':attribute doit être plus grand ou égal à :value.',
+ 'file' => ':attribute doit être plus grand ou égal à :value kilobytes.',
+ 'string' => ':attribute doit être plus grand ou égal à :value caractères.',
+ 'array' => ':attribute doit avoir :value éléments ou plus.',
+ ],
'exists' => 'L\'attribut :attribute est invalide.',
'image' => ':attribute doit être une image.',
'image_extension' => ':attribute doit avoir une extension d\'image valide et supportée.',
'in' => 'L\'attribut :attribute est invalide.',
'integer' => ':attribute doit être un chiffre entier.',
'ip' => ':attribute doit être une adresse IP valide.',
+ 'ipv4' => ':attribute doit être une adresse IPv4 valide.',
+ 'ipv6' => ':attribute doit être une adresse IPv6 valide.',
+ 'json' => ':attribute doit être une chaine JSON valide.',
+ 'lt' => [
+ 'numeric' => ':attribute doit être plus petit que :value.',
+ 'file' => ':attribute doit être plus petit que :value kilobytes.',
+ 'string' => ':attribute doit être plus petit que :value caractères.',
+ 'array' => ':attribute doit avoir moins de :value éléments.',
+ ],
+ 'lte' => [
+ 'numeric' => ':attribute doit être plus petit ou égal à :value.',
+ 'file' => ':attribute doit être plus petit ou égal à :value kilobytes.',
+ 'string' => ':attribute doit être plus petit ou égal à :value caractères.',
+ 'array' => ':attribute ne doit pas avoir plus de :value éléments.',
+ ],
'max' => [
'numeric' => ':attribute ne doit pas excéder :max.',
'file' => ':attribute ne doit pas excéder :max kilobytes.',
],
'no_double_extension' => ':attribute ne doit avoir qu\'une seule extension de fichier.',
'not_in' => 'L\'attribut sélectionné :attribute est invalide.',
+ 'not_regex' => ':attribute a un format invalide.',
'numeric' => ':attribute doit être un nombre.',
'regex' => ':attribute a un format invalide.',
'required' => ':attribute est un champ requis.',
// App Settings
'app_customization' => 'Настройки',
- 'app_features_security' => 'ФÑ\83нкÑ\86ии & Безопасность',
+ 'app_features_security' => 'ФÑ\83нкÑ\86ионал & Безопасность',
'app_name' => 'Имя приложения',
'app_name_desc' => 'Имя отображается в заголовке email отправленных системой.',
'app_name_header' => 'Отображать имя приложения в заголовке',
// Registration Settings
'reg_settings' => 'Настройки регистрации',
- 'reg_enable' => 'РазÑ\80еÑ\88иÑ\82Ñ\8c Ñ\80егиÑ\81Ñ\82Ñ\80аÑ\86иÑ\8fÑ\8e',
+ 'reg_enable' => 'Разрешить регистрацию',
'reg_enable_toggle' => 'Разрешить регистрацию',
- 'reg_enable_desc' => 'Если регистрация разрешена, пользователь сможет зарегистрироваться в системе самомтоятельно. При регистрации назначается роль пользователя по умолчанию',
+ 'reg_enable_desc' => 'Если регистрация разрешена, пользователь сможет зарегистрироваться в системе самостоятельно. При регистрации назначается роль пользователя по умолчанию',
'reg_default_role' => 'Роль пользователя по умолчанию после регистрации',
'reg_email_confirmation' => 'Подтверждение электонной почты',
'reg_email_confirmation_toggle' => 'Требовать подтверждение по электронной почте',
- 'reg_confirm_email_desc' => 'Ð\95Ñ\81ли иÑ\81полÑ\8cзÑ\83еÑ\82Ñ\81Ñ\8f огÑ\80аниÑ\87ение по доменÑ\83, подÑ\82веÑ\80ждение бÑ\83деÑ\82 обÑ\8fзаÑ\82елÑ\8cно, а Ñ\8dÑ\82оÑ\82 пÑ\83нкÑ\82 пÑ\80оигноÑ\80иÑ\80ован.',
+ 'reg_confirm_email_desc' => 'Ð\9fÑ\80и иÑ\81полÑ\8cзовании огÑ\80аниÑ\87ениÑ\8f по доменÑ\83 - подÑ\82веÑ\80ждение обÑ\8fзаÑ\82елÑ\8cно, Ñ\8dÑ\82оÑ\82 пÑ\83нкÑ\82 игноÑ\80иÑ\80Ñ\83еÑ\82Ñ\81Ñ\8f.',
'reg_confirm_restrict_domain' => 'Ограничить регистрацию по домену',
'reg_confirm_restrict_domain_desc' => 'Введите список доменов почты через запятую, для которых разрешена регистрация. Пользователям будет отправлено письмо для подтверждения адреса перед входом в приложение. <br> Обратите внимание, что пользователи смогут изменить свои адреса уже после регистрации.',
'reg_confirm_restrict_domain_placeholder' => 'Без ограничений',
--- /dev/null
+<?php
+/**
+ * Activity text strings.
+ * Is used for all the text within activity logs & notifications.
+ */
+return [
+
+ // Pages
+ 'page_create' => 'saya oluşturuldu',
+ 'page_create_notification' => 'Sayfa Başarıyla Oluşturuldu',
+ 'page_update' => 'sayfa güncellendi',
+ 'page_update_notification' => 'Sayfa Başarıyla Güncellendi',
+ 'page_delete' => 'sayfa silindi',
+ 'page_delete_notification' => 'Sayfa Başarıyla Silindi',
+ 'page_restore' => 'sayfa kurtarıldı',
+ 'page_restore_notification' => 'Sayfa Başarıyla Kurtarıldı',
+ 'page_move' => 'sayfa taşındı',
+
+ // Chapters
+ 'chapter_create' => 'bölüm oluşturuldu',
+ 'chapter_create_notification' => 'Bölüm Başarıyla Oluşturuldu',
+ 'chapter_update' => 'bölüm güncellendi',
+ 'chapter_update_notification' => 'Bölüm Başarıyla Güncellendi',
+ 'chapter_delete' => 'bölüm silindi',
+ 'chapter_delete_notification' => 'Bölüm Başarıyla Silindi',
+ 'chapter_move' => 'bölüm taşındı',
+
+ // Books
+ 'book_create' => 'kitap oluşturuldu',
+ 'book_create_notification' => 'Kitap Başarıyla Oluşturuldu',
+ 'book_update' => 'kitap güncellendi',
+ 'book_update_notification' => 'Kitap Başarıyla Güncellendi',
+ 'book_delete' => 'kitap silindi',
+ 'book_delete_notification' => 'Kitap Başarıyla Silindi',
+ 'book_sort' => 'kitap düzenlendi',
+ 'book_sort_notification' => 'Kitap Başarıyla Yeniden Sıralandı',
+
+ // Bookshelves
+ 'bookshelf_create' => 'kitaplık oluşturuldu',
+ 'bookshelf_create_notification' => 'Kitaplık Başarıyla Oluşturuldu',
+ 'bookshelf_update' => 'kitaplık güncellendi',
+ 'bookshelf_update_notification' => 'Kitaplık Başarıyla Güncellendi',
+ 'bookshelf_delete' => 'kitaplık silindi',
+ 'bookshelf_delete_notification' => 'Kitaplık Başarıyla Silindi',
+
+ // Other
+ 'commented_on' => 'yorum yaptı',
+];
--- /dev/null
+<?php
+/**
+ * Authentication Language Lines
+ * The following language lines are used during authentication for various
+ * messages that we need to display to the user.
+ */
+return [
+
+ 'failed' => 'Girilen bilgiler bizdeki kayıtlarla uyuşmuyor.',
+ 'throttle' => 'Çok fazla giriş yapmaya çalıştınız. Lütfen :seconds saniye içinde tekrar deneyin.',
+
+ // Login & Register
+ 'sign_up' => 'Kayıt Ol',
+ 'log_in' => 'Giriş Yap',
+ 'log_in_with' => ':socialDriver ile giriş yap',
+ 'sign_up_with' => ':socialDriver ile kayıt ol',
+ 'logout' => 'Çıkış Yap',
+
+ 'name' => 'İsim',
+ 'username' => 'Kullanıcı Adı',
+ 'email' => 'Email',
+ 'password' => 'Şifre',
+ 'password_confirm' => 'Şifreyi onayla',
+ 'password_hint' => 'En az 5 karakter olmalı',
+ 'forgot_password' => 'Şifrenizi mi unuttunuz?',
+ 'remember_me' => 'Beni Hatırla',
+ 'ldap_email_hint' => 'Hesabı kullanmak istediğiniz e-mail adresinizi giriniz.',
+ 'create_account' => 'Hesap Oluştur',
+ 'already_have_account' => 'Zaten bir hesabınız var mı?',
+ 'dont_have_account' => 'Hesabınız yok mu?',
+ 'social_login' => 'Diğer Servisler ile Giriş Yap',
+ 'social_registration' => 'Diğer Servisler ile Kayıt Ol',
+ 'social_registration_text' => 'Diğer servisler ile kayıt ol ve giriş yap.',
+
+ 'register_thanks' => 'Kayıt olduğunuz için teşekkürler!',
+ 'register_confirm' => 'Lütfen e-posta adresinizi kontrol edin ve gelen doğrulama bağlantısına tıklayınız. :appName.',
+ 'registrations_disabled' => 'Kayıt olma özelliği geçici olarak kısıtlanmıştır',
+ 'registration_email_domain_invalid' => 'Bu e-mail sağlayıcısının bu uygulamaya erişim izni yoktur.',
+ 'register_success' => 'Artık kayıtlı bir kullanıcı olarak giriş yaptınız.',
+
+
+ // Password Reset
+ 'reset_password' => 'Parolayı Sıfırla',
+ 'reset_password_send_instructions' => 'Aşağıya e-mail adresinizi girdiğinizde parola yenileme bağlantısı mail adresinize gönderilecektir.',
+ 'reset_password_send_button' => '>Sıfırlama Bağlantısını Gönder',
+ 'reset_password_sent_success' => 'Sıfırlama bağlantısı :email adresinize gönderildi.',
+ 'reset_password_success' => 'Parolanız başarıyla sıfırlandı.',
+ 'email_reset_subject' => ':appName şifrenizi sıfırlayın.',
+ 'email_reset_text' => ' Parola sıfırlama isteğinde bulunduğunuz için bu maili görüntülüyorsunuz.',
+ 'email_reset_not_requested' => 'Eğer bu parola sıfırlama isteğinde bulunmadıysanız herhangi bir işlem yapmanıza gerek yoktur.',
+
+
+ // Email Confirmation
+ 'email_confirm_subject' => ':appName için girdiğiniz mail adresiniz onaylayınız',
+ 'email_confirm_greeting' => ':appName\'e katıldığınız için teşekkürler!',
+ 'email_confirm_text' => 'Lütfen e-mail adresinizi aşağıda bulunan butona tıklayarak onaylayınız:',
+ 'email_confirm_action' => 'E-Maili Onayla',
+ 'email_confirm_send_error' => 'e-mail onayı gerekli fakat sistem mail göndermeyi başaramadı. Yöneticiniz ile görüşüp kurulumlarda bir sorun olmadığını doğrulayın.',
+ 'email_confirm_success' => 'e-mail adresiniz onaylandı!',
+ 'email_confirm_resent' => 'Doğrulama maili gönderildi, lütfen gelen kutunuzu kontrol ediniz...',
+
+ 'email_not_confirmed' => 'E-mail Adresi Doğrulanmadı',
+ 'email_not_confirmed_text' => 'Sağlamış olduğunuz e-mail adresi henüz doğrulanmadı.',
+ 'email_not_confirmed_click_link' => 'Lütfen kayıt olduktan kısa süre sonra size gönderilen maildeki bağlantıya tıklayın ve mail adresinizi onaylayın.',
+ 'email_not_confirmed_resend' => 'Eğer gelen maili bulamadıysanız aşağıdaki formu tekrar doldurarak onay mailini kendinize tekrar gönderebilirsiniz.',
+ 'email_not_confirmed_resend_button' => 'Doğrulama Mailini Yeniden Yolla',
+];
\ No newline at end of file
--- /dev/null
+<?php
+/**
+ * Common elements found throughout many areas of BookStack.
+ */
+return [
+
+ // Buttons
+ 'cancel' => 'İptal',
+ 'confirm' => 'Onayla',
+ 'back' => 'Geri',
+ 'save' => 'Kaydet',
+ 'continue' => 'Devam',
+ 'select' => 'Seç',
+ 'toggle_all' => 'Hepsini Değiştir',
+ 'more' => 'Daha Fazla',
+
+ // Form Labels
+ 'name' => 'İsim',
+ 'description' => 'Açıklama',
+ 'role' => 'Rol',
+ 'cover_image' => 'Kapak resmi',
+ 'cover_image_description' => 'Bu resim yaklaşık 440x250px boyutlarında olmalıdır.',
+
+ // Actions
+ 'actions' => 'Aksiyonlar',
+ 'view' => 'Görüntüle',
+ 'view_all' => 'Hepsini Görüntüle',
+ 'create' => 'Oluştur',
+ 'update' => 'Güncelle',
+ 'edit' => 'Düzenle',
+ 'sort' => 'Sırala',
+ 'move' => 'Taşı',
+ 'copy' => 'Kopyala',
+ 'reply' => 'Yanıtla',
+ 'delete' => 'Sil',
+ 'search' => 'Ara',
+ 'search_clear' => 'Aramayı Temizle',
+ 'reset' => 'Sıfırla',
+ 'remove' => 'Kaldır',
+ 'add' => 'Ekle',
+
+ // Sort Options
+ 'sort_name' => 'İsim',
+ 'sort_created_at' => 'Oluşturulma Tarihi',
+ 'sort_updated_at' => 'Güncellenme Tarihi',
+
+ // Misc
+ 'deleted_user' => 'Silinmiş Kullanıcı',
+ 'no_activity' => 'Gösterilecek aktivite yok',
+ 'no_items' => 'Kullanılabilir öge yok',
+ 'back_to_top' => 'Başa dön',
+ 'toggle_details' => 'Detayları değiştir',
+ 'toggle_thumbnails' => 'Küçük resimleri değiştir',
+ 'details' => 'Detaylar',
+ 'grid_view' => 'Grid görünümü',
+ 'list_view' => 'Liste görünümü',
+ 'default' => 'Varsayılan',
+
+ // Header
+ 'view_profile' => 'Profili Görüntüle',
+ 'edit_profile' => 'Profili Düzenle',
+
+ // Layout tabs
+ 'tab_info' => 'Bilgi',
+ 'tab_content' => 'İçerik',
+
+ // Email Content
+ 'email_action_help' => 'Eğer ":actionText" butonuna tıklamakta zorluk çekiyorsanız, aşağıda bulunan linki kopyalayıp tarayıcınıza yapıştırabilirsiniz.',
+ 'email_rights' => 'Bütün hakları saklıdır',
+];
--- /dev/null
+<?php
+/**
+ * Text used in custom JavaScript driven components.
+ */
+return [
+
+ // Image Manager
+ 'image_select' => 'Görsel Seç',
+ 'image_all' => 'Tümü',
+ 'image_all_title' => 'Tüm görselleri temizle',
+ 'image_book_title' => 'Bu kitaba ait görselleri görüntüle',
+ 'image_page_title' => 'Bu sayfaya ait görselleri görüntüle',
+ 'image_search_hint' => 'Görsel adı ile ara',
+ 'image_uploaded' => ':uploadedDate tarihinde yüklendi',
+ 'image_load_more' => 'Daha Fazla ',
+ 'image_image_name' => 'Görsel Adı',
+ 'image_delete_used' => 'Bu görsel aşağıda bulunan görsellerde kullanılmış.',
+ 'image_delete_confirm' => 'Gerçekten bu görseli silmek istiyorsanız sil tuşuna basınız.',
+ 'image_select_image' => 'Görsel Seç',
+ 'image_dropzone' => 'Görselleri buraya sürükle veya seçmek için buraya tıkla',
+ 'images_deleted' => 'Görseller Silindi',
+ 'image_preview' => 'Görsel Önizleme',
+ 'image_upload_success' => 'Görsel başarıyla yüklendi',
+ 'image_update_success' => 'Görsel başarıyla güncellendi',
+ 'image_delete_success' => 'Görsel başarıyla silindi',
+ 'image_upload_remove' => 'Kaldır',
+
+ // Code Editor
+ 'code_editor' => 'Kodu Güncelle',
+ 'code_language' => 'Kod Dil',
+ 'code_content' => 'Kod İçeriği',
+ 'code_save' => 'Kodu Kaydet',
+];
--- /dev/null
+<?php
+/**
+ * Text used for 'Entities' (Document Structure Elements) such as
+ * Books, Shelves, Chapters & Pages
+ */
+return [
+
+ // Shared
+ 'recently_created' => 'Yakın Zamanda Oluşturuldu',
+ 'recently_created_pages' => 'Yakın Zamanda Oluşturulmuş Sayfalar',
+ 'recently_updated_pages' => 'Yakın Zamanda Güncellenmiş Sayfalar',
+ 'recently_created_chapters' => 'Yakın Zamanda Oluşturulmuş Bölümler',
+ 'recently_created_books' => 'Yakın Zamanda Olşturulmuş Kitaplar',
+ 'recently_created_shelves' => 'Yakın Zamanda Oluşturulmuş Kitaplıklar',
+ 'recently_update' => 'Yakın Zamanda Güncellenmiş',
+ 'recently_viewed' => 'Yakın Zamanda Görüntülenmiş',
+ 'recent_activity' => 'Son Hareketler',
+ 'create_now' => 'Hemen bir tane oluştur',
+ 'revisions' => 'Revizyonlar',
+ 'meta_revision' => 'Revizyon #:revisionCount',
+ 'meta_created' => 'Oluşturuldu :timeLength',
+ 'meta_created_name' => ':user tarafından :timeLength tarihinde oluşturuldu',
+ 'meta_updated' => 'Güncellendi :timeLength',
+ 'meta_updated_name' => ':user tarafından :timeLength tarihinde güncellendi',
+ 'entity_select' => 'Öğe Seçme',
+ 'images' => 'Görseller',
+ 'my_recent_drafts' => 'Son Taslaklarım',
+ 'my_recently_viewed' => 'Son Görüntülemelerim',
+ 'no_pages_viewed' => 'Herhangi bir sayfa görüntülemediniz',
+ 'no_pages_recently_created' => 'Yakın zamanda bir sayfa oluşturulmadı',
+ 'no_pages_recently_updated' => 'Yakın zamanda bir sayfa güncellenmedi',
+ 'export' => 'Dışa Aktar',
+ 'export_html' => 'Contained Web Dosyası',
+ 'export_pdf' => 'PDF Dosyası',
+ 'export_text' => 'Düz Metin Dosyası',
+
+ // Permissions and restrictions
+ 'permissions' => 'İzinler',
+ 'permissions_intro' => 'Etkinleştirildikten sonra bu izinler diğer diğer bütün izinlerden öncelikli olacaktır.',
+ 'permissions_enable' => 'Özelleştirilmiş Yetkileri Etkinleştir',
+ 'permissions_save' => 'İzinleri Kaydet',
+
+ // Search
+ 'search_results' => 'Arama Sonuçları',
+ 'search_total_results_found' => ':count sonuç bulundu |:count toplam sonuç bulundu',
+ 'search_clear' => 'Aramaları Temizle',
+ 'search_no_pages' => 'Bu aramayla herhangi bir sonuç eşleşmedi',
+ 'search_for_term' => ':term için ara',
+ 'search_more' => 'Daha Fazla Sonuç',
+ 'search_filters' => 'Arama Filtreleri',
+ 'search_content_type' => 'İçerik Türü',
+ 'search_exact_matches' => 'Tam Eşleşmeler',
+ 'search_tags' => 'Etiket Aramaları',
+ 'search_options' => 'Ayarlar',
+ 'search_viewed_by_me' => 'Benim tarafımdan görüntülendi',
+ 'search_not_viewed_by_me' => 'Benim tarafımdan görüntülenmedi',
+ 'search_permissions_set' => 'İzinler ayarlandı',
+ 'search_created_by_me' => 'Benim tarafımdan oluşturuldu',
+ 'search_updated_by_me' => 'Benim tarafımdan güncellendi',
+ 'search_date_options' => 'Tarih Seçenekleri',
+ 'search_updated_before' => 'Önce güncellendi',
+ 'search_updated_after' => 'Sonra güncellendi',
+ 'search_created_before' => 'Önce oluşturuldu',
+ 'search_created_after' => 'Sonra oluşturuldu',
+ 'search_set_date' => 'Tarih Ayarla',
+ 'search_update' => 'Aramayı Güncelle',
+
+ // Shelves
+ 'shelf' => 'Kitaplık',
+ 'shelves' => 'Kitaplıklar',
+ 'x_shelves' => ':count Kitaplık|:count Kitaplıklar',
+ 'shelves_long' => 'Kitaplıklar',
+ 'shelves_empty' => 'Hiç kitaplık oluşturulmadı',
+ 'shelves_create' => 'Yeni Kitaplık Oluştur',
+ 'shelves_popular' => 'Popüler Kitaplıklar',
+ 'shelves_new' => 'Yeni Kitaplıklar',
+ 'shelves_new_action' => 'Yeni Kitaplık',
+ 'shelves_popular_empty' => 'En popüler kitaplıklar burada görüntülenecek.',
+ 'shelves_new_empty' => 'En son oluşturulmuş kitaplıklar burada görüntülenecek.',
+ 'shelves_save' => 'Kitaplığı Kaydet',
+ 'shelves_books' => 'Bu kitaplıktaki kitaplar',
+ 'shelves_add_books' => 'Bu kitaplığa kitap ekle',
+ 'shelves_drag_books' => 'Bu kitaplığa kitap eklemek için kitapları buraya sürükle',
+ 'shelves_empty_contents' => 'Bu kitaplığa henüz hiç bir kitap atanmamış',
+ 'shelves_edit_and_assign' => 'Kitaplığa kitap eklemek için güncelle',
+ 'shelves_edit_named' => ':name Kitaplığını Güncelle',
+ 'shelves_edit' => 'Kitaplığı Güncelle',
+ 'shelves_delete' => 'Kitaplığı Sil',
+ 'shelves_delete_named' => ':name Kitaplığını Sil',
+ 'shelves_delete_explain' => "Bu işlem :name kitaplığını silecektir. İçerdiği kitaplar silinmeyecektir.",
+ 'shelves_delete_confirmation' => 'Bu kitaplığı silmek istediğinizden emin misiniz?',
+ 'shelves_permissions' => 'Kitaplık İzinleri',
+ 'shelves_permissions_updated' => 'Kitaplık İzinleri Güncellendi',
+ 'shelves_permissions_active' => 'Kitaplık İzinleri Aktif',
+ 'shelves_copy_permissions_to_books' => 'İzinleri Kitaplara Kopyala',
+ 'shelves_copy_permissions' => 'İzinleri Kopyala',
+ 'shelves_copy_permissions_explain' => 'Bu işlem sonucunda kitaplığınızın izinleri içerdiği kitaplara da aynen uygulanır. Aktifleştirmeden bu kitaplığa ait izinleri kaydettiğinizden emin olun.',
+ 'shelves_copy_permission_success' => 'Kitaplık izinleri :count adet kitaba kopyalandı',
+
+ // Books
+ 'book' => 'Kitap',
+ 'books' => 'Kitaplar',
+ 'x_books' => ':count Kitap|:count Kitaplar',
+ 'books_empty' => 'Hiç kitap oluşturulmadı',
+ 'books_popular' => 'Popüler Kitaplar',
+ 'books_recent' => 'En Son Kitaplar',
+ 'books_new' => 'Yeni Kitaplar',
+ 'books_new_action' => 'Yeni Kitap',
+ 'books_popular_empty' => 'En popüler kitaplar burada görüntülenecek.',
+ 'books_new_empty' => 'En yeni kitaplar burada görüntülenecek.',
+ 'books_create' => 'Yeni Kitap Oluştur',
+ 'books_delete' => 'Kitabı Sil',
+ 'books_delete_named' => ':bookName kitabını sil',
+ 'books_delete_explain' => 'Bu işlem \':bookName\' kitabını silecek. Bütün sayfa ve bölümler silinecektir.',
+ 'books_delete_confirmation' => 'Bu kitabı silmek istediğinizden emin misiniz?',
+ 'books_edit' => 'Kitabı Güncelle',
+ 'books_edit_named' => ':bookName Kitabını Güncelle',
+ 'books_form_book_name' => 'Kitap Adı',
+ 'books_save' => 'Kitabı Kaydet',
+ 'books_permissions' => 'Kitap İzinleri',
+ 'books_permissions_updated' => 'Kitap İzinleri Güncellendi',
+ 'books_empty_contents' => 'Bu kitaba ait sayfa veya bölüm oluşturulmadı.',
+ 'books_empty_create_page' => 'Yeni sayfa oluştur',
+ 'books_empty_sort_current_book' => 'Mevcut kitabı sırala',
+ 'books_empty_add_chapter' => 'Yeni bölüm ekle',
+ 'books_permissions_active' => 'Kitap İzinleri Aktif',
+ 'books_search_this' => 'Bu kitapta ara',
+ 'books_navigation' => 'Kitap Navigasyonu',
+ 'books_sort' => 'Kitap İçeriklerini Sırala',
+ 'books_sort_named' => ':bookName Kitabını Sırala',
+ 'books_sort_name' => 'İsme Göre Sırala',
+ 'books_sort_created' => 'Oluşturulma Tarihine Göre Sırala',
+ 'books_sort_updated' => 'Güncellenme Tarihine Göre Sırala',
+ 'books_sort_chapters_first' => 'Önce Bölümler',
+ 'books_sort_chapters_last' => 'En Son Bölümler',
+ 'books_sort_show_other' => 'Diğer Kitapları Göster',
+ 'books_sort_save' => 'Yeni Düzeni Kaydet',
+
+ // Chapters
+ 'chapter' => 'Bölüm',
+ 'chapters' => 'Bölümler',
+ 'x_chapters' => ':count Bölüm|:count Bölümler',
+ 'chapters_popular' => 'Popüler Bölümler',
+ 'chapters_new' => 'Yeni Bölüm',
+ 'chapters_create' => 'Yeni Bölüm Oluştur',
+ 'chapters_delete' => 'Bölümü Sil',
+ 'chapters_delete_named' => ':chapterName Bölümünü Sil',
+ 'chapters_delete_explain' => 'Bu işlem \':chapterName\' kitabını silecek. Bütün sayfalar silinecek ve direkt olarak ana kitab eklenecektir.',
+ 'chapters_delete_confirm' => 'Bölümü silmek istediğinizden emin misiniz?',
+ 'chapters_edit' => 'Bölümü Güncelle',
+ 'chapters_edit_named' => ':chapterName Bölümünü Güncelle',
+ 'chapters_save' => 'Bölümü Kaydet',
+ 'chapters_move' => 'Bölümü Taşı',
+ 'chapters_move_named' => ':chapterName Bölümünü Taşı',
+ 'chapter_move_success' => 'Bölüm :bookName Kitabına Taşındı',
+ 'chapters_permissions' => 'Bölüm İzinleri',
+ 'chapters_empty' => 'Bu bölümde henüz bir sayfa yok.',
+ 'chapters_permissions_active' => 'Bölüm İzinleri Aktif',
+ 'chapters_permissions_success' => 'Bölüm İzinleri Güncellendi',
+ 'chapters_search_this' => 'Bu bölümü ara',
+
+ // Pages
+ 'page' => 'Sayfa',
+ 'pages' => 'Sayfalar',
+ 'x_pages' => ':count Sayfa|:count Sayfalar',
+ 'pages_popular' => 'Popüler Sayfalar',
+ 'pages_new' => 'Yeni Sayfa',
+ 'pages_attachments' => 'Ekler',
+ 'pages_navigation' => 'Sayfa Navigasyonu',
+ 'pages_delete' => 'Sayfayı Sil',
+ 'pages_delete_named' => ':pageName Sayfasını Sil',
+ 'pages_delete_draft_named' => ':pageName Taslak Sayfasını Sil',
+ 'pages_delete_draft' => 'Taslak Sayfayı Sil',
+ 'pages_delete_success' => 'Sayfa silindi',
+ 'pages_delete_draft_success' => 'Taslak sayfa silindi',
+ 'pages_delete_confirm' => 'Bu sayfayı silmek istediğinizden emin misiniz?',
+ 'pages_delete_draft_confirm' => 'Bu taslak sayfayı silmek istediğinizden emin misiniz?',
+ 'pages_editing_named' => ':pageName Sayfası Düzenleniyor',
+ 'pages_edit_save_draft' => 'Taslağı Kaydet',
+ 'pages_edit_draft' => 'Taslak Sayfasını Düzenle',
+ 'pages_editing_draft' => 'Taslak Düzenleniyor',
+ 'pages_editing_page' => 'Sayfa Düzenleniyor',
+ 'pages_edit_draft_save_at' => 'Taslak kaydedildi ',
+ 'pages_edit_delete_draft' => 'Taslağı Sl',
+ 'pages_edit_discard_draft' => 'Taslağı Yoksay',
+ 'pages_edit_set_changelog' => 'Değişiklik Logunu Kaydet',
+ 'pages_edit_enter_changelog_desc' => 'Yaptığınız değişiklikler hakkında kısa bir bilgilendirme ekleyin',
+ 'pages_edit_enter_changelog' => 'Değişim Günlüğü Ekleyin',
+ 'pages_save' => 'Sayfayı Kaydet',
+ 'pages_title' => 'Sayfa Başlığı',
+ 'pages_name' => 'Sayfa İsmi',
+ 'pages_md_editor' => 'Editör',
+ 'pages_md_preview' => 'Önizleme',
+ 'pages_md_insert_image' => 'Görsel Ekle',
+ 'pages_md_insert_link' => 'Öge Linki Ekle',
+ 'pages_md_insert_drawing' => 'Çizim Ekle',
+ 'pages_not_in_chapter' => 'Sayfa Bu Bölümde Değil',
+ 'pages_move' => 'Sayfayı Taşı',
+ 'pages_move_success' => 'Sayfa ":parentName"\'a taşındı',
+ 'pages_copy' => 'Sayfayı Kopyala',
+ 'pages_copy_desination' => 'Kopyalanacak Hedef',
+ 'pages_copy_success' => 'Sayfa başarıyla kopyalandı',
+ 'pages_permissions' => 'Sayfa İzinleri',
+ 'pages_permissions_success' => 'Sayfa izinleri güncellendi',
+ 'pages_revision' => 'Revizyon',
+ 'pages_revisions' => 'Sayfa Revizyonları',
+ 'pages_revisions_named' => ':pageName için Sayfa Revizyonları',
+ 'pages_revision_named' => ':pageName için Sayfa Revizyonu',
+ 'pages_revisions_created_by' => 'Oluşturan',
+ 'pages_revisions_date' => 'Revizyon Tarihi',
+ 'pages_revisions_number' => '#',
+ 'pages_revisions_numbered' => 'Revizyon #:id',
+ 'pages_revisions_numbered_changes' => 'Revizyon #:id Değişiklikleri',
+ 'pages_revisions_changelog' => 'Değişim Günlüğü',
+ 'pages_revisions_changes' => 'Değişiklikler',
+ 'pages_revisions_current' => 'Mevcut Versiyon',
+ 'pages_revisions_preview' => 'Önizleme',
+ 'pages_revisions_restore' => 'Kurtar',
+ 'pages_revisions_none' => 'Bu sayfaya ait revizyon yok',
+ 'pages_copy_link' => 'Linki kopyala',
+ 'pages_edit_content_link' => 'İçeriği Düzenle',
+ 'pages_permissions_active' => 'Sayfa İzinleri Aktif',
+ 'pages_initial_revision' => 'İlk Yayın',
+ 'pages_initial_name' => 'Yeni Sayfa',
+ 'pages_editing_draft_notification' => 'Şu anda :timeDiff tarhinde kaydedilmiş olan taslağı düzenlemektesiniz.',
+ 'pages_draft_edited_notification' => 'Bu sayfa son girişinizden bu yana güncellendi. Değişiklikleri yoksayıp, kaydetmeden çıkmanız önerilir.',
+ 'pages_draft_edit_active' => [
+ 'start_a' => ':count kullanıcı bu sayfayı düzenlemeye başladı',
+ 'start_b' => ':userName kullanıcısı bu sayfayı düzenlemeye başladı',
+ 'time_a' => 'sayfa son güncellendiğinden beri',
+ 'time_b' => 'son :minCount dakikada',
+ 'message' => ':start :time. Birbirinizin düzenlemelerinin çakışmamasına dikkat edin!',
+ ],
+ 'pages_draft_discarded' => 'Taslak yok sayıldı, editör mevcut sayfa içeriği ile güncellendi',
+ 'pages_specific' => 'Özel Sayfa',
+
+ // Editor Sidebar
+ 'page_tags' => 'Sayfa Etiketleri',
+ 'chapter_tags' => 'Bölüm Etiketleri',
+ 'book_tags' => 'Kitap Etiketleri',
+ 'shelf_tags' => 'Kitaplık Etiketleri',
+ 'tag' => 'Etiket',
+ 'tags' => 'Etiketler',
+ 'tag_value' => 'Etiket İçeriği (Opsiyonel)',
+ 'tags_explain' => "İçeriğini daha iyi kategorize etmek için bazı etiketler ekle. Etiketlere değer atayarak daha derin bir organizasyon yapısına sahip olabilirsin.",
+ 'tags_add' => 'Başka etiket ekle',
+ 'attachments' => 'Ekler',
+ 'attachments_explain' => 'Sayfanızda göstermek için bazı dosyalar yükleyin veya bazı bağlantılar ekleyin. Bunlar sayfanın sidebarında görülebilir.',
+ 'attachments_explain_instant_save' => 'Burada yapılan değişiklikler anında kaydedilir.',
+ 'attachments_items' => 'Eklenmiş Ögeler',
+ 'attachments_upload' => 'Dosya Yükle',
+ 'attachments_link' => 'Link Ekle',
+ 'attachments_set_link' => 'Link Düzenle',
+ 'attachments_delete_confirm' => 'Eki gerçekten silmek istiyor musunuz?',
+ 'attachments_dropzone' => 'Dosyaları buraya sürükle veya eklemek için buraya tıkla',
+ 'attachments_no_files' => 'Hiç bir dosya yüklenmedi',
+ 'attachments_explain_link' => 'Eğer dosya yüklememeyi tercih ederseniz link ekleyebilirsiniz. Bu başka bir sayfaya veya buluttaki bir dosyanın linki olabilir.',
+ 'attachments_link_name' => 'Bağlantı Adı',
+ 'attachment_link' => 'Ek linki',
+ 'attachments_link_url' => 'Dosya linki',
+ 'attachments_link_url_hint' => 'Dosyanın veya sitenin url adres',
+ 'attach' => 'Ekle',
+ 'attachments_edit_file' => 'Dosyayı Düzenle',
+ 'attachments_edit_file_name' => 'Dosya Adı',
+ 'attachments_edit_drop_upload' => 'Dosyaları sürükle veya yüklemek için buraya tıkla',
+ 'attachments_order_updated' => 'Ek sırası güncellendi',
+ 'attachments_updated_success' => 'Ek detayları güncellendi',
+ 'attachments_deleted' => 'Ek silindi',
+ 'attachments_file_uploaded' => 'Dosya başarıyla yüklendi',
+ 'attachments_file_updated' => 'Dosya başarıyla güncellendi',
+ 'attachments_link_attached' => 'Link sayfaya başarıyla eklendi',
+
+ // Profile View
+ 'profile_user_for_x' => 'Kullanıcı :time',
+ 'profile_created_content' => 'Oluşturulan İçerik',
+ 'profile_not_created_pages' => ':userName herhangi bir sayfa oluşturmadı',
+ 'profile_not_created_chapters' => ':userName herhangi bir bölüm oluşturmadı',
+ 'profile_not_created_books' => ':userName herhangi bir kitap oluşturmadı',
+ 'profile_not_created_shelves' => ':userName herhangi bir kitaplık oluşturmadı',
+
+ // Comments
+ 'comment' => 'Yorum',
+ 'comments' => 'Yorumlar',
+ 'comment_add' => 'Yorum Ekle',
+ 'comment_placeholder' => 'Buraya yorum ekle',
+ 'comment_count' => '{0} Yorum Yok|{1} 1 Yorum|[2,*] :count Yorun',
+ 'comment_save' => 'Yorum Kaydet',
+ 'comment_saving' => 'Yorum kaydediliyor...',
+ 'comment_deleting' => 'Yorum siliniyor...',
+ 'comment_new' => 'Yeni Yorum',
+ 'comment_created' => 'yorum yaptı :createDiff',
+ 'comment_updated' => ':username tarafından :updateDiff önce güncellendi',
+ 'comment_deleted_success' => 'Yorum silindi',
+ 'comment_created_success' => 'Yorum eklendi',
+ 'comment_updated_success' => 'Yorum güncellendi',
+ 'comment_delete_confirm' => 'Bu yorumu silmek istediğinizden emin misiniz?',
+ 'comment_in_reply_to' => ':commentId yorumuna yanıt olarak',
+
+ // Revision
+ 'revision_delete_confirm' => 'Bu revizyonu silmek istediğinizden emin misiniz?',
+ 'revision_restore_confirm' => 'Bu revizyonu yeniden yüklemek istediğinizden emin misiniz? Mevcut sayfa içeriği değiştirilecektir.',
+ 'revision_delete_success' => 'Revizyon silindi',
+ 'revision_cannot_delete_latest' => 'Son revizyon silinemez.'
+];
\ No newline at end of file
--- /dev/null
+<?php
+/**
+ * Text shown in error messaging.
+ */
+return [
+
+ // Permissions
+ 'permission' => 'Bu sayfaya erişme yetkiniz yok.',
+ 'permissionJson' => 'Bu işlemi yapmak için yetkiniz yo.',
+
+ // Auth
+ 'error_user_exists_different_creds' => ':email adresi farklı kullanıcı bilgileri ile zaten kullanımda.',
+ 'email_already_confirmed' => 'E-mail halihazırda onaylanmış, giriş yapmayı dene.',
+ 'email_confirmation_invalid' => 'Bu doğrulama tokenı daha önce kullanılmış veya geçerli değil, lütfen tekrar kayıt olmayı deneyin.',
+ 'email_confirmation_expired' => 'Doğrulama token\'ının süresi geçmiş, yeni bir mail gönderildi.',
+ 'ldap_fail_anonymous' => 'Anonim LDAP girişi başarısız oldu',
+ 'ldap_fail_authed' => 'Verdiğiniz bilgiler ile LDAP girişi başarısız oldu.',
+ 'ldap_extension_not_installed' => 'LDAP PHP eklentisi yüklenmedi',
+ 'ldap_cannot_connect' => 'LDAP sunucusuna bağlanılamadı, ilk bağlantı başarısız oldu',
+ 'social_no_action_defined' => 'Bir aksiyon tanımlanmadı',
+ 'social_login_bad_response' => ":socialAccount girişi sırasında hata oluştu: \n:error",
+ 'social_account_in_use' => 'Bu :socialAccount zaten kullanımda, :socialAccount hesabıyla giriş yapmayı deneyin.',
+ 'social_account_email_in_use' => ':email adresi zaten kullanımda. Eğer zaten bir hesabınız varsa :socialAccount hesabınızı profil ayarları kısmından bağlayabilirsiniz.',
+ 'social_account_existing' => 'Bu :socialAccount zaten profilinize eklenmiş.',
+ 'social_account_already_used_existing' => 'Bu :socialAccount başka bir kullanıcı tarafından kullanılıyor.',
+ 'social_account_not_used' => 'Bu :socialAccount hesabı hiç bir kullanıcıya bağlı değil. Lütfen profil ayarlarına gidiniz ve bağlayınız. ',
+ 'social_account_register_instructions' => 'Hala bir hesabınız yoksa :socialAccount ile kayıt olabilirsiniz.',
+ 'social_driver_not_found' => 'Social driver bulunamadı',
+ 'social_driver_not_configured' => ':socialAccount ayarlarınız doğru bir şekilde ayarlanmadı.',
+
+ // System
+ 'path_not_writable' => ':filePath dosya yolu yüklenemedi. Sunucuya yazılabilir olduğundan emin olun.',
+ 'cannot_get_image_from_url' => ':url\'den görsel alınamadı',
+ 'cannot_create_thumbs' => 'Sunucu küçük resimleri oluşturamadı. Lütfen GD PHP eklentisinin yüklü olduğundan emin olun.',
+ 'server_upload_limit' => 'Sunucu bu boyutta dosya yüklemenize izin vermiyor. Lütfen daha küçük boyutta dosya yüklemeyi deneyiniz.',
+ 'uploaded' => 'Sunucu bu boyutta dosya yüklemenize izin vermiyor. Lütfen daha küçük boyutta dosya yüklemeyi deneyiniz.',
+ 'image_upload_error' => 'Görsel yüklenirken bir hata oluştu',
+ 'image_upload_type_error' => 'Yüklemeye çalıştığınız dosya türü geçerli değildir',
+ 'file_upload_timeout' => 'Dosya yüklemesi zaman aşımına uğradı',
+
+ // Attachments
+ 'attachment_page_mismatch' => 'Ek güncellemesi sırasında sayfa uyuşmazlığı yaşandı',
+ 'attachment_not_found' => 'Ek bulunamadı',
+
+ // Pages
+ 'page_draft_autosave_fail' => 'Taslak kaydetme başarısız. Sayfanızı kaydetmeden önce internet bağlantınız olduğundan emin olun',
+ 'page_custom_home_deletion' => 'Bu sayfa anasayfa olarak ayarlandığı için silinemez',
+
+ // Entities
+ 'entity_not_found' => 'Eleman bulunamadı',
+ 'bookshelf_not_found' => 'Kitaplık bulunamadı',
+ 'book_not_found' => 'Kitap bulunamadı',
+ 'page_not_found' => 'Sayfa bulunamadı',
+ 'chapter_not_found' => 'Bölüm bulunamadı',
+ 'selected_book_not_found' => 'Seçilen kitap bulunamadı',
+ 'selected_book_chapter_not_found' => 'Seçilen kitap veya bölüm bulunamadı',
+ 'guests_cannot_save_drafts' => 'Misafirler taslak kaydedemezler',
+
+ // Users
+ 'users_cannot_delete_only_admin' => 'Tek olan yöneticiyi silemezsiniz',
+ 'users_cannot_delete_guest' => 'Misafir kullanıyıcıyı silemezsiniz',
+
+ // Roles
+ 'role_cannot_be_edited' => 'Bu rol düzenlenemez',
+ 'role_system_cannot_be_deleted' => 'Bu bir yönetici rolüdür ve silinemez',
+ 'role_registration_default_cannot_delete' => 'Bu rol varsayılan yönetici rolü olarak atandığı için silinemez ',
+ 'role_cannot_remove_only_admin' => 'Bu kullanıcı yönetici rolü olan tek kullanıcı olduğu için silinemez. Bu kullanıcıyı silmek için önce başka bir kullanıcıya yönetici rolü atayın.',
+
+ // Comments
+ 'comment_list' => 'Yorumlar yüklenirken bir hata oluştu.',
+ 'cannot_add_comment_to_draft' => 'Taslaklara yorum ekleyemezsiniz.',
+ 'comment_add' => 'Yorum eklerken/güncellerken bir hata olıuştu.',
+ 'comment_delete' => 'Yorum silinirken bir hata oluştu.',
+ 'empty_comment' => 'Boş bir yorum eklenemez.',
+
+ // Error pages
+ '404_page_not_found' => 'Sayfa Bulunamadı',
+ 'sorry_page_not_found' => 'Üzgünüz, aradığınız sayfa bulunamıyor.',
+ 'return_home' => 'Anasayfaya dön',
+ 'error_occurred' => 'Bir Hata Oluştu',
+ 'app_down' => ':appName şu anda inaktif',
+ 'back_soon' => 'En kısa zamanda aktif hale gelecek.',
+
+];
--- /dev/null
+<?php
+/**
+ * Pagination Language Lines
+ * The following language lines are used by the paginator library to build
+ * the simple pagination links.
+ */
+return [
+
+ 'previous' => '« Önceki',
+ 'next' => 'Sonraki »',
+
+];
--- /dev/null
+<?php
+/**
+ * Password Reminder Language Lines
+ * The following language lines are the default lines which match reasons
+ * that are given by the password broker for a password update attempt has failed.
+ */
+return [
+
+ 'password' => 'Parolanız en az 6 karakterden oluşmalı ve doğrulama parolası ile eşleşmelidir. ',
+ 'user' => "Bu e-mail adresi ile ilişkilendirilmiş bir kullanıcı bulamadık.",
+ 'token' => 'Parola yenileme tokeni geçerli değil.',
+ 'sent' => 'Parola sıfırlanma bağlantısını e-mail adresinize gönderdik!',
+ 'reset' => 'Parolanız sıfırlandı!',
+
+];
--- /dev/null
+<?php
+/**
+ * Settings text strings
+ * Contains all text strings used in the general settings sections of BookStack
+ * including users and roles.
+ */
+return [
+
+ // Common Messages
+ 'settings' => 'Ayarlar',
+ 'settings_save' => 'Ayarları Kaydet',
+ 'settings_save_success' => 'Ayarlar Kaydedildi',
+
+ // App Settings
+ 'app_customization' => 'Özelleştirme',
+ 'app_features_security' => 'Özellikler & Güvenlik',
+ 'app_name' => 'Uygulama Adı',
+ 'app_name_desc' => 'Bu isim başlıkta ve sistem tarafında gönderilen tüm mesajlarda gösterilecektir.',
+ 'app_name_header' => 'İsmi başlıkta göster',
+ 'app_public_access' => 'Açık Erişim',
+ 'app_public_access_desc' => 'Bu özelliği aktif etmek giriş yapmamış misafir kullanıcıların sizin BookStack uygulamanıza erişmesini sağlar',
+ 'app_public_access_desc_guest' => 'Kayıtlı olmayan kullanıcılar için erişim yetkisi "Guest" kullanıcısı üzerinden düzenlenebilir.',
+ 'app_public_access_toggle' => 'Açık erişime izin ver',
+ 'app_public_viewing' => 'Herkese açık görüntülenmeye izin verilsin mi?',
+ 'app_secure_images' => 'Daha Yüksek Güvenlikli Görsel Yüklemeleri',
+ 'app_secure_images_toggle' => 'Daha yüksek güveblikli görsel yüklemelerine izin ver',
+ 'app_secure_images_desc' => 'Performans sebepleri nedeniyle bütün görseller halka açık. Bu opsiyon rastgele ve tahmin edilmesi zor dizileri görsel linklerinin önüne ekler. Dizin indexlerinin kapalı olduğundan emin olun.',
+ 'app_editor' => 'Sayfa Editörü',
+ 'app_editor_desc' => 'Sayfa düzenlemesi yapılırken hangi editörün kullanılacağını seçin.',
+ 'app_custom_html' => 'Özel HTML Head İçeriği',
+ 'app_custom_html_desc' => 'Buraya eklenecek olan içerik <head> taginin en sonuna eklenecektir. Bu stilleri override ederken veya analytics eklerken faydalı bir kullanım şeklidir.',
+ 'app_custom_html_disabled_notice' => 'Yapılan hatalı değişikliklerin geriye alınabilmesi için bu sayfada özel HTML head içeriği kapalı.',
+ 'app_logo' => 'Uygulama Logosu',
+ 'app_logo_desc' => 'Bu görsel 43px yüksekliğinde olmalı. <br>Büyük görseller ölçeklenecektir.',
+ 'app_primary_color' => 'Uygulamanın Birincil Rengi',
+ 'app_primary_color_desc' => 'Bu bir hex değeri olmalıdır. <br>Varsayılan rengi seçmek için boş bırakın.',
+ 'app_homepage' => 'Uygulama Anasayfası',
+ 'app_homepage_desc' => 'Anasayfada görünmesi için bir view seçin. Sayfa izinleri seçili sayfalar için yok sayılacaktır.',
+ 'app_homepage_select' => 'Sayfa seçiniz',
+ 'app_disable_comments' => 'Yorumları Engelle',
+ 'app_disable_comments_toggle' => 'Yorumları engelle',
+ 'app_disable_comments_desc' => 'Yorumları uygulamadaki bütün sayfalar için engelle. <br> Mevcut yorumlar gösterilmeyecektir.',
+
+ // Registration Settings
+ 'reg_settings' => 'Kayıt',
+ 'reg_enable' => 'Kaydolmaya İzin Ver',
+ 'reg_enable_toggle' => 'Kaydolmaya izin ver',
+ 'reg_enable_desc' => 'Kayıt olmaya izin verdiğinizde kullanıcılar kendilerini uygulamaya kaydedebilecekler. Kayıt olduktan sonra kendilerine varsayılan kullanıcı rolü atanacaktır.',
+ 'reg_default_role' => 'Kayıt olduktan sonra varsayılan kullanıcı rolü',
+ 'reg_email_confirmation' => 'Email Doğrulama',
+ 'reg_email_confirmation_toggle' => 'E-mail onayı gerektir',
+ 'reg_confirm_email_desc' => 'Eğer domain kısıtlaması kullanılıyorsa o zaman email doğrulaması gereklidir ve bu seçenek yok sayılacaktır.',
+ 'reg_confirm_restrict_domain' => 'Domain Kısıtlaması',
+ 'reg_confirm_restrict_domain_desc' => 'Kısıtlamak istediğiniz email domainlerini vigül ile ayırarak yazınız. Kullanıcılara uygulamaya erişmeden önce adreslerini doğrulamak için bir mail gönderilecektir. <br> Kullanıcılar başarıyla kaydolduktan sonra email adreslerini değiştiremeyeceklerdir.',
+ 'reg_confirm_restrict_domain_placeholder' => 'Hiçbir kısıtlama tanımlanmamış',
+
+ // Maintenance settings
+ 'maint' => 'Bakım',
+ 'maint_image_cleanup' => 'Görsel Temizliği',
+ 'maint_image_cleanup_desc' => "Sayfaları ve revizyon içeriklerini tarayarak hangi gösel ve çizimlerin kullanımda olduğunu ve hangilerinin gereksiz olduğunu tespit eder. Bunu başlatmadan veritabanı ve görsellerin tam bir yedeğinin alındığından emin olun.",
+ 'maint_image_cleanup_ignore_revisions' => 'Revizyonlardaki görselleri yoksay',
+ 'maint_image_cleanup_run' => 'Temizliği Başlat',
+ 'maint_image_cleanup_warning' => ':count potansiyel kullanılmayan görsel bulundu. Bu görselleri silmek istediğinizden emin misiniz?',
+ 'maint_image_cleanup_success' => ':count potanisyel kullanılmayan görsel bulundu ve silindi!',
+ 'maint_image_cleanup_nothing_found' => 'Kullanılmayan görsel bulunamadı ve birşey silinmedi!',
+
+ // Role Settings
+ 'roles' => 'Roller',
+ 'role_user_roles' => 'Kullanıcı Rolleri',
+ 'role_create' => 'Yeni Rol Oluştur',
+ 'role_create_success' => 'Rol Başarıyla Oluşturuldu',
+ 'role_delete' => 'Rolü Sil',
+ 'role_delete_confirm' => 'Bu işlem \':roleName\' rolünü silecektir.',
+ 'role_delete_users_assigned' => 'Bu role atanmış :userCount adet kullanıcı var. Eğer bu kullanıcıların rollerini değiştirmek istiyorsanız aşağıdan yeni bir rol seçin.',
+ 'role_delete_no_migration' => "Kullanıcıları taşıma",
+ 'role_delete_sure' => 'Bu rolü silmek istediğinizden emin misiniz?',
+ 'role_delete_success' => 'Rol başarıyla silindi',
+ 'role_edit' => 'Rolü Düzenle',
+ 'role_details' => 'Rol Detayları',
+ 'role_name' => 'Rol Adı',
+ 'role_desc' => 'Rolün Kısa Tanımı',
+ 'role_external_auth_id' => 'Harici Authentication ID\'leri',
+ 'role_system' => 'Sistem Yetkileri',
+ 'role_manage_users' => 'Kullanıcıları yönet',
+ 'role_manage_roles' => 'Rolleri ve rol izinlerini yönet',
+ 'role_manage_entity_permissions' => 'Bütün kitap, bölüm ve sayfa izinlerini yönet',
+ 'role_manage_own_entity_permissions' => 'Sahip olunan kitap, bölüm ve sayfaların izinlerini yönet',
+ 'role_manage_settings' => 'Uygulama ayarlarını yönet',
+ 'role_asset' => 'Asset Yetkileri',
+ 'role_asset_desc' => 'Bu izinleri assetlere sistem içinden varsayılan erişimi kontrol eder. Kitaplar, bölümler ve sayfaların izinleri bu izinleri override eder.',
+ 'role_asset_admins' => 'Yöneticilere otomatik olarak bütün içeriğe erişim yetkisi verilir fakat bu opsiyonlar UI özelliklerini gösterir veya gizler.',
+ 'role_all' => 'Hepsi',
+ 'role_own' => 'Sahip Olunan',
+ 'role_controlled_by_asset' => 'Yükledikleri asset tarafından kontrol ediliyor',
+ 'role_save' => 'Rolü Kaydet',
+ 'role_update_success' => 'Rol başarıyla güncellendi',
+ 'role_users' => 'Bu roldeki kullanıcılar',
+ 'role_users_none' => 'Bu role henüz bir kullanıcı atanmadı',
+
+ // Users
+ 'users' => 'Kullanıcılar',
+ 'user_profile' => 'Kullanıcı Profili',
+ 'users_add_new' => 'Yeni Kullanıcı Ekle',
+ 'users_search' => 'Kullanıcıları Ara',
+ 'users_details' => 'Kullanıcı Detayları',
+ 'users_details_desc' => 'Bu kullanıcı için gösterilecek bir isim ve mail adresi belirleyin. Bu e-mail adresi kullanıcı tarafından giriş yaparken kullanılacak.',
+ 'users_details_desc_no_email' => 'Diğer kullanıcılar tarafından tanınabilmesi için bir isim belirleyin.',
+ 'users_role' => 'Kullanıcı Rolleri',
+ 'users_role_desc' => 'Bu kullanıcının hangi rollere atanabileceğini belirleyin. Eğer bir kullanıcıya birden fazla rol atanırsa, kullanıcı bütün rollerin özelliklerini kullanabilir.',
+ 'users_password' => 'Kullanıcı Parolası',
+ 'users_password_desc' => 'Kullanıcının giriş yaparken kullanacağı bir parola belirleyin. Parola en az 5 karakter olmalıdır.',
+ 'users_external_auth_id' => 'Harici Authentication ID\'si',
+ 'users_external_auth_id_desc' => 'Bu ID kullanıcı LDAP sunucu ile bağlantı kurarken kullanılır.',
+ 'users_password_warning' => 'Sadece parolanızı değiştirmek istiyorsanız aşağıyı doldurunuz.',
+ 'users_system_public' => 'Bu kullanıcı sizin uygulamanızı ziyaret eden bütün misafir kullanıcıları temsil eder. Giriş yapmak için kullanılamaz, otomatik olarak atanır.',
+ 'users_delete' => 'Kullanıcı Sil',
+ 'users_delete_named' => ':userName kullanıcısını sil ',
+ 'users_delete_warning' => 'Bu işlem \':userName\' kullanıcısını sistemden tamamen silecektir.',
+ 'users_delete_confirm' => 'Bu kullanıcıyı tamamen silmek istediğinize emin misiniz?',
+ 'users_delete_success' => 'Kullanıcılar başarıyla silindi.',
+ 'users_edit' => 'Kullanıcıyı Güncelle',
+ 'users_edit_profile' => 'Profili Düzenle',
+ 'users_edit_success' => 'Kullanıcı başarıyla güncellendi',
+ 'users_avatar' => 'Kullanıcı Avatarı',
+ 'users_avatar_desc' => 'Bu kullanıcıyı temsil eden bir görsel seçin. Yaklaşık 256px kare olmalıdır.',
+ 'users_preferred_language' => 'Tercih Edilen Dil',
+ 'users_preferred_language_desc' => 'Bu seçenek kullanıcı arayüzünün dilini değiştirecektir. Herhangi bir kullanıcı içeriğini etkilemeyecektir.',
+ 'users_social_accounts' => 'Sosyal Hesaplar',
+ 'users_social_accounts_info' => 'Burada diğer hesaplarınızı ekleyerek daha hızlı ve kolay giriş sağlayabilirsiniz. Bir hesabın bağlantısını kesmek daha önce edilnilen erişiminizi kaldırmaz. Profil ayarlarınızdan bağlı sosyal hesabınızın erişimini kaldırınız.',
+ 'users_social_connect' => 'Hesap Bağla',
+ 'users_social_disconnect' => 'Hesabın Bağlantısını Kes',
+ 'users_social_connected' => ':socialAccount hesabı profilinize başarıyla bağlandı.',
+ 'users_social_disconnected' => ':socialAccount hesabınızın profilinizle ilişiği başarıyla kesildi.',
+];
--- /dev/null
+<?php
+/**
+ * Validation Lines
+ * The following language lines contain the default error messages used by
+ * the validator class. Some of these rules have multiple versions such
+ * as the size rules. Feel free to tweak each of these messages here.
+ */
+return [
+
+ // Standard laravel validation lines
+ 'accepted' => ':attribute kabul edilmelidir.',
+ 'active_url' => ':attribute geçerli bir URL adresi değildir.',
+ 'after' => ':attribute :date tarihinden sonra bir tarih olmalıdır.',
+ 'alpha' => ':attribute sadece harflerden oluşabilir.',
+ 'alpha_dash' => ':attribute sadece harf, rakam ve tirelerden oluşabilir.',
+ 'alpha_num' => ':attribute sadece harf ve rakam oluşabilir.',
+ 'array' => ':attribute array olmalıdır..',
+ 'before' => ':attribute :date tarihinden önce bir tarih olmalıdır.',
+ 'between' => [
+ 'numeric' => ':attribute, :min ve :max değerleri arasında olmalıdır.',
+ 'file' => ':attribute, :min ve :max kilobyte boyutları arasında olmalıdır.',
+ 'string' => ':attribute, :min ve :max karakter arasında olmalıdır.',
+ 'array' => ':attribute :min ve :max öge arasında olmalıdır.',
+ ],
+ 'boolean' => ':attribute true veya false olmalıdır.',
+ 'confirmed' => ':attribute doğrulaması eşleşmiyor.',
+ 'date' => ':attribute geçerli bir tarih değil.',
+ 'date_format' => ':attribute formatı :format\'ına uymuyor.',
+ 'different' => ':attribute be :other birbirinden farklı olmalıdır.',
+ 'digits' => ':attribute :digits basamaklı olmalıdır.',
+ 'digits_between' => ':attribute :min ve :max basamaklı olmalıdır.',
+ 'email' => ':attribute geçerli bir e-mail adresi olmalıdır.',
+ 'filled' => ':attribute gerekli bir alandır.',
+ 'exists' => 'Seçilen :attribute geçerli bir alan değildir.',
+ 'image' => ':attribute bir görsel olmalıdır.',
+ 'image_extension' => ':attribute geçerli ve desteklenen bir görsel uzantısı değildir.',
+ 'in' => 'Seçilen :attribute geçerli değildir.',
+ 'integer' => ':attribute bir integer değeri olmalıdır.',
+ 'ip' => ':attribute geçerli bir IP adresi olmalıdır.',
+ 'max' => [
+ 'numeric' => ':attribute, :max değerinden büyük olmamalıdır.',
+ 'file' => ':attribute, :max kilobyte boyutundan büyük olmamalıdır.',
+ 'string' => ':attribute, :max karakter boyutundan büyük olmamalıdır.',
+ 'array' => ':attribute, en fazla :max öge içermelidir.',
+ ],
+ 'mimes' => ':attribute :values dosya tipinde olmalıdır.',
+ 'min' => [
+ 'numeric' => ':attribute, :min değerinden az olmamalıdır.',
+ 'file' => ':attribute, :min kilobyte boyutundan küçük olmamalıdır.',
+ 'string' => ':attribute, :min karakter boyutundan küçük olmamalıdır.',
+ 'array' => ':attribute, en az :min öge içermelidir.',
+ ],
+ 'no_double_extension' => ':attribute sadece tek bir dosya tipinde olmalıdır.',
+ 'not_in' => 'Seçili :attribute geçerli değildir.',
+ 'numeric' => ':attribute rakam olmalıdır.',
+ 'regex' => ':attribute formatı geçerli değildir.',
+ 'required' => 'The :attribute field is required. :attribute alanı gereklidir.',
+ 'required_if' => ':other alanı :value değerinde ise :attribute alanı gereklidir.',
+ 'required_with' => 'Eğer :values değeri geçerli ise :attribute alanı gereklidir.',
+ 'required_with_all' => 'Eğer :values değeri geçerli ise :attribute alanı gereklidir. ',
+ 'required_without' => 'Eğer :values değeri geçerli değil ise :attribute alanı gereklidir.',
+ 'required_without_all' => 'Eğer :values değerlerinden hiçbiri geçerli değil ise :attribute alanı gereklidir.',
+ 'same' => ':attribute ve :other eşleşmelidir.',
+ 'size' => [
+ 'numeric' => ':attribute, :size boyutunda olmalıdır.',
+ 'file' => ':attribute, :size kilobyte boyutunda olmalıdır.',
+ 'string' => ':attribute, :size karakter uzunluğunda olmalıdır.',
+ 'array' => ':attribute, :size sayıda öge içermelidir.',
+ ],
+ 'string' => ':attribute string olmalıdır.',
+ 'timezone' => ':attribute geçerli bir alan olmalıdır.',
+ 'unique' => ':attribute daha önce alınmış.',
+ 'url' => ':attribute formatı geçerli değil.',
+ 'uploaded' => 'Dosya yüklemesi başarısız oldu. Server bu boyutta dosyaları kabul etmiyor olabilir.',
+
+ // Custom validation lines
+ 'custom' => [
+ 'password-confirm' => [
+ 'required_with' => 'Parola onayı gereklidir.',
+ ],
+ ],
+
+ // Custom validation attributes
+ 'attributes' => [],
+];
@extend .code-base;
display: inline;
padding: 1px 3px;
- white-space:pre;
+ white-space:pre-wrap;
line-height: 1.2em;
margin-bottom: 1.2em;
}
margin-right: $-xs;
pointer-events: none;
}
-
.list-sort {
display: inline-grid;
margin-left: $-s;
- grid-template-columns: 120px 40px;
+ grid-template-columns: minmax(120px, max-content) 40px;
+ font-size: 0.9rem;
border: 2px solid #DDD;
border-radius: 4px;
}
<div class="form-group">
<label for="username">{{ trans('auth.username') }}</label>
- @include('form.text', ['name' => 'username', 'tabindex' => 1])
+ @include('form.text', ['name' => 'username', 'autofocus' => true])
</div>
@if(session('request-email', false) === true)
<div class="form-group">
<label for="email">{{ trans('auth.email') }}</label>
- @include('form.text', ['name' => 'email', 'tabindex' => 1])
+ @include('form.text', ['name' => 'email'])
<span class="text-neg">
{{ trans('auth.ldap_email_hint') }}
</span>
<div class="form-group">
<label for="password">{{ trans('auth.password') }}</label>
- @include('form.password', ['name' => 'password', 'tabindex' => 1])
+ @include('form.password', ['name' => 'password'])
</div>
\ No newline at end of file
<div class="form-group">
<label for="email">{{ trans('auth.email') }}</label>
- @include('form.text', ['name' => 'email', 'tabindex' => 1])
+ @include('form.text', ['name' => 'email', 'autofocus' => true])
</div>
<div class="form-group">
<label for="password">{{ trans('auth.password') }}</label>
- @include('form.password', ['name' => 'password', 'tabindex' => 1])
+ @include('form.password', ['name' => 'password'])
<span class="block small mt-s">
<a href="{{ url('/password/email') }}">{{ trans('auth.forgot_password') }}</a>
</span>
'name' => 'remember',
'checked' => false,
'value' => 'on',
- 'tabindex' => 1,
'label' => trans('auth.remember_me'),
])
</div>
<div class="text-right">
- <button class="button" tabindex="1">{{ Str::title(trans('auth.log_in')) }}</button>
+ <button class="button">{{ Str::title(trans('auth.log_in')) }}</button>
</div>
</div>
<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)
<h1 class="list-heading">{{ trans('entities.books') }}</h1>
<div class="text-m-right my-m">
- @include('partials.sort', ['options' => $sortOptions, 'order' => $order, 'sort' => $sort, 'type' => 'books'])
+ @include('partials.sort', ['options' => [
+ 'name' => trans('common.sort_name'),
+ 'created_at' => trans('common.sort_created_at'),
+ 'updated_at' => trans('common.sort_updated_at'),
+ ], 'order' => $order, 'sort' => $sort, 'type' => 'books'])
</div>
</div>
<button type="button" dropdown-toggle aria-haspopup="true" aria-expanded="false" class="text-button" title="{{ trans('common.delete') }}">@icon('delete')</button>
<ul class="dropdown-menu" role="menu">
<li class="px-m text-small text-muted pb-s">{{trans('entities.comment_delete_confirm')}}</li>
- <li><a action="delete" href="#" class="text-button text-neg" >@icon('delete'){{ trans('common.delete') }}</a></li>
+ <li><button action="delete" type="button" class="text-button text-neg" >@icon('delete'){{ trans('common.delete') }}</button></li>
</ul>
</div>
@endif
$value
$checked
$label
-$tabindex
--}}
<label custom-checkbox class="toggle-switch @if($errors->has($name)) text-neg @endif">
<input type="checkbox" name="{{$name}}" value="{{ $value }}" @if($checked) checked="checked" @endif>
- <span tabindex="{{ $tabindex ?? '0' }}"
- role="checkbox"
+ <span tabindex="0" role="checkbox"
aria-checked="{{ $checked ? 'true' : 'false' }}"
class="custom-checkbox text-primary">@icon('check')</span>
<span class="label">{{$label}}</span>
<label toggle-switch="{{$name}}" custom-checkbox class="toggle-switch">
<input type="hidden" name="{{$name}}" value="{{$value?'true':'false'}}"/>
<input type="checkbox" @if($value) checked="checked" @endif>
- <span tabindex="{{ $tabindex ?? '0' }}"
- role="checkbox"
+ <span tabindex="0" role="checkbox"
aria-checked="{{ $value ? 'true' : 'false' }}"
class="custom-checkbox text-primary">@icon('check')</span>
<span class="label">{{ $label }}</span>
<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>
<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>
<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>
<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 }}
<input type="password" id="{{ $name }}" name="{{ $name }}"
@if($errors->has($name)) class="text-neg" @endif
@if(isset($placeholder)) placeholder="{{$placeholder}}" @endif
- @if(isset($tabindex)) tabindex="{{$tabindex}}" @endif
@if(old($name)) value="{{ old($name)}}" @endif>
@if($errors->has($name))
<div class="text-neg text-small">{{ $errors->first($name) }}</div>
<input type="text" id="{{ $name }}" name="{{ $name }}"
@if($errors->has($name)) class="text-neg" @endif
@if(isset($placeholder)) placeholder="{{$placeholder}}" @endif
- @if(isset($disabled) && $disabled) disabled="disabled" @endif
- @if(isset($tabindex)) tabindex="{{$tabindex}}" @endif
+ @if($autofocus ?? false) autofocus @endif
+ @if($disabled ?? false) disabled="disabled" @endif
@if(isset($model) || old($name)) value="{{ old($name) ? old($name) : $model->$name}}" @endif>
@if($errors->has($name))
<div class="text-neg text-small">{{ $errors->first($name) }}</div>
-@endif
\ No newline at end of file
+@endif
<div class="py-m">
@include('settings.navbar', ['selected' => 'settings'])
</div>
- <div class="text-right mb-l px-m">
- <br>
- BookStack @if(strpos($version, 'v') !== 0) version @endif {{ $version }}
+ <div class="text-right p-m">
+ <a target="_blank" rel="noopener noreferrer" href="https://github.com/BookStackApp/BookStack/releases">
+ BookStack @if(strpos($version, 'v') !== 0) version @endif {{ $version }}
+ </a>
</div>
</div>
<div class="py-m">
@include('settings.navbar', ['selected' => 'maintenance'])
</div>
- <div class="text-right mb-l px-m">
- <br>
+ <div class="text-right p-m">
+ <a target="_blank" rel="noopener noreferrer" href="https://github.com/BookStackApp/BookStack/releases">
BookStack @if(strpos($version, 'v') !== 0) version @endif {{ $version }}
+ </a>
</div>
</div>
<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>
<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>
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');
Route::get('/{slug}/edit', 'BookController@edit');
Route::put('/{slug}', 'BookController@update');
Route::delete('/{id}', 'BookController@destroy');
- Route::get('/{slug}/sort-item', 'BookController@getSortItem');
+ 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');
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');
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Entities\Page;
-use BookStack\Entities\Repos\EntityRepo;
use BookStack\Auth\User;
use BookStack\Entities\Repos\PageRepo;
$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', [
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;
$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);
}
$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);
}
public function test_slug_multi_byte_lower_casing()
{
- $entityRepo = app(EntityRepo::class);
- $book = $entityRepo->createFromInput('book', [
+ $book = $this->newBook([
'name' => 'КНИГА'
]);
public function test_slug_format()
{
- $entityRepo = app(EntityRepo::class);
- $book = $entityRepo->createFromInput('book', [
+ $book = $this->newBook([
'name' => 'PartA / PartB / PartC'
]);
<?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
{
$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]);
+ }
}
<?php namespace Tests;
-
use BookStack\Entities\Repos\PageRepo;
class PageDraftTest extends BrowserKitTest
{
protected $page;
+
+ /**
+ * @var PageRepo
+ */
protected $pageRepo;
public function setUp(): void
public function test_draft_content_shows_if_available()
{
$addedContent = '<p>test message content</p>';
- $this->asAdmin()->visit($this->page->getUrl() . '/edit')
+ $this->asAdmin()->visit($this->page->getUrl('/edit'))
->dontSeeInField('html', $addedContent);
$newContent = $this->page->html . $addedContent;
$this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]);
- $this->asAdmin()->visit($this->page->getUrl() . '/edit')
+ $this->asAdmin()->visit($this->page->getUrl('/edit'))
->seeInField('html', $newContent);
}
public function test_draft_not_visible_by_others()
{
$addedContent = '<p>test message content</p>';
- $this->asAdmin()->visit($this->page->getUrl() . '/edit')
+ $this->asAdmin()->visit($this->page->getUrl('/edit'))
->dontSeeInField('html', $addedContent);
$newContent = $this->page->html . $addedContent;
$newUser = $this->getEditor();
$this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]);
- $this->actingAs($newUser)->visit($this->page->getUrl() . '/edit')
+
+ $this->actingAs($newUser)->visit($this->page->getUrl('/edit'))
->dontSeeInField('html', $newContent);
}
{
$this->asAdmin();
$this->pageRepo->updatePageDraft($this->page, ['html' => 'test content']);
- $this->asAdmin()->visit($this->page->getUrl() . '/edit')
+ $this->asAdmin()->visit($this->page->getUrl('/edit'))
->see('You are currently editing a draft');
}
{
$nonEditedPage = \BookStack\Entities\Page::take(10)->get()->last();
$addedContent = '<p>test message content</p>';
- $this->asAdmin()->visit($this->page->getUrl() . '/edit')
+ $this->asAdmin()->visit($this->page->getUrl('/edit'))
->dontSeeInField('html', $addedContent);
$newContent = $this->page->html . $addedContent;
$this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]);
$this->actingAs($newUser)
- ->visit($this->page->getUrl() . '/edit')
+ ->visit($this->page->getUrl('/edit'))
->see('Admin has started editing this page');
$this->flushSession();
$this->visit($nonEditedPage->getUrl() . '/edit')
$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')
$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);
$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);
$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();
<?php namespace Tests;
-use BookStack\Auth\Role;
use BookStack\Entities\Book;
use BookStack\Entities\Chapter;
use BookStack\Entities\Page;
{
$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);
'entity_selection' => 'book:' . $newBook->id,
'name' => 'My copied test page'
]);
-
$pageCopy = Page::where('name', '=', 'My copied test page')->first();
$movePageResp->assertRedirect($pageCopy->getUrl());
use BookStack\Entities\Book;
use BookStack\Entities\Chapter;
use BookStack\Actions\Tag;
+use BookStack\Entities\Entity;
use BookStack\Entities\Page;
use BookStack\Auth\Permissions\PermissionService;
/**
* Get an instance of a page that has many tags.
* @param \BookStack\Actions\Tag[]|bool $tags
- * @return mixed
+ * @return Entity
*/
- protected function getEntityWithTags($class, $tags = false)
+ protected function getEntityWithTags($class, $tags = false): Entity
{
$entity = $class::first();
// Set restricted permission the page
$page->restricted = true;
$page->save();
- $permissionService->buildJointPermissionsForEntity($page);
+ $page->rebuildPermissions();
$this->asAdmin()->get('/ajax/tags/suggest/names?search=co')->seeJsonEquals(['color', 'country']);
$this->asEditor()->get('/ajax/tags/suggest/names?search=co')->seeJsonEquals([]);
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;
public function test_page_view_restriction()
{
- $page = \BookStack\Entities\Page::first();
+ $page = Page::first();
$pageUrl = $page->getUrl();
$this->actingAs($this->user)
public function test_page_delete_restriction()
{
- $page = \BookStack\Entities\Page::first();
+ $page = Page::first();
$pageUrl = $page->getUrl();
$this->actingAs($this->user)
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')
$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 = [
<?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;
use BookStack\Settings\SettingService;
use BookStack\Uploads\HttpFetcher;
use Illuminate\Support\Env;
+use Mockery;
+use Throwable;
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;
}
/**
* Regenerate the permission for an entity.
* @param Entity $entity
- * @throws \Throwable
+ * @throws Throwable
*/
protected function regenEntityPermissions(Entity $entity)
{
- app(PermissionService::class)->buildJointPermissionsForEntity($entity);
+ $entity->rebuildPermissions();
$entity->load('jointPermissions');
}
/**
* 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, []);
}
/**
* @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);
}
/**
/**
* 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);
*/
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)
protected function runWithEnv(string $name, $value, callable $callback)
{
Env::disablePutenv();
- $originalVal = $_ENV[$name] ?? null;
+ $originalVal = $_SERVER[$name] ?? null;
if (is_null($value)) {
- unset($_ENV[$name]);
unset($_SERVER[$name]);
} else {
- $_ENV[$name] = $value;
$_SERVER[$name] = $value;
}
if (is_null($originalVal)) {
unset($_SERVER[$name]);
- unset($_ENV[$name]);
} else {
$_SERVER[$name] = $originalVal;
- $_ENV[$name] = $originalVal;
}
}
public function test_filesystem_attachments_falls_back_to_storage_type_var()
{
- putenv('STORAGE_TYPE=local_secure');
$this->runWithEnv('STORAGE_TYPE', 'local_secure', function() {
$this->checkEnvConfigResult('STORAGE_ATTACHMENT_TYPE', 's3', 'filesystems.attachments', 's3');
$this->checkEnvConfigResult('STORAGE_ATTACHMENT_TYPE', null, 'filesystems.attachments', 'local_secure');
+++ /dev/null
-<?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]);
- }
-
-}
\ No newline at end of file
{
$admin = $this->getAdmin();
$viewer = $this->getViewer();
- $page = Page::first();
+ $page = Page::first(); /** @var Page $page */
$this->actingAs($admin);
$fileName = 'permission_test.txt';
$page->restricted = true;
$page->permissions()->delete();
$page->save();
- $this->app[PermissionService::class]->buildJointPermissionsForEntity($page);
+ $page->rebuildPermissions();
$page->load('jointPermissions');
$this->actingAs($viewer);
$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' => ''
$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' => ''