Skip to content

Commit 8760481

Browse files
committed
Layouts: Added testing for access control, and view impact
Added a new middlware to properly handle per-request in-memory caching within the app.
1 parent e16a4b2 commit 8760481

5 files changed

Lines changed: 97 additions & 2 deletions

File tree

app/App/Providers/ViewTweaksServiceProvider.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use BookStack\Entities\BreadcrumbsViewComposer;
66
use BookStack\Util\DateFormatter;
77
use BookStack\View\ViewBlockManager;
8+
use BookStack\View\ViewBlockPreferences;
89
use Illuminate\Pagination\Paginator;
910
use Illuminate\Support\Facades\Blade;
1011
use Illuminate\Support\Facades\View;
@@ -19,6 +20,10 @@ public function register()
1920
$app['config']->get('app.display_timezone'),
2021
);
2122
});
23+
24+
$this->app->singleton(ViewBlockManager::class, function ($app) {
25+
return new ViewBlockManager(new ViewBlockPreferences());
26+
});
2227
}
2328

2429
/**

app/Http/Kernel.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class Kernel extends HttpKernel
3636
\BookStack\Http\Middleware\CheckEmailConfirmed::class,
3737
\BookStack\Http\Middleware\RunThemeActions::class,
3838
\BookStack\Http\Middleware\Localization::class,
39+
\BookStack\Http\Middleware\ClearPerRequestCaches::class,
3940
],
4041
'api' => [
4142
\BookStack\Http\Middleware\ThrottleApiRequests::class,
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace BookStack\Http\Middleware;
4+
5+
use BookStack\View\ViewBlockManager;
6+
use Closure;
7+
use Illuminate\Http\Request;
8+
9+
/**
10+
* Custom middleware to clear any local caches in the app which are created on a
11+
* per-request basis. While this can be somewhat redundant in the normal PHP request
12+
* lifecycle (since in memory caches are cleared on each request), this can be useful
13+
* in testing and to prepare for future long-serving PHP runtimes.
14+
*/
15+
class ClearPerRequestCaches
16+
{
17+
public function __construct(
18+
protected ViewBlockManager $viewBlockManager,
19+
) {
20+
}
21+
22+
public function handle(Request $request, Closure $next)
23+
{
24+
$response = $next($request);
25+
26+
$this->viewBlockManager->clearLocalCache();
27+
28+
return $response;
29+
}
30+
}

app/View/ViewBlockManager.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public function __construct(
1717
protected array $blocksByLocationAndPosition = [];
1818

1919
/**
20-
* @var array<string, array<string, ViewBlockInterface[]>
20+
* @var array<string, array<string, class-string<ViewBlockInterface>[]>>
2121
*/
2222
protected array $locationBlockCache = [];
2323

@@ -46,7 +46,7 @@ public function register(string $location, string $position, string $blockClass)
4646
*/
4747
public function getInstancesForLocationAndPositionForCurrentUser(string $location, string $position): array
4848
{
49-
$key = user()->id . ':' . $location;
49+
$key = $location;
5050
if (isset($this->locationBlockCache[$key])) {
5151
$blocks = $this->locationBlockCache[$key][$position] ?? [];
5252
return $this->blocksToInstances($blocks);
@@ -165,6 +165,16 @@ public function updatePreferencesFromIdPositionMap(string $location, array $layo
165165
);
166166
}
167167

168+
/**
169+
* Clear the local user-specific cache of blocks.
170+
* The cache only needs to exist for the current request time since its purpose is to
171+
* avoid duplicate loading across views.
172+
*/
173+
public function clearLocalCache(): void
174+
{
175+
$this->locationBlockCache = [];
176+
}
177+
168178
/**
169179
* Convert a blocksByPosition array into a map of block IDs to blocks.
170180
* @param array<string, class-string<ViewBlockInterface>[]> $blocksByPosition

tests/View/LayoutEditTest.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,20 @@
22

33
namespace Tests\View;
44

5+
use BookStack\Activity\Models\Favourite;
56
use Tests\TestCase;
67

78
class LayoutEditTest extends TestCase
89
{
10+
public function test_route_access_limited_to_logged_in_users()
11+
{
12+
$this->setSettings(['app-public' => 'true']);
13+
14+
$this->get('/layouts/books-show')->assertRedirect('/');
15+
$this->put('/layouts/books-show')->assertRedirect('/');
16+
$this->put('/layouts/books-show/reset')->assertRedirect('/');
17+
}
18+
919
public function test_view()
1020
{
1121
$resp = $this->asEditor()->get('/layouts/books-index');
@@ -111,6 +121,45 @@ public function test_update()
111121
], $storedData);
112122
}
113123

124+
public function test_layout_changes_take_effect()
125+
{
126+
$editor = $this->users->editor();
127+
$this->actingAs($editor);
128+
$page = $this->entities->page();
129+
$page->favourites()->save((new Favourite())->forceFill(['user_id' => $editor->id]));
130+
$this->get($page->getUrl());
131+
132+
$html = $this->withHtml($this->get('/'));
133+
134+
$html->assertElementExists('.grid.third > div:nth-child(1) h3:contains("My Recently Viewed")');
135+
$html->assertElementExists('.grid.third > div:nth-child(2) h3:contains("My Most Viewed Favourites")');
136+
$html->assertElementExists('.grid.third > div:nth-child(2) h3:contains("Recently Updated Pages")');
137+
$html->assertElementExists('.grid.third > div:nth-child(3) h3:contains("Recent Activity")');
138+
139+
$layout = [
140+
'left' => [
141+
'builtin_home-top-favourites',
142+
],
143+
'center' => [
144+
'builtin_home-recent-activity',
145+
],
146+
'right' => [
147+
'builtin_home-recently-viewed-or-recent-books',
148+
],
149+
'unused' => [
150+
'builtin_home-recently-updated-pages',
151+
],
152+
];
153+
154+
$this->put('/layouts/home-default', ['layout' => json_encode($layout)])->assertRedirect('/layouts/home-default');
155+
$html = $this->withHtml($this->get('/'));
156+
157+
$html->assertElementExists('.grid.third > div:nth-child(3) h3:contains("My Recently Viewed")');
158+
$html->assertElementExists('.grid.third > div:nth-child(1) h3:contains("My Most Viewed Favourites")');
159+
$html->assertElementNotExists('h3:contains("Recently Updated Pages")');
160+
$html->assertElementExists('.grid.third > div:nth-child(2) h3:contains("Recent Activity")');
161+
}
162+
114163
public function test_reset()
115164
{
116165
$this->asEditor();

0 commit comments

Comments
 (0)