use BookStack\Exceptions\NotifyException;
use BookStack\Uploads\AttachmentService;
use DOMDocument;
+use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
* Get all entities in a paginated format
* @param $type
* @param int $count
+ * @param string $sort
+ * @param string $order
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
- public function getAllPaginated($type, $count = 10)
+ public function getAllPaginated($type, int $count = 10, string $sort = 'name', string $order = 'asc')
{
- return $this->entityQuery($type)->orderBy('name', 'asc')->paginate($count);
+ $query = $this->entityQuery($type);
+ $query = $this->addSortToQuery($query, $sort, $order);
+ return $query->paginate($count);
+ }
+
+ 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;
}
/**
}
/**
- * Render the page for viewing, Parsing and performing features such as page transclusion.
+ * Render the page for viewing
* @param Page $page
- * @param bool $ignorePermissions
- * @return mixed|string
+ * @param bool $blankIncludes
+ * @return string
*/
- public function renderPage(Page $page, $ignorePermissions = false)
+ public function renderPage(Page $page, bool $blankIncludes = false) : string
{
$content = $page->html;
+
if (!config('app.allow_content_scripts')) {
$content = $this->escapeScripts($content);
}
- $matches = [];
- preg_match_all("/{{@\s?([0-9].*?)}}/", $content, $matches);
- if (count($matches[0]) === 0) {
- return $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);
continue;
}
- $matchedPage = $this->getById('page', $pageId, false, $ignorePermissions);
+ $matchedPage = $this->getById('page', $pageId);
if ($matchedPage === null) {
- $content = str_replace($matches[0][$index], '', $content);
+ $html = str_replace($matches[0][$index], '', $html);
continue;
}
if (count($splitInclude) === 1) {
- $content = str_replace($matches[0][$index], $matchedPage->html, $content);
+ $html = str_replace($matches[0][$index], $matchedPage->html, $html);
continue;
}
$doc->loadHTML(mb_convert_encoding('<body>'.$matchedPage->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
$matchingElem = $doc->getElementById($splitInclude[1]);
if ($matchingElem === null) {
- $content = str_replace($matches[0][$index], '', $content);
+ $html = str_replace($matches[0][$index], '', $html);
continue;
}
$innerContent = '';
$innerContent .= $doc->saveHTML($childNode);
}
}
- $content = str_replace($matches[0][$index], trim($innerContent), $content);
+ $html = str_replace($matches[0][$index], trim($innerContent), $html);
}
- return $content;
+ return $html;
}
/**
* Escape script tags within HTML content.
* @param string $html
- * @return mixed
+ * @return string
*/
- protected function escapeScripts(string $html)
+ protected function escapeScripts(string $html) : string
{
$scriptSearchRegex = '/<script.*?>.*?<\/script>/ms';
$matches = [];
preg_match_all($scriptSearchRegex, $html, $matches);
- if (count($matches) === 0) {
- return $html;
- }
foreach ($matches[0] as $match) {
$html = str_replace($match, htmlentities($match), $html);
use BookStack\Auth\Access\SocialAuthService;
use BookStack\Auth\User;
use BookStack\Auth\UserRepo;
+ use BookStack\Exceptions\UserUpdateException;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
/**
* UserController constructor.
* @param User $user
- * @param \BookStack\Auth\UserRepo $userRepo
+ * @param UserRepo $userRepo
*/
public function __construct(User $user, UserRepo $userRepo)
{
* Store a newly created user in storage.
* @param Request $request
* @return Response
+ * @throws UserUpdateException
*/
public function store(Request $request)
{
if ($request->filled('roles')) {
$roles = $request->get('roles');
- $user->roles()->sync($roles);
+ $this->userRepo->setUserRoles($user, $roles);
}
- $this->userRepo->downloadGravatarToUserAvatar($user);
+ $this->userRepo->downloadAndAssignUserAvatar($user);
return redirect('/settings/users');
}
/**
* Update the specified user in storage.
* @param Request $request
- * @param int $id
+ * @param int $id
* @return Response
+ * @throws UserUpdateException
*/
public function update(Request $request, $id)
{
'setting' => 'array'
]);
- $user = $this->user->findOrFail($id);
+ $user = $this->userRepo->getById($id);
$user->fill($request->all());
// Role updates
if (userCan('users-manage') && $request->filled('roles')) {
$roles = $request->get('roles');
- $user->roles()->sync($roles);
+ $this->userRepo->setUserRoles($user, $roles);
}
// Password updates
return $this->currentUser->id == $id;
});
- $user = $this->user->findOrFail($id);
+ $user = $this->userRepo->getById($id);
$this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name]));
return view('users/delete', ['user' => $user]);
}
* Remove the specified user from storage.
* @param int $id
* @return Response
+ * @throws \Exception
*/
public function destroy($id)
{
*/
public function switchBookView($id, Request $request)
{
- $this->checkPermissionOr('users-manage', function () use ($id) {
- return $this->currentUser->id == $id;
- });
+ return $this->switchViewType($id, $request, 'books');
+ }
+
+ /**
+ * Update the user's preferred shelf-list display setting.
+ * @param $id
+ * @param Request $request
+ * @return \Illuminate\Http\RedirectResponse
+ */
+ public function switchShelfView($id, Request $request)
+ {
+ return $this->switchViewType($id, $request, 'bookshelves');
+ }
+
+ /**
+ * For a type of list, switch with stored view type for a user.
+ * @param integer $userId
+ * @param Request $request
+ * @param string $listName
+ * @return \Illuminate\Http\RedirectResponse
+ */
+ protected function switchViewType($userId, Request $request, string $listName)
+ {
+ $this->checkPermissionOrCurrentUser('users-manage', $userId);
$viewType = $request->get('view_type');
if (!in_array($viewType, ['grid', 'list'])) {
$viewType = 'list';
}
- $user = $this->user->findOrFail($userId);
- $user = $this->user->findOrFail($id);
- setting()->putUser($user, 'books_view_type', $viewType);
++ $user = $this->userRepo->getById($id);
+ $key = $listName . '_view_type';
+ setting()->putUser($user, $key, $viewType);
- return redirect()->back(302, [], "/settings/users/$id");
+ return redirect()->back(302, [], "/settings/users/$userId");
}
/**
- * Update the user's preferred shelf-list display setting.
+ * Change the stored sort type for the books view.
* @param $id
* @param Request $request
* @return \Illuminate\Http\RedirectResponse
*/
- public function switchShelfView($id, Request $request)
+ public function changeBooksSort($id, Request $request)
{
- $this->checkPermissionOr('users-manage', function () use ($id) {
- return $this->currentUser->id == $id;
- });
+ // TODO - Test this endpoint
+ return $this->changeListSort($id, $request, 'books');
+ }
- $viewType = $request->get('view_type');
- if (!in_array($viewType, ['grid', 'list'])) {
- $viewType = 'list';
+ /**
+ * Changed the stored preference for a list sort order.
+ * @param int $userId
+ * @param Request $request
+ * @param string $listName
+ * @return \Illuminate\Http\RedirectResponse
+ */
+ protected function changeListSort(int $userId, Request $request, string $listName)
+ {
+ $this->checkPermissionOrCurrentUser('users-manage', $userId);
+
+ $sort = $request->get('sort');
+ if (!in_array($sort, ['name', 'created_at', 'updated_at'])) {
+ $sort = 'name';
}
- $user = $this->userRepo->getById($id);
- setting()->putUser($user, 'bookshelves_view_type', $viewType);
+ $order = $request->get('order');
+ if (!in_array($order, ['asc', 'desc'])) {
+ $order = 'asc';
+ }
- return redirect()->back(302, [], "/settings/users/$id");
+ $user = $this->user->findOrFail($userId);
+ $sortKey = $listName . '_sort';
+ $orderKey = $listName . '_sort_order';
+ setting()->putUser($user, $sortKey, $sort);
+ setting()->putUser($user, $orderKey, $order);
+
+ return redirect()->back(302, [], "/settings/users/$userId");
}
+
}
<?php
+ /**
+ * Common elements found throughout many areas of BookStack.
+ */
return [
- /**
- * Buttons
- */
+ // Buttons
'cancel' => 'Cancel',
'confirm' => 'Confirm',
'back' => 'Back',
'select' => 'Select',
'more' => 'More',
- /**
- * Form Labels
- */
+ // Form Labels
'name' => 'Name',
'description' => 'Description',
'role' => 'Role',
'cover_image' => 'Cover image',
'cover_image_description' => 'This image should be approx 440x250px.',
- /**
- * Actions
- */
+ // Actions
'actions' => 'Actions',
'view' => 'View',
'create' => 'Create',
'remove' => 'Remove',
'add' => 'Add',
- /**
- * Sort Options
- */
++ // Sort Options
+ 'sort_name' => 'Name',
+ 'sort_created_at' => 'Created Date',
+ 'sort_updated_at' => 'Updated Date',
+
- /**
- * Misc
- */
+ // Misc
'deleted_user' => 'Deleted User',
'no_activity' => 'No activity to show',
'no_items' => 'No items available',
'list_view' => 'List View',
'default' => 'Default',
- /**
- * Header
- */
+ // Header
'view_profile' => 'View Profile',
'edit_profile' => 'Edit Profile',
- /**
- * Email Content
- */
+ // Email Content
'email_action_help' => 'If you’re having trouble clicking the ":actionText" button, copy and paste the URL below into your web browser:',
'email_rights' => 'All rights reserved',
--];
++];
--- /dev/null
- <a href="{{ baseUrl('/login') }}">@icon('login'){{ trans('auth.log_in') }}</a>
+<header id="header" header-mobile-toggle>
+ <div class="grid break-l mx-l">
+ <div>
+ <a href="{{ baseUrl('/') }}" class="logo">
+ @if(setting('app-logo', '') !== 'none')
+ <img class="logo-image" src="{{ setting('app-logo', '') === '' ? baseUrl('/logo.png') : baseUrl(setting('app-logo', '')) }}" alt="Logo">
+ @endif
+ @if (setting('app-name-header'))
+ <span class="logo-text">{{ setting('app-name') }}</span>
+ @endif
+ </a>
+ <div class="mobile-menu-toggle hide-over-l">@icon('more')</div>
+ </div>
+ <div class="header-search hide-under-l">
+ <form action="{{ baseUrl('/search') }}" method="GET" class="search-box">
+ <button id="header-search-box-button" type="submit">@icon('search') </button>
+ <input id="header-search-box-input" type="text" name="term" tabindex="2" placeholder="{{ trans('common.search') }}" value="{{ isset($searchTerm) ? $searchTerm : '' }}">
+ </form>
+ </div>
+ <div class="text-right">
+ <div class="header-links">
+ <div class="links text-center">
+ <a class="hide-over-l" href="{{ baseUrl('/search') }}">@icon('search'){{ trans('common.search') }}</a>
+ @if(userCan('bookshelf-view-all') || userCan('bookshelf-view-own'))
+ <a href="{{ baseUrl('/shelves') }}">@icon('bookshelf'){{ trans('entities.shelves') }}</a>
+ @endif
+ <a href="{{ baseUrl('/books') }}">@icon('book'){{ trans('entities.books') }}</a>
+ @if(signedInUser() && userCan('settings-manage'))
+ <a href="{{ baseUrl('/settings') }}">@icon('settings'){{ trans('settings.settings') }}</a>
+ @endif
++ @if(signedInUser() && userCan('users-manage') && !userCan('settings-manage'))
++ <a href="{{ baseUrl('/settings/users') }}">@icon('users'){{ trans('settings.users') }}</a>
++ @endif
+ @if(!signedInUser())
++ @if(setting('registration-enabled', false))
++ <a href="{{ baseUrl("/register") }}">@icon('new-user') {{ trans('auth.sign_up') }}</a>
++ @endif
++ <a href="{{ baseUrl('/login') }}">@icon('login') {{ trans('auth.log_in') }}</a>
+ @endif
+ </div>
+ @if(signedInUser())
+ <?php $currentUser = user(); ?>
+ <div class="dropdown-container" dropdown>
+ <span class="user-name hide-under-l" dropdown-toggle>
+ <img class="avatar" src="{{$currentUser->getAvatar(30)}}" alt="{{ $currentUser->name }}">
+ <span class="name">{{ $currentUser->getShortName(9) }}</span> @icon('caret-down')
+ </span>
+ <ul>
+ <li>
+ <a href="{{ baseUrl("/user/{$currentUser->id}") }}" class="text-primary">@icon('user'){{ trans('common.view_profile') }}</a>
+ </li>
+ <li>
+ <a href="{{ baseUrl("/settings/users/{$currentUser->id}") }}" class="text-primary">@icon('edit'){{ trans('common.edit_profile') }}</a>
+ </li>
+ <li>
+ <a href="{{ baseUrl('/logout') }}" class="text-neg">@icon('logout'){{ trans('auth.logout') }}</a>
+ </li>
+ </ul>
+ </div>
+ @endif
+ </div>
+ </div>
+ </div>
+</header>
-@extends('sidebar-layout')
+@extends('tri-layout')
+@section('container-classes', 'mt-xl')
-@section('toolbar')
- <div class="col-sm-8 col-xs-5 faded">
- @include('pages._breadcrumbs', ['page' => $page])
- </div>
- <div class="col-sm-4 col-xs-7 faded">
- <div class="action-buttons">
- <span dropdown class="dropdown-container">
- <div dropdown-toggle class="text-button text-primary">@icon('export'){{ trans('entities.export') }}</div>
- <ul class="wide">
- <li><a href="{{ $page->getUrl('/export/html') }}" target="_blank">{{ trans('entities.export_html') }} <span class="text-muted float right">.html</span></a></li>
- <li><a href="{{ $page->getUrl('/export/pdf') }}" target="_blank">{{ trans('entities.export_pdf') }} <span class="text-muted float right">.pdf</span></a></li>
- <li><a href="{{ $page->getUrl('/export/plaintext') }}" target="_blank">{{ trans('entities.export_text') }} <span class="text-muted float right">.txt</span></a></li>
- </ul>
- </span>
- @if(userCan('page-update', $page))
- <a href="{{ $page->getUrl('/edit') }}" class="text-primary text-button" >@icon('edit'){{ trans('common.edit') }}</a>
- @endif
- @if(userCan('page-update', $page) || userCan('restrictions-manage', $page) || userCan('page-delete', $page))
- <div dropdown class="dropdown-container">
- <a dropdown-toggle class="text-primary text-button">@icon('more') {{ trans('common.more') }}</a>
- <ul>
- @if(userCan('page-update', $page))
- <li><a href="{{ $page->getUrl('/copy') }}" class="text-primary" >@icon('copy'){{ trans('common.copy') }}</a></li>
- @if(userCan('page-delete', $page))
- <li><a href="{{ $page->getUrl('/move') }}" class="text-primary" >@icon('folder'){{ trans('common.move') }}</a></li>
- @endif
- <li><a href="{{ $page->getUrl('/revisions') }}" class="text-primary">@icon('history'){{ trans('entities.revisions') }}</a></li>
- @endif
- @if(userCan('restrictions-manage', $page))
- <li><a href="{{ $page->getUrl('/permissions') }}" class="text-primary">@icon('lock'){{ trans('entities.permissions') }}</a></li>
- @endif
- @if(userCan('page-delete', $page))
- <li><a href="{{ $page->getUrl('/delete') }}" class="text-neg">@icon('delete'){{ trans('common.delete') }}</a></li>
- @endif
- </ul>
- </div>
- @endif
-
- </div>
- </div>
-@stop
-
-@section('sidebar')
+@section('left')
@if($page->tags->count() > 0)
<section>
@endif
@if ($page->attachments->count() > 0)
- <div class="card">
- <h3>@icon('attach') {{ trans('entities.pages_attachments') }}</h3>
+ <div id="page-attachments" class="mb-xl">
+ <h5>{{ trans('entities.pages_attachments') }}</h5>
<div class="body">
@foreach($page->attachments as $attachment)
<div class="attachment">
@endif
@if (isset($pageNav) && count($pageNav))
- <div class="card">
- <h3>@icon('open-book') {{ trans('entities.pages_navigation') }}</h3>
+ <div id="page-navigation" class="mb-xl">
+ <h5>{{ trans('entities.pages_navigation') }}</h5>
<div class="body">
<div class="sidebar-page-nav menu">
@foreach($pageNav as $navItem)
</div>
@endif
- <div class="card entity-details">
- <h3>@icon('info') {{ trans('common.details') }}</h3>
+ <div id="page-details" class="entity-details mb-xl">
+ <h5>{{ trans('common.details') }}</h5>
<div class="body text-muted text-small blended-links">
@include('partials.entity-meta', ['entity' => $page])
</div>
</div>
- @include('partials/book-tree', ['book' => $book, 'sidebarTree' => $sidebarTree])
-
+ @include('partials.book-tree', ['book' => $book, 'sidebarTree' => $sidebarTree])
@stop
-@section('body-wrap-classes', 'flex-fill columns')
-
@section('body')
- <div class="page-content flex" page-display="{{ $page->id }}">
+ <div class="mb-m">
+ @include('pages._breadcrumbs', ['page' => $page])
+ </div>
+
+ <div class="content-wrap card">
+ <div class="page-content flex" page-display="{{ $page->id }}">
- <div class="pointer-container" id="pointer">
- <div class="pointer anim {{ userCan('page-update', $page) ? 'is-page-editable' : ''}}" >
- <span class="icon text-primary">@icon('link') @icon('include', ['style' => 'display:none;'])</span>
- <span class="input-group">
+ <div class="pointer-container" id="pointer">
+ <div class="pointer anim {{ userCan('page-update', $page) ? 'is-page-editable' : ''}}" >
+ <span class="icon text-primary">@icon('link') @icon('include', ['style' => 'display:none;'])</span>
+ <span class="input-group">
<input readonly="readonly" type="text" id="pointer-url" placeholder="url">
<button class="button icon" data-clipboard-target="#pointer-url" type="button" title="{{ trans('entities.pages_copy_link') }}">@icon('copy')</button>
</span>
- @if(userCan('page-update', $page))
- <a href="{{ $page->getUrl('/edit') }}" id="pointer-edit" data-edit-href="{{ $page->getUrl('/edit') }}"
- class="button icon heading-edit-icon" title="{{ trans('entities.pages_edit_content_link')}}">@icon('edit')</a>
- @endif
+ @if(userCan('page-update', $page))
+ <a href="{{ $page->getUrl('/edit') }}" id="pointer-edit" data-edit-href="{{ $page->getUrl('/edit') }}"
+ class="button icon heading-edit-icon" title="{{ trans('entities.pages_edit_content_link')}}">@icon('edit')</a>
+ @endif
+ </div>
</div>
- </div>
- @include('pages/page-display')
+ @include('pages.page-display')
+ </div>
</div>
@if ($commentsEnabled)
- <div class="container small nopad comments-container">
- @include('comments/comments', ['page' => $page])
+ <div class="container small nopad comments-container mb-l">
+ @include('comments.comments', ['page' => $page])
+ <div class="clearfix"></div>
</div>
@endif
@stop
- <a href="{{ $page->getUrl('/move') }}" class="icon-list-item">
- <span class="icon">@icon('folder')</span>
- <span>{{ trans('common.move') }}</span>
- </a>
+
+@section('right')
+ <div class="actions mb-xl">
+ <h5>Actions</h5>
+
+ <div class="icon-list text-primary">
+ {{--Export--}}
+ <div dropdown class="dropdown-container block">
+ <div dropdown-toggle class="icon-list-item">
+ <span class="icon">@icon('export')</span>
+ <span>{{ trans('entities.export') }}</span>
+ </div>
+ <ul class="wide">
+ <li><a href="{{ $page->getUrl('/export/html') }}" target="_blank">{{ trans('entities.export_html') }} <span class="text-muted float right">.html</span></a></li>
+ <li><a href="{{ $page->getUrl('/export/pdf') }}" target="_blank">{{ trans('entities.export_pdf') }} <span class="text-muted float right">.pdf</span></a></li>
+ <li><a href="{{ $page->getUrl('/export/plaintext') }}" target="_blank">{{ trans('entities.export_text') }} <span class="text-muted float right">.txt</span></a></li>
+ </ul>
+ </div>
+
+ {{--User Actions--}}
+ @if(userCan('page-update', $page))
+ <a href="{{ $page->getUrl('/edit') }}" class="icon-list-item">
+ <span class="icon">@icon('edit')</span>
+ <span>{{ trans('common.edit') }}</span>
+ </a>
+ <a href="{{ $page->getUrl('/copy') }}" class="icon-list-item">
+ <span class="icon">@icon('copy')</span>
+ <span>{{ trans('common.copy') }}</span>
+ </a>
++ @if(userCan('page-delete', $page))
++ <a href="{{ $page->getUrl('/move') }}" class="icon-list-item">
++ <span class="icon">@icon('folder')</span>
++ <span>{{ trans('common.move') }}</span>
++ </a>
++ @endif
+ <a href="{{ $page->getUrl('/revisions') }}" class="icon-list-item">
+ <span class="icon">@icon('history')</span>
+ <span>{{ trans('entities.revisions') }}</span>
+ </a>
+ @endif
+ @if(userCan('restrictions-manage', $page))
+ <a href="{{ $page->getUrl('/permissions') }}" class="icon-list-item">
+ <span class="icon">@icon('lock')</span>
+ <span>{{ trans('entities.permissions') }}</span>
+ </a>
+ @endif
+ @if(userCan('page-delete', $page))
+ <a href="{{ $page->getUrl('/delete') }}" class="icon-list-item">
+ <span class="icon">@icon('delete')</span>
+ <span>{{ trans('common.delete') }}</span>
+ </a>
+ @endif
+ </div>
+
+ </div>
+@stop