Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Auth/Protect/Protectors/Password/PasswordProtector.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public function protect()
throw new ForbiddenHttpException();
}

if (request()->isLivePreview()) {
if (request()->isLivePreviewOf($this->data)) {
return;
}

Expand Down
4 changes: 4 additions & 0 deletions src/GraphQL/Queries/EntryQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ public function resolve($root, $args)

$entry = $query->limit(1)->get()->first();

if ($entry && $entry->published() === false && ! request()->isLivePreviewOf($entry)) {
return null;
}

// The `AuthorizeSubResources` middleware will authorize when using `collection` arg,
// but this is still required when the user queries entry using other args.
if ($entry && ! in_array($collection = $entry->collection()->handle(), $this->allowedSubResources())) {
Expand Down
2 changes: 1 addition & 1 deletion src/Http/Controllers/API/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class ApiController extends Controller
*/
protected function abortIfUnpublished($item)
{
if (request()->isLivePreview()) {
if (request()->isLivePreviewOf($item)) {
return;
}

Expand Down
9 changes: 7 additions & 2 deletions src/Http/Responses/DataResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ protected function handleDraft()
return $this;
}

throw_unless($this->request->isLivePreview(), new NotFoundHttpException);
throw_unless($this->isLivePreviewing(), new NotFoundHttpException);

$this->headers['X-Statamic-Draft'] = true;

Expand All @@ -129,13 +129,18 @@ protected function handlePrivateEntries()
return $this;
}

throw_unless($this->request->isLivePreview(), new NotFoundHttpException);
throw_unless($this->isLivePreviewing(), new NotFoundHttpException);

$this->headers['X-Statamic-Private'] = true;

return $this;
}

private function isLivePreviewing()
{
return $this->request->isLivePreviewOf($this->data);
}

protected function view()
{
return app(View::class)
Expand Down
12 changes: 12 additions & 0 deletions src/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,18 @@ public function boot()
return optional($this->statamicToken())->handler() === LivePreview::class;
});

Request::macro('isLivePreviewOf', function ($item) {
$token = $this->statamicToken();

if (! $token || $token->handler() !== LivePreview::class) {
return false;
}

$previewItem = \Facades\Statamic\CP\LivePreview::item($token);

return $item && $previewItem && method_exists($item, 'reference') && $previewItem->reference() === $item->reference();
});

TrimStrings::skipWhen(function (Request $request) {
$route = config('statamic.cp.route');

Expand Down
10 changes: 9 additions & 1 deletion src/Tokens/FileTokenRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,15 @@ public function find(string $token): ?TokenContract
return null;
}

return $this->makeFromPath($path);
$token = $this->makeFromPath($path);

if ($token->hasExpired()) {
$this->delete($token);

return null;
}

return $token;
}

public function save(TokenContract $token): bool
Expand Down
15 changes: 15 additions & 0 deletions tests/API/APITest.php
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,21 @@ public function non_live_preview_tokens_doesnt_bypass_entry_status_check()
]);
}

#[Test]
public function live_preview_token_for_different_entry_doesnt_bypass_status_check()
{
Facades\Config::set('statamic.api.resources.collections', true);
Facades\Collection::make('pages')->save();
tap(Facades\Entry::make()->collection('pages')->id('dance')->published(false)->set('title', 'Dance')->slug('dance'))->save();
$otherEntry = tap(Facades\Entry::make()->collection('pages')->id('sing')->published(true)->set('title', 'Sing')->slug('sing'))->save();

LivePreview::tokenize('test-token', $otherEntry);

$this->get('/api/collections/pages/entries/dance?token=test-token')->assertJson([
'message' => 'Not found.',
]);
}

#[Test]
public function it_replaces_terms_using_live_preview_token()
{
Expand Down
43 changes: 43 additions & 0 deletions tests/Auth/Protect/PasswordProtectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
namespace Tests\Auth\Protect;

use Facades\Statamic\Auth\Protect\Protectors\Password\Token;
use Facades\Statamic\CP\LivePreview;
use Facades\Tests\Factories\EntryFactory;
use Illuminate\Support\Facades\Route;
use PHPUnit\Framework\Attributes\Test;
use Statamic\Facades\Entry;

class PasswordProtectionTest extends PageProtectionTestCase
{
Expand Down Expand Up @@ -124,4 +127,44 @@ public function custom_password_form_url_is_unprotected()
->assertOk()
->assertSee('Password form template');
}

#[Test]
public function live_preview_token_bypasses_password_protection()
{
config(['statamic.protect.schemes.password-scheme' => [
'driver' => 'password',
'allowed' => ['test'],
]]);

$this->createPage('test', ['data' => ['protect' => 'password-scheme']]);

$entry = Entry::find('test');

LivePreview::tokenize('test-token', $entry);

$this
->get('/test?token=test-token')
->assertOk();
}

#[Test]
public function live_preview_token_for_different_entry_doesnt_bypass_password_protection()
{
config(['statamic.protect.schemes.password-scheme' => [
'driver' => 'password',
'allowed' => ['test'],
]]);

$this->createPage('test', ['data' => ['protect' => 'password-scheme']]);

$other = EntryFactory::slug('other')->id('other')->collection('pages')->create();

LivePreview::tokenize('test-token', $other);

Token::shouldReceive('generate')->andReturn('pw-token');

$this
->get('/test?token=test-token')
->assertRedirect();
}
}
37 changes: 37 additions & 0 deletions tests/Feature/GraphQL/EntryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -796,4 +796,41 @@ public function it_only_shows_unpublished_entries_with_token()
'title' => 'That was so rad!',
]]]);
}

#[Test]
public function it_does_not_show_unpublished_entries_with_token_for_different_entry()
{
FilterAuthorizer::shouldReceive('allowedForSubResources')
->andReturn(['published', 'status']);

EntryFactory::collection('blog')
->id('6')
->slug('that-was-so-rad')
->data(['title' => 'That was so rad!'])
->published(false)
->create();

$other = EntryFactory::collection('blog')
->id('7')
->slug('other')
->data(['title' => 'Other'])
->create();

LivePreview::tokenize('test-token', $other);

$query = <<<'GQL'
{
entry(id: "6") {
id
title
}
}
GQL;

$this
->withoutExceptionHandling()
->post('/graphql?token=test-token', ['query' => $query])
->assertGqlOk()
->assertExactJson(['data' => ['entry' => null]]);
}
}
15 changes: 15 additions & 0 deletions tests/FrontendTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,21 @@ public function drafts_are_visible_if_using_live_preview()
$this->assertEquals('Testing 123', $response->content());
}

#[Test]
public function drafts_are_not_visible_if_using_live_preview_token_for_different_entry()
{
$this->withStandardFakeErrorViews();

$page = tap($this->createPage('about')->published(false)->set('content', 'Testing 123'))->save();
$other = $this->createPage('other');

LivePreview::tokenize('test-token', $other);

$this
->get('/about?token=test-token')
->assertStatus(404);
}

#[Test]
public function drafts_dont_get_statically_cached()
{
Expand Down
30 changes: 22 additions & 8 deletions tests/Tokens/TokenRepositoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ public function it_deletes_a_token()
#[Test]
public function it_finds_a_token()
{
Carbon::setTestNow(Carbon::create(2020, 1, 1, 0, 0, 0));

$contents = <<<YAML
handler: 'The\Test\Class'
expires_at: 1577849700
Expand All @@ -109,6 +111,18 @@ public function it_finds_a_token()
$this->assertTrue($token->expiry()->eq(Carbon::create(2020, 1, 1, 3, 35)));
}

#[Test]
public function it_returns_null_and_deletes_expired_token_on_find()
{
Carbon::setTestNow(Carbon::create(2020, 1, 1, 3, 0, 0));

$this->tokens->make('expired-token', 'test')->expireAt(Carbon::now()->subMinute())->save();

$this->assertFileExists(storage_path('statamic/tokens/expired-token.yaml'));
$this->assertNull($this->tokens->find('expired-token'));
$this->assertFileDoesNotExist(storage_path('statamic/tokens/expired-token.yaml'));
}

#[Test]
public function attempting_to_find_a_non_existent_token_returns_null()
{
Expand All @@ -125,16 +139,16 @@ public function it_deletes_expired_tokens()
$this->tokens->make('c', 'test')->expireAt(Carbon::now()->subHour())->save();
$this->tokens->make('d', 'test')->expireAt(Carbon::now()->addMinute())->save();

$this->assertNotNull($this->tokens->find('a'));
$this->assertNotNull($this->tokens->find('b'));
$this->assertNotNull($this->tokens->find('c'));
$this->assertNotNull($this->tokens->find('d'));
$this->assertFileExists(storage_path('statamic/tokens/a.yaml'));
$this->assertFileExists(storage_path('statamic/tokens/b.yaml'));
$this->assertFileExists(storage_path('statamic/tokens/c.yaml'));
$this->assertFileExists(storage_path('statamic/tokens/d.yaml'));

$this->tokens->collectGarbage();

$this->assertNotNull($this->tokens->find('a'));
$this->assertNull($this->tokens->find('b'));
$this->assertNull($this->tokens->find('c'));
$this->assertNotNull($this->tokens->find('d'));
$this->assertFileExists(storage_path('statamic/tokens/a.yaml'));
$this->assertFileDoesNotExist(storage_path('statamic/tokens/b.yaml'));
$this->assertFileDoesNotExist(storage_path('statamic/tokens/c.yaml'));
$this->assertFileExists(storage_path('statamic/tokens/d.yaml'));
}
}
Loading