Skip to content
Open
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
25 changes: 25 additions & 0 deletions src/Data/DataReferenceUpdater.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ public function updateReferences($originalValue, $newValue)
$this->originalValue = $originalValue;
$this->newValue = $newValue;

if (! $this->itemMayContainReferences()) {
return false;
}

$this->recursivelyUpdateFields($this->getTopLevelFields());

if ($this->updated) {
Expand All @@ -78,6 +82,27 @@ public function updateReferences($originalValue, $newValue)
return (bool) $this->updated;
}

protected function itemMayContainReferences()
{
if (! is_string($this->originalValue) || $this->originalValue === '') {
return true;
}

try {
$flags = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;

if (! is_string($data = json_encode($this->item->data()->all(), $flags))) {
return true;
}

$needle = substr(json_encode($this->originalValue, $flags), 1, -1);

return str_contains($data, $needle);
} catch (\Throwable $e) {
return true;
}
}

/**
* Get top level fields off item blueprint.
*
Expand Down
134 changes: 134 additions & 0 deletions tests/Data/DataReferenceUpdaterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<?php

namespace Tests\Data;

use PHPUnit\Framework\Attributes\Test;
use Statamic\Assets\AssetReferenceUpdater;
use Statamic\Facades\Blueprint;
use Tests\TestCase;

class DataReferenceUpdaterTest extends TestCase
{
private function makeItem(array $data)
{
return new class($data)
{
public $blueprintResolved = false;

public function __construct(private $data)
{
$this->data = collect($data);
}

public function data()
{
return $this->data;
}

public function blueprint()
{
$this->blueprintResolved = true;

return Blueprint::makeFromFields([]);
}

public function save()
{
//
}
};
}

#[Test]
public function it_skips_blueprint_traversal_when_data_cannot_contain_the_original_value()
{
$item = $this->makeItem(['hero' => 'unrelated.jpg']);

$updated = AssetReferenceUpdater::item($item)
->filterByContainer('assets')
->updateReferences('img/hoff.jpg', 'img/new-hoff.jpg');

$this->assertFalse($updated);
$this->assertFalse($item->blueprintResolved);
}

#[Test]
public function it_traverses_blueprint_when_data_contains_the_original_value()
{
$item = $this->makeItem(['hero' => 'img/hoff.jpg']);

AssetReferenceUpdater::item($item)
->filterByContainer('assets')
->updateReferences('img/hoff.jpg', 'img/new-hoff.jpg');

$this->assertTrue($item->blueprintResolved);
}

#[Test]
public function it_traverses_blueprint_when_original_value_appears_within_a_larger_string()
{
$item = $this->makeItem(['content' => '[link](statamic://asset::assets::img/hoff.jpg)']);

AssetReferenceUpdater::item($item)
->filterByContainer('assets')
->updateReferences('img/hoff.jpg', 'img/new-hoff.jpg');

$this->assertTrue($item->blueprintResolved);
}

#[Test]
public function it_traverses_blueprint_when_original_value_contains_non_ascii_characters()
{
$item = $this->makeItem(['hero' => 'img/föö-bär.jpg']);

AssetReferenceUpdater::item($item)
->filterByContainer('assets')
->updateReferences('img/föö-bär.jpg', 'img/new.jpg');

$this->assertTrue($item->blueprintResolved);
}

#[Test]
public function it_traverses_blueprint_when_original_value_contains_json_special_characters()
{
$item = $this->makeItem(['hero' => 'img/we"ird\\file.jpg']);

AssetReferenceUpdater::item($item)
->filterByContainer('assets')
->updateReferences('img/we"ird\\file.jpg', 'img/new.jpg');

$this->assertTrue($item->blueprintResolved);
}

#[Test]
public function it_traverses_blueprint_when_json_encoding_data_throws()
{
$throwing = new class implements \JsonSerializable
{
public function jsonSerialize(): mixed
{
throw new \Exception('Cannot be serialized.');
}
};

$item = $this->makeItem(['object' => $throwing, 'hero' => 'unrelated.jpg']);

AssetReferenceUpdater::item($item)
->filterByContainer('assets')
->updateReferences('img/hoff.jpg', 'img/new-hoff.jpg');

$this->assertTrue($item->blueprintResolved);
}

#[Test]
public function it_traverses_blueprint_when_data_cannot_be_json_encoded()
{
$item = $this->makeItem(['broken' => "\xB1\x31", 'hero' => 'unrelated.jpg']);

AssetReferenceUpdater::item($item)
->filterByContainer('assets')
->updateReferences('img/hoff.jpg', 'img/new-hoff.jpg');

$this->assertTrue($item->blueprintResolved);
}
}
Loading