]> BookStack Code Mirror - bookstack/commitdiff
Merge branch 'master' into draw.io
authorDan Brown <redacted>
Sat, 20 Jan 2018 14:01:56 +0000 (14:01 +0000)
committerDan Brown <redacted>
Sat, 20 Jan 2018 14:01:56 +0000 (14:01 +0000)
28 files changed:
app/Console/Commands/DeleteUsers.php [new file with mode: 0644]
app/Exceptions/Handler.php
app/Http/Controllers/BookController.php
app/Http/Controllers/PageController.php
app/Http/Controllers/UserController.php
app/Http/Kernel.php
app/Repos/EntityRepo.php
app/Repos/UserRepo.php
app/Services/SettingService.php
app/User.php
config/view.php
readme.md
resources/assets/sass/_components.scss
resources/lang/en/common.php
resources/lang/en/settings.php
resources/views/books/index.blade.php
resources/views/errors/404.blade.php
resources/views/errors/500.blade.php
resources/views/users/edit.blade.php
routes/web.php
tests/BrowserKitTest.php
tests/Entity/EntityTest.php
tests/Entity/PageContentTest.php
tests/ErrorTest.php [new file with mode: 0644]
tests/Permissions/RestrictionsTest.php
tests/UserProfileTest.php
themes/.gitignore [new file with mode: 0755]
version

diff --git a/app/Console/Commands/DeleteUsers.php b/app/Console/Commands/DeleteUsers.php
new file mode 100644 (file)
index 0000000..8829d39
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+
+namespace BookStack\Console\Commands;
+
+use BookStack\User;
+use BookStack\Repos\UserRepo;
+use Illuminate\Console\Command;
+
+class DeleteUsers extends Command{
+
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'bookstack:delete-users';
+
+    protected $user;
+
+    protected $userRepo;
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Delete users that are not "admin" or system users.';
+
+    public function __construct(User $user, UserRepo $userRepo)
+    {
+        $this->user = $user;
+        $this->userRepo = $userRepo;
+        parent::__construct();
+    }
+
+    public function handle()
+    {
+        $confirm = $this->ask('This will delete all users from the system that are not "admin" or system users. Are you sure you want to continue? (Type "yes" to continue)');
+        $numDeleted = 0;
+        if (strtolower(trim($confirm)) === 'yes')
+        {
+            $totalUsers = $this->user->count();
+            $users = $this->user->where('system_name', '=', null)->with('roles')->get();
+            foreach ($users as $user)
+            {
+                if ($user->hasSystemRole('admin'))
+                {
+                    // don't delete users with "admin" role
+                    continue;
+                }
+                $this->userRepo->destroy($user);
+                ++$numDeleted;
+            }
+            $this->info("Deleted $numDeleted of $totalUsers total users.");
+        }
+        else
+        {
+            $this->info('Exiting...');
+        }
+    }
+
+}
index 12792e15184dfb0cef7bafaa8c28958e0ae5e9f8..a979072e23822f5fcc54b633696ef4115a86c5bf 100644 (file)
@@ -4,11 +4,14 @@ namespace BookStack\Exceptions;
 
 use Exception;
 use Illuminate\Auth\AuthenticationException;
+use Illuminate\Http\Request;
+use Illuminate\Pipeline\Pipeline;
 use Illuminate\Validation\ValidationException;
 use Illuminate\Database\Eloquent\ModelNotFoundException;
 use Symfony\Component\HttpKernel\Exception\HttpException;
 use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
 use Illuminate\Auth\Access\AuthorizationException;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
 class Handler extends ExceptionHandler
 {
@@ -60,9 +63,32 @@ class Handler extends ExceptionHandler
             return response()->view('errors/' . $code, ['message' => $message], $code);
         }
 
+        // Handle 404 errors with a loaded session to enable showing user-specific information
+        if ($this->isExceptionType($e, NotFoundHttpException::class)) {
+            return $this->loadErrorMiddleware($request, function ($request) use ($e) {
+                $message = $e->getMessage() ?: trans('errors.404_page_not_found');
+                return response()->view('errors/404', ['message' => $message], 404);
+            });
+        }
+
         return parent::render($request, $e);
     }
 
+    /**
+     * Load the middleware required to show state/session-enabled error pages.
+     * @param Request $request
+     * @param $callback
+     * @return mixed
+     */
+    protected function loadErrorMiddleware(Request $request, $callback)
+    {
+        $middleware = (\Route::getMiddlewareGroups()['web_errors']);
+        return (new Pipeline($this->container))
+            ->send($request)
+            ->through($middleware)
+            ->then($callback);
+    }
+
     /**
      * Check the exception chain to compare against the original exception type.
      * @param Exception $e
index e181aec8967285956ab5f707dc9a4fd09e326bf7..f1645bb4b1606537b45a58d065787d874fe8ee63 100644 (file)
@@ -46,7 +46,7 @@ class BookController extends Controller
             'books' => $books,
             'recents' => $recents,
             'popular' => $popular,
-            'new' => $new, 
+            'new' => $new,
             'booksViewType' => $booksViewType
         ]);
     }
@@ -155,7 +155,7 @@ class BookController extends Controller
         $book = $this->entityRepo->getBySlug('book', $bookSlug);
         $this->checkOwnablePermission('book-update', $book);
         $bookChildren = $this->entityRepo->getBookChildren($book, true);
-        $books = $this->entityRepo->getAll('book', false);
+        $books = $this->entityRepo->getAll('book', false, 'update');
         $this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
         return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]);
     }
@@ -190,42 +190,56 @@ class BookController extends Controller
         }
 
         // Sort pages and chapters
-        $sortedBooks = [];
-        $updatedModels = collect();
-        $sortMap = json_decode($request->get('sort-tree'));
-        $defaultBookId = $book->id;
+        $sortMap = collect(json_decode($request->get('sort-tree')));
+        $bookIdsInvolved = collect([$book->id]);
 
-        // Loop through contents of provided map and update entities accordingly
-        foreach ($sortMap as $bookChild) {
-            $priority = $bookChild->sort;
-            $id = intval($bookChild->id);
-            $isPage = $bookChild->type == 'page';
-            $bookId = $this->entityRepo->exists('book', $bookChild->book) ? intval($bookChild->book) : $defaultBookId;
-            $chapterId = ($isPage && $bookChild->parentChapter === false) ? 0 : intval($bookChild->parentChapter);
-            $model = $this->entityRepo->getById($isPage?'page':'chapter', $id);
+        // Load models into map
+        $sortMap->each(function($mapItem) use ($bookIdsInvolved) {
+            $mapItem->type = ($mapItem->type === 'page' ? 'page' : 'chapter');
+            $mapItem->model = $this->entityRepo->getById($mapItem->type, $mapItem->id);
+            // Store source and target books
+            $bookIdsInvolved->push(intval($mapItem->model->book_id));
+            $bookIdsInvolved->push(intval($mapItem->book));
+        });
 
-            // Update models only if there's a change in parent chain or ordering.
-            if ($model->priority !== $priority || $model->book_id !== $bookId || ($isPage && $model->chapter_id !== $chapterId)) {
-                $this->entityRepo->changeBook($isPage?'page':'chapter', $bookId, $model);
-                $model->priority = $priority;
-                if ($isPage) $model->chapter_id = $chapterId;
+        // Get the books involved in the sort
+        $bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
+        $booksInvolved = $this->entityRepo->book->newQuery()->whereIn('id', $bookIdsInvolved)->get();
+        // 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->entityRepo->changeBook($mapItem->type, $mapItem->book, $model);
+            }
+            if ($chapterChanged) {
+                $model->chapter_id = intval($mapItem->parentChapter);
                 $model->save();
-                $updatedModels->push($model);
             }
-
-            // Store involved books to be sorted later
-            if (!in_array($bookId, $sortedBooks)) {
-                $sortedBooks[] = $bookId;
+            if ($priorityChanged) {
+                $model->priority = intval($mapItem->sort);
+                $model->save();
             }
-        }
+        });
 
-        // Add activity for books
-        foreach ($sortedBooks as $bookId) {
-            /** @var Book $updatedBook */
-            $updatedBook = $this->entityRepo->getById('book', $bookId);
-            $this->entityRepo->buildJointPermissionsForBook($updatedBook);
-            Activity::add($updatedBook, 'book_sort', $updatedBook->id);
-        }
+        // Rebuild permissions and add activity for involved books.
+        $booksInvolved->each(function(Book $book) {
+            $this->entityRepo->buildJointPermissionsForBook($book);
+            Activity::add($book, 'book_sort', $book->id);
+        });
 
         return redirect($book->getUrl());
     }
index 13e9284650e4f98e725570d43cfe9b4303c2e34b..9dc7d6401f97c07cd8915f802371ebc9bf395021 100644 (file)
@@ -145,6 +145,7 @@ class PageController extends Controller
      * @param string $bookSlug
      * @param string $pageSlug
      * @return Response
+     * @throws NotFoundException
      */
     public function show($bookSlug, $pageSlug)
     {
@@ -152,7 +153,7 @@ class PageController extends Controller
             $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
         } catch (NotFoundException $e) {
             $page = $this->entityRepo->getPageByOldSlug($pageSlug, $bookSlug);
-            if ($page === null) abort(404);
+            if ($page === null) throw $e;
             return redirect($page->getUrl());
         }
 
index fe5c7a243b6c4e7899cf5730da9755225268f371..2fe22f1e107f99e7ced4cbbff5dd344dd24444ba 100644 (file)
@@ -249,4 +249,27 @@ class UserController extends Controller
             'assetCounts' => $assetCounts
         ]);
     }
+
+    /**
+     * Update the user's preferred book-list display setting.
+     * @param $id
+     * @param Request $request
+     * @return \Illuminate\Http\RedirectResponse
+     */
+    public function switchBookView($id, Request $request) {
+        $this->checkPermissionOr('users-manage', function () use ($id) {
+            return $this->currentUser->id == $id;
+        });
+
+        $viewType = $request->get('book_view_type');
+        if (!in_array($viewType, ['grid', 'list'])) {
+            $viewType = 'list';
+        }
+
+        $user = $this->user->findOrFail($id);
+        setting()->putUser($user, 'books_view_type', $viewType);
+
+        return redirect()->back(302, [], "/settings/users/$id");
+    }
+
 }
index cd894de95340471f87e73c876cac8e2a4e49a657..9d2871bbeb828dd4c64ed63f78a35eb63c8806e5 100644 (file)
@@ -33,6 +33,14 @@ class Kernel extends HttpKernel
             \Illuminate\Routing\Middleware\SubstituteBindings::class,
             \BookStack\Http\Middleware\Localization::class
         ],
+        'web_errors' => [
+            \BookStack\Http\Middleware\EncryptCookies::class,
+            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
+            \Illuminate\Session\Middleware\StartSession::class,
+            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
+            \BookStack\Http\Middleware\VerifyCsrfToken::class,
+            \BookStack\Http\Middleware\Localization::class
+        ],
         'api' => [
             'throttle:60,1',
             'bindings',
index c31ddfefe46dbee7d6957cfb230458e622bfa6d9..2c92e1907228548dc2d2f89209e27608fa8fb71f 100644 (file)
@@ -113,9 +113,9 @@ class EntityRepo
      * @param bool $allowDrafts
      * @return \Illuminate\Database\Query\Builder
      */
-    protected function entityQuery($type, $allowDrafts = false)
+    protected function entityQuery($type, $allowDrafts = false, $permission = 'view')
     {
-        $q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type), 'view');
+        $q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type), $permission);
         if (strtolower($type) === 'page' && !$allowDrafts) {
             $q = $q->where('draft', '=', false);
         }
@@ -196,14 +196,15 @@ class EntityRepo
     }
 
     /**
-     * Get all entities of a type limited by count unless count if false.
+     * 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)
+    public function getAll($type, $count = 20, $permission = 'view')
     {
-        $q = $this->entityQuery($type)->orderBy('name', 'asc');
+        $q = $this->entityQuery($type, false, $permission)->orderBy('name', 'asc');
         if ($count !== false) $q = $q->take($count);
         return $q->get();
     }
@@ -690,6 +691,7 @@ class EntityRepo
         preg_match_all("/{{@\s?([0-9].*?)}}/", $content, $matches);
         if (count($matches[0]) === 0) return $content;
 
+        $topLevelTags = ['table', 'ul', 'ol'];
         foreach ($matches[1] as $index => $includeId) {
             $splitInclude = explode('#', $includeId, 2);
             $pageId = intval($splitInclude[0]);
@@ -714,8 +716,13 @@ class EntityRepo
                 continue;
             }
             $innerContent = '';
-            foreach ($matchingElem->childNodes as $childNode) {
-                $innerContent .= $doc->saveHTML($childNode);
+            $isTopLevel = in_array(strtolower($matchingElem->nodeName), $topLevelTags);
+            if ($isTopLevel) {
+                $innerContent .= $doc->saveHTML($matchingElem);
+            } else {
+                foreach ($matchingElem->childNodes as $childNode) {
+                    $innerContent .= $doc->saveHTML($childNode);
+                }
             }
             $content = str_replace($matches[0][$index], trim($innerContent), $content);
         }
index c3546a442cc27dc8fecfc54d04ec66f0b16ceac9..52ad2e47e9a3390dd5ed3665a714fe2d361bcee2 100644 (file)
@@ -115,9 +115,9 @@ class UserRepo
      */
     public function isOnlyAdmin(User $user)
     {
-        if (!$user->roles->pluck('name')->contains('admin')) return false;
+        if (!$user->hasSystemRole('admin')) return false;
 
-        $adminRole = $this->role->getRole('admin');
+        $adminRole = $this->role->getSystemRole('admin');
         if ($adminRole->users->count() > 1) return false;
         return true;
     }
index 18a7c0d1b75846543aee5b535c45cd1ea4134ac7..ce87f5a4b8da2d9962a857a26d9f71427ed19838 100644 (file)
@@ -98,6 +98,9 @@ class SettingService
     {
         $cacheKey = $this->cachePrefix . $key;
         $this->cache->forget($cacheKey);
+        if (isset($this->localCache[$key])) {
+            unset($this->localCache[$key]);
+        }
     }
 
     /**
index 8033557e4cb9a0a048c1d7112c90f84dc4e4bf70..fd6879ba007dc1d551b7385c6a93c043aebc98db 100644 (file)
@@ -81,7 +81,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
      */
     public function hasSystemRole($role)
     {
-        return $this->roles->pluck('system_name')->contains('admin');
+        return $this->roles->pluck('system_name')->contains($role);
     }
 
     /**
index e193ab61d910e0ce50230fc4616c5f9d9c9cdc33..8dc2841e748f035bd4860081ab6dfb760c922e53 100644 (file)
@@ -1,5 +1,10 @@
 <?php
 
+$viewPaths = [realpath(base_path('resources/views'))];
+if ($theme = env('APP_THEME', false)) {
+    array_unshift($viewPaths, base_path('themes/' . $theme));
+}
+
 return [
 
     /*
@@ -13,9 +18,7 @@ return [
     |
     */
 
-    'paths' => [
-        realpath(base_path('resources/views')),
-    ],
+    'paths' => $viewPaths,
 
     /*
     |--------------------------------------------------------------------------
index 1b3db4a5645c383589c9de1c33f6a4570c9fd138..77f1e88054372c96b7b079885ddfd2ef9dcba73f 100644 (file)
--- a/readme.md
+++ b/readme.md
@@ -72,7 +72,13 @@ Some strings have colon-prefixed variables in such as `:userName`. Leave these v
 
 Feel free to create issues to request new features or to report bugs and problems. Just please follow the template given when creating the issue.
 
-Pull requests are very welcome. If the scope of your pull request is very large it may be best to open the pull request early or create an issue for it to discuss how it will fit in to the project and plan out the merge.
+### Pull Request
+
+Pull requests are very welcome. If the scope of your pull request is large it may be best to open the pull request early or create an issue for it to discuss how it will fit in to the project and plan out the merge.
+
+Pull requests should be created from the `master` branch and should be merged back into `master` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases.
+
+If you are looking to alter CSS or JavaScript content please edit the source files found in `resources/assets`. Any CSS or JS files within `public` are built from these source files and therefore should not be edited directly. 
 
 ## Website, Docs & Blog 
 
index 54e109067098bce3608526227ee8918e0348b390..051d1978a18f9f08dc5d7f5bc77d03fda7ee6efa 100644 (file)
@@ -549,7 +549,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   .content {
     padding: $-s;
     font-size: 0.666em;
-    p, ul {
+    p, ul, ol {
       font-size: $fs-m;
       margin: .5em 0;
     }
index 7cdd7c23e527a8f8b4394bf8a906ff49185d9247..26f096327a324b21d2bd9fb3eeddeaf6990cafc2 100644 (file)
@@ -49,6 +49,8 @@ return [
     'toggle_details' => 'Toggle Details',
     'toggle_thumbnails' => 'Toggle Thumbnails',
     'details' => 'Details',
+    'grid_view' => 'Grid View',
+    'list_view' => 'List View',
 
     /**
      * Header
index f35c486ad9f6daa36f90257a3a6be50685fad528..f3e26fb4552d02a9af86b382d5d2159e754ed2ff 100755 (executable)
@@ -96,7 +96,6 @@ return [
     'users_external_auth_id' => 'External Authentication ID',
     'users_password_warning' => 'Only fill the below if you would like to change your password:',
     'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.',
-    'users_books_view_type' => 'Preferred layout for books viewing',
     'users_delete' => 'Delete User',
     'users_delete_named' => 'Delete user :userName',
     'users_delete_warning' => 'This will fully delete this user with the name \':userName\' from the system.',
index d392af045aab36ee3a99e03a21a8a2a9200517d5..2ab819327fe58b1bbfecf5caf93b7f65ed6cb9f7 100644 (file)
@@ -1,8 +1,21 @@
 @extends('sidebar-layout')
 
 @section('toolbar')
-    <div class="col-xs-1"></div>
-    <div class="col-xs-11 faded">
+    <div class="col-xs-6">
+        <div class="action-buttons text-left">
+            <form action="{{ baseUrl("/settings/users/{$currentUser->id}/switch-book-view") }}" method="POST" class="inline">
+                {!! csrf_field() !!}
+                {!! method_field('PATCH') !!}
+                <input type="hidden" value="{{ $booksViewType === 'list'? 'grid' : 'list' }}" name="book_view_type">
+                @if ($booksViewType === 'list')
+                    <button type="submit" class="text-pos text-button"><i class="zmdi zmdi-view-module"></i>{{ trans('common.grid_view') }}</button>
+                @else
+                    <button type="submit" class="text-pos text-button"><i class="zmdi zmdi-view-list"></i>{{ trans('common.list_view') }}</button>
+                @endif
+            </form>
+        </div>
+    </div>
+    <div class="col-xs-6 faded">
         <div class="action-buttons">
             @if($currentUser->can('book-create-all'))
                 <a href="{{ baseUrl("/books/create") }}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>{{ trans('entities.books_create') }}</a>
@@ -52,7 +65,7 @@
                     <hr>
                 @endforeach
                 {!! $books->render() !!}
-            @else 
+            @else
              <div class="row auto-clear">
                     @foreach($books as $key => $book)
                             @include('books/grid-item', ['book' => $book])
index f6ef850afb2c2375c04109f6c6427ddc02b393d4..7cc67a6776bea41bf451bf86902ebee3f220a18d 100644 (file)
@@ -1,8 +1,6 @@
 @extends('simple-layout')
 
 @section('content')
-
-
 <div class="container">
 
     <p>&nbsp;</p>
@@ -16,7 +14,6 @@
     </div>
 
     @if (setting('app-public') || !user()->isDefault())
-
         <div class="row">
             <div class="col-md-4">
                 <div class="card">
index 71fb78a350242909420e9e6abbfbc26912e92044..a01234d811703a4d069a1fd915c23208d2a837b4 100644 (file)
@@ -6,7 +6,7 @@
         <div class="card">
             <h3 class="text-muted">{{ trans('errors.error_occurred') }}</h3>
             <div class="body">
-                <h5>{{ $message }}</h5>
+                <h5>{{ $message or 'An unknown error occurred' }}</h5>
                 <p><a href="{{ baseUrl('/') }}" class="button outline">{{ trans('errors.return_home') }}</a></p>
             </div>
         </div>
index 14ab19e0e9b97a1c651d80c9605243a7b8a8d9ca..fc75593b824b7221e869437727c59768a1d42d46 100644 (file)
                                     @endforeach
                                 </select>
                             </div>
-                            <div class="form-group">
-                                <label for="books-view-type">{{ trans('settings.users_books_view_type') }}</label>
-                                <select name="setting[books_view_type]" id="books-view-type">
-                                    <option @if(setting()->getUser($user, 'books_view_type', 'list') === 'list') selected @endif value="list">List</option>
-                                    <option @if(setting()->getUser($user, 'books_view_type', 'list') === 'grid') selected @endif value="grid">Grid</option>
-                                </select>
-                            </div>
                         </div>
                     </div>
                     <div class="form-group text-right">
index 266e297f38f23f4d5f61407c07c3341566bcbbe5..08c919e26875564ac8d527bea114d37e935eeae7 100644 (file)
@@ -149,6 +149,7 @@ Route::group(['middleware' => 'auth'], function () {
         Route::get('/users', 'UserController@index');
         Route::get('/users/create', 'UserController@create');
         Route::get('/users/{id}/delete', 'UserController@delete');
+        Route::patch('/users/{id}/switch-book-view', 'UserController@switchBookView');
         Route::post('/users/create', 'UserController@store');
         Route::get('/users/{id}', 'UserController@edit');
         Route::put('/users/{id}', 'UserController@update');
index 1eabc74170ab75067e1ff95c57057cc0d9e156d6..d5c9911f8c619c3f41ff41a7d52cb2abc62f6e6c 100644 (file)
@@ -3,7 +3,6 @@
 use BookStack\Entity;
 use BookStack\Role;
 use BookStack\Services\PermissionService;
-use BookStack\User;
 use Illuminate\Contracts\Console\Kernel;
 use Illuminate\Foundation\Testing\DatabaseTransactions;
 use Laravel\BrowserKitTesting\TestCase;
index a43f65b5efd568c58d1ce6a8e9c9343276d1be07..4d4e0e6cd30112721df035f16a3386b577354079 100644 (file)
@@ -82,6 +82,27 @@ class EntityTest extends BrowserKitTest
             ->see($firstChapter->name);
     }
 
+    public function test_toggle_book_view()
+    {
+        $editor = $this->getEditor();
+        setting()->putUser($editor, 'books_view_type', 'grid');
+
+        $this->actingAs($editor)
+            ->visit('/books')
+            ->pageHasElement('.featured-image-container')
+            ->submitForm('List View')
+            // Check redirection.
+            ->seePageIs('/books')
+            ->pageNotHasElement('.featured-image-container');
+
+        $this->actingAs($editor)
+            ->visit('/books')
+            ->submitForm('Grid View')
+            ->seePageIs('/books')
+            ->pageHasElement('.featured-image-container');
+
+    }
+
     public function pageCreation($chapter)
     {
         $page = factory(Page::class)->make([
@@ -155,7 +176,7 @@ class EntityTest extends BrowserKitTest
             ->type($book->name, '#name')
             ->type($book->description, '#description')
             ->press('Save Book');
-        
+
         $expectedPattern = '/\/books\/my-first-book-[0-9a-zA-Z]{3}/';
         $this->assertRegExp($expectedPattern, $this->currentUri, "Did not land on expected page [$expectedPattern].\n");
 
index cd6526aecb1758bb530b69a3b9a6a26d53b522c6..37051478882e94f25c9cb1f6f2c5f334779a043d 100644 (file)
@@ -9,7 +9,7 @@ class PageContentTest extends TestCase
     public function test_page_includes()
     {
         $page = Page::first();
-        $secondPage = Page::all()->get(2);
+        $secondPage = Page::where('id', '!=', $page->id)->first();
 
         $secondPage->html = "<p id='section1'>Hello, This is a test</p><p id='section2'>This is a second block of content</p>";
         $secondPage->save();
@@ -38,7 +38,7 @@ class PageContentTest extends TestCase
     public function test_saving_page_with_includes()
     {
         $page = Page::first();
-        $secondPage = Page::all()->get(2);
+        $secondPage = Page::where('id', '!=', $page->id)->first();
         $this->asEditor();
         $page->html = "<p>{{@$secondPage->id}}</p>";
 
@@ -50,6 +50,23 @@ class PageContentTest extends TestCase
         $this->assertContains("{{@$secondPage->id}}", $page->html);
     }
 
+    public function test_page_includes_do_not_break_tables()
+    {
+        $page = Page::first();
+        $secondPage = Page::where('id', '!=', $page->id)->first();
+
+        $content = '<table id="table"><tbody><tr><td>test</td></tr></tbody></table>';
+        $secondPage->html = $content;
+        $secondPage->save();
+
+        $page->html = "{{@{$secondPage->id}#table}}";
+        $page->save();
+
+        $this->asEditor();
+        $pageResp = $this->get($page->getUrl());
+        $pageResp->assertSee($content);
+    }
+
     public function test_page_revision_views_viewable()
     {
         $this->asEditor();
diff --git a/tests/ErrorTest.php b/tests/ErrorTest.php
new file mode 100644 (file)
index 0000000..c9b5a01
--- /dev/null
@@ -0,0 +1,18 @@
+<?php namespace Tests;
+
+class ErrorTest extends TestCase
+{
+
+    public function test_404_page_does_not_show_login()
+    {
+        // Due to middleware being handled differently this will not fail
+        // if our custom, middleware-loaded handler fails but this is here
+        // as a reminder and as a general check in the event of other issues.
+        $editor = $this->getEditor();
+        $this->actingAs($editor);
+        $notFound = $this->get('/fgfdngldfnotfound');
+        $notFound->assertStatus(404);
+        $notFound->assertDontSeeText('Log in');
+        $notFound->assertSeeText($editor->getShortName(9));
+    }
+}
\ No newline at end of file
index 218b7a0d8175925ecd9d270d175a6604ceb31e90..8f37b2517b9b94e317bd2b3335452db9f47c474f 100644 (file)
@@ -3,6 +3,7 @@
 use BookStack\Book;
 use BookStack\Services\PermissionService;
 use BookStack\User;
+use BookStack\Repos\EntityRepo;
 
 class RestrictionsTest extends BrowserKitTest
 {
@@ -554,4 +555,70 @@ class RestrictionsTest extends BrowserKitTest
         $this->dontSee(substr($bookChapter->name, 0, 15));
     }
 
+    public function test_book_sort_view_permission()
+    {
+        $firstBook = Book::first();
+        $secondBook = Book::find(2);
+        $thirdBook = Book::find(3);
+
+        $this->setEntityRestrictions($firstBook, ['view', 'update']);
+        $this->setEntityRestrictions($secondBook, ['view']);
+        $this->setEntityRestrictions($thirdBook, ['view', 'update']);
+
+        // Test sort page visibility
+        $this->actingAs($this->user)->visit($secondBook->getUrl() . '/sort')
+                ->see('You do not have permission')
+                ->seePageIs('/');
+
+        // Check sort page on first book
+        $this->actingAs($this->user)->visit($firstBook->getUrl() . '/sort')
+                ->see($thirdBook->name)
+                ->dontSee($secondBook->name);
+    }
+
+    public function test_book_sort_permission() {
+        $firstBook = Book::first();
+        $secondBook = Book::find(2);
+
+        $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);
+
+        // Create request data
+        $reqData = [
+            [
+                'id' => $firstBookChapter->id,
+                'sort' => 0,
+                'parentChapter' => false,
+                'type' => 'chapter',
+                'book' => $secondBook->id
+            ]
+        ];
+
+        // Move chapter from first book to a second book
+        $this->actingAs($this->user)->put($firstBook->getUrl() . '/sort', ['sort-tree' => json_encode($reqData)])
+                ->followRedirects()
+                ->see('You do not have permission')
+                ->seePageIs('/');
+
+        $reqData = [
+            [
+                'id' => $secondBookChapter->id,
+                'sort' => 0,
+                'parentChapter' => false,
+                'type' => 'chapter',
+                'book' => $firstBook->id
+            ]
+        ];
+
+        // Move chapter from second book to first book
+        $this->actingAs($this->user)->put($firstBook->getUrl() . '/sort', ['sort-tree' => json_encode($reqData)])
+                ->followRedirects()
+                ->see('You do not have permission')
+                ->seePageIs('/');
+    }
 }
index 0c66363f0b34a0e8c241108d8675b7a29ca67105..3262117d5009e0bfd5bb8f9aa2b71886fbf4bf6e 100644 (file)
@@ -103,7 +103,7 @@ class UserProfileTest extends BrowserKitTest
         $this->actingAs($editor)
             ->visit('/books')
             ->pageNotHasElement('.featured-image-container')
-            ->pageHasElement('.entity-list-item');
+            ->pageHasElement('.content .entity-list-item');
     }
 
     public function test_books_view_is_grid()
diff --git a/themes/.gitignore b/themes/.gitignore
new file mode 100755 (executable)
index 0000000..d6b7ef3
--- /dev/null
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/version b/version
index de0ad77915bab6a1a85aa60ba37a325637f2ada4..0507cd08e3bcd70182796a18b0e80854f9a6c35f 100644 (file)
--- a/version
+++ b/version
@@ -1 +1 @@
-v0.18-dev
+v0.20-dev