fix: Variablen-Detach meldet verbleibende eingebettete Verweise und lehnt Werte mit Verweis, Markup oder leerem Inhalt ab
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
e951cc1353
commit
62f8465e60
@@ -24,6 +24,8 @@ final class DetachTarget
|
||||
* @param string $nestedIn Where the preset step removes references (messages).
|
||||
* @param list<string> $notes Warnings of every answer.
|
||||
* @param string $doneHint The next step once nothing references the target.
|
||||
* @param bool $complete False when references stay that this call cannot
|
||||
* remove (embedded ones in presets): no done hint.
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $subject,
|
||||
@@ -33,7 +35,8 @@ final class DetachTarget
|
||||
public readonly ?Closure $presets,
|
||||
public readonly string $nestedIn,
|
||||
public readonly array $notes,
|
||||
public readonly string $doneHint
|
||||
public readonly string $doneHint,
|
||||
public readonly bool $complete = true
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -50,6 +53,6 @@ final class DetachTarget
|
||||
*/
|
||||
public function finished(array $skipped, int $hidden): bool
|
||||
{
|
||||
return $this->postIds === null && $skipped === [] && $hidden === 0;
|
||||
return $this->complete && $this->postIds === null && $skipped === [] && $hidden === 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,20 +235,58 @@ final class DiviDesignDetachAbility implements Ability, RequiresDiviFeatures
|
||||
'Verweise auf diese Variable im Divi-Builder lösen.'
|
||||
);
|
||||
}
|
||||
$nested = $postIds === null && PresetBook::fromDivi($this->writer->presets())->referencing($id) !== [];
|
||||
$refusal = $this->unsafeValue($value, $label, $type);
|
||||
if ($refusal !== null) {
|
||||
return $refusal;
|
||||
}
|
||||
$presets = PresetBook::fromDivi($this->writer->presets());
|
||||
$nested = $postIds === null && $presets->referencing($id) !== [];
|
||||
$affixes = $type === 'strings';
|
||||
$notes = [];
|
||||
if ($nested) {
|
||||
$left = $presets->withVariableResolved($id, $value, $affixes)->referencing($id);
|
||||
if ($left !== []) {
|
||||
$notes[] = sprintf(
|
||||
'In %d Presets steht die Variable eingebettet in einem längeren Text (%s); diese Verweise bleiben '
|
||||
. 'unverändert – im Divi-Builder prüfen.',
|
||||
count($left),
|
||||
implode(', ', array_column($left, 'name'))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new DetachTarget(
|
||||
sprintf('Variable „%s“', $label),
|
||||
$postIds,
|
||||
new ReferenceUsageQuery([$id]),
|
||||
static fn (): NodeRewrite => new VariableDetachRewrite($id, $value, $type),
|
||||
fn (): NodeRewrite => new VariableDetachRewrite($id, $value, $type, ($this->unfiltered)()),
|
||||
$nested
|
||||
? static fn (PresetBook $presets): PresetBook => $presets->withVariableResolved($id, $value, $affixes)
|
||||
: null,
|
||||
'Presets',
|
||||
[],
|
||||
self::ARCHIVE_HINT
|
||||
$notes,
|
||||
self::ARCHIVE_HINT,
|
||||
$notes === []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refuses a value that must not be inlined into content (S1, D7): empty (it would blank every reference), a
|
||||
* reference marker, or markup without unfiltered_html. DiviSaver writes without kses, so nothing else checks.
|
||||
*/
|
||||
private function unsafeValue(string $value, string $label, string $type): ?WP_Error
|
||||
{
|
||||
$problem = match (true) {
|
||||
$value === '' => 'ist leer',
|
||||
VariableDetachRewrite::unsafe($value, true) => 'enthält einen Verweis',
|
||||
VariableDetachRewrite::unsafe($value, ($this->unfiltered)()) => 'enthält Markup (< oder >), das dieses '
|
||||
. 'Konto nicht speichern darf (unfiltered_html)',
|
||||
default => null,
|
||||
};
|
||||
|
||||
return $problem === null ? null : Errors::unsupported(
|
||||
sprintf('Der Wert der Variablen „%s“ (Typ %s) %s; nichts wurde geändert.', $label, $type, $problem),
|
||||
'Verweise auf diese Variable im Divi-Builder lösen.'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -328,7 +366,13 @@ final class DiviDesignDetachAbility implements Ability, RequiresDiviFeatures
|
||||
}
|
||||
|
||||
if ($presets === [] && PageRewriteReport::isEmpty($preview)) {
|
||||
$summary = sprintf('Keine Verweise auf %s gefunden – nichts zu tun.', $target->subject);
|
||||
$summary = $preview['skipped'] === [] && $target->complete
|
||||
? sprintf('Keine Verweise auf %s gefunden – nichts zu tun.', $target->subject)
|
||||
: sprintf(
|
||||
'Verweise auf %s lassen sich nicht automatisch lösen und bleiben unverändert (siehe skipped und '
|
||||
. 'warnings) – nichts zu tun.',
|
||||
$target->subject
|
||||
);
|
||||
$done = $target->finished($preview['skipped'], $preview['hidden_count']);
|
||||
|
||||
return PageRewriteReport::nothingToDo(
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Design;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Optional extension of a NodeRewrite: it names the readable blocks that still hold what it removes after rewrite()
|
||||
* (changed or not), so TreeRewrite lists them as skipped and the caller never announces "done" (VariableDetachRewrite).
|
||||
*/
|
||||
interface ReportsLeftovers
|
||||
{
|
||||
/** Why $block stays (partly) as it is, or null when nothing of it is left behind. */
|
||||
public function leftover(Block $block): ?string;
|
||||
}
|
||||
@@ -14,8 +14,9 @@ defined('ABSPATH') || exit;
|
||||
* Applies a NodeRewrite to every block of a NodeIndex, in place (ruling R8): readable blocks are offered to
|
||||
* rewrite(), a changed one gets its new attributes and then its notes; opaque blocks – global instances, blocks with
|
||||
* unreadable attributes – are never offered and never changed, but listed as skipped when the strategy says the
|
||||
* change concerns them (a global module is edited in its library entry). The reason names the library entry's ID
|
||||
* only with $revealLibraryIds (ruling P13: the caller may see library IDs); otherwise it points to the Divi Builder
|
||||
* change concerns them (a global module is edited in its library entry). A ReportsLeftovers strategy also gets its
|
||||
* readable blocks listed as skipped when they keep what it removes. The reason names the library entry's ID only
|
||||
* with $revealLibraryIds (ruling P13: the caller may see library IDs); otherwise it points to the Divi Builder
|
||||
* (fail closed). Pure PHP.
|
||||
*/
|
||||
final class TreeRewrite
|
||||
@@ -45,13 +46,15 @@ final class TreeRewrite
|
||||
}
|
||||
|
||||
$attrs = $this->rewrite->rewrite($block);
|
||||
if ($attrs === null) {
|
||||
continue;
|
||||
if ($attrs !== null) {
|
||||
$block->setAttrs($attrs);
|
||||
$changed[] = ['id' => $id, 'module' => $block->name(), 'label' => NodeSummary::label($block)];
|
||||
array_push($warnings, ...$this->rewrite->notes($block, $id));
|
||||
}
|
||||
$leftover = $this->rewrite instanceof ReportsLeftovers ? $this->rewrite->leftover($block) : null;
|
||||
if ($leftover !== null) {
|
||||
$skipped[] = ['id' => $id, 'reason' => $leftover];
|
||||
}
|
||||
|
||||
$block->setAttrs($attrs);
|
||||
$changed[] = ['id' => $id, 'module' => $block->name(), 'label' => NodeSummary::label($block)];
|
||||
array_push($warnings, ...$this->rewrite->notes($block, $id));
|
||||
}
|
||||
|
||||
return ['changed' => $changed, 'warnings' => $warnings, 'skipped' => $skipped];
|
||||
|
||||
@@ -17,13 +17,31 @@ defined('ABSPATH') || exit;
|
||||
* change the text) and are named in notes(). Other references – colors, other variables, dynamic content – stay
|
||||
* untouched. Pure PHP.
|
||||
*/
|
||||
final class VariableDetachRewrite implements NodeRewrite
|
||||
final class VariableDetachRewrite implements NodeRewrite, ReportsLeftovers
|
||||
{
|
||||
private const EMBEDDED = '%s: Die Variable steht hier eingebettet in einem längeren Text; dieser Verweis bleibt '
|
||||
. 'stehen – im Divi-Builder ersetzen.';
|
||||
private const EMBEDDED = 'Die Variable steht hier eingebettet in einem längeren Text; dieser Verweis bleibt '
|
||||
. 'unverändert – im Divi-Builder prüfen.';
|
||||
|
||||
public function __construct(private string $id, private string $value, private string $variableType)
|
||||
private const UNSAFE = 'Vor- oder Nachtext des Verweises enthält Markup oder einen Verweis; der Verweis bleibt '
|
||||
. 'unverändert – im Divi-Builder prüfen.';
|
||||
|
||||
/** @param bool $markupAllowed Whether "<" and ">" may enter the content (unfiltered_html); markers never may. */
|
||||
public function __construct(
|
||||
private string $id,
|
||||
private string $value,
|
||||
private string $variableType,
|
||||
private bool $markupAllowed = false
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether $text must not be inlined into content: it holds a reference marker (it never went through
|
||||
* VariableRef::canonical()) or – without unfiltered_html – markup (D7, S1).
|
||||
*/
|
||||
public static function unsafe(string $text, bool $markupAllowed): bool
|
||||
{
|
||||
return str_contains($text, VariableRef::MARKER) || str_contains($text, VariableRef::END)
|
||||
|| (! $markupAllowed && (str_contains($text, '<') || str_contains($text, '>')));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,8 +62,8 @@ final class VariableDetachRewrite implements NodeRewrite
|
||||
|
||||
public function rewrite(Block $block): ?array
|
||||
{
|
||||
if (! $block->isDivi()) {
|
||||
return null;
|
||||
if (! $block->isDivi() || $this->hasUnsafeAffix($block->attrs())) {
|
||||
return null; // fail closed: the block stays whole and leftover() reports it
|
||||
}
|
||||
|
||||
$changed = false;
|
||||
@@ -56,7 +74,19 @@ final class VariableDetachRewrite implements NodeRewrite
|
||||
|
||||
public function notes(Block $block, string $nodeId): array
|
||||
{
|
||||
return $this->embeds($block->attrs()) ? [sprintf(self::EMBEDDED, $nodeId)] : [];
|
||||
return []; // leftovers go through leftover(): they also matter for blocks rewrite() did not change
|
||||
}
|
||||
|
||||
public function leftover(Block $block): ?string
|
||||
{
|
||||
if (! $block->isDivi()) {
|
||||
return null;
|
||||
}
|
||||
if ($this->hasUnsafeAffix($block->attrs())) {
|
||||
return self::UNSAFE;
|
||||
}
|
||||
|
||||
return $this->embeds($block->attrs()) ? self::EMBEDDED : null;
|
||||
}
|
||||
|
||||
public function concernsInstance(Block $instance): bool
|
||||
@@ -86,6 +116,36 @@ final class VariableDetachRewrite implements NodeRewrite
|
||||
return $value;
|
||||
}
|
||||
|
||||
/** Whether a whole reference below $value carries before/after text that must not be inlined. */
|
||||
private function hasUnsafeAffix(mixed $value): bool
|
||||
{
|
||||
if (is_string($value)) {
|
||||
if ($this->variableType !== 'strings' || VariableRef::nameOf($value) !== $this->id) {
|
||||
return false;
|
||||
}
|
||||
$settings = VariableRef::settingsOf($value);
|
||||
|
||||
foreach (['before', 'after'] as $key) {
|
||||
$text = is_string($settings[$key] ?? null) ? $settings[$key] : '';
|
||||
if (self::unsafe($text, $this->markupAllowed)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
if (! is_array($value)) {
|
||||
return false;
|
||||
}
|
||||
foreach ($value as $item) {
|
||||
if ($this->hasUnsafeAffix($item)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether a string below $value names the variable inside a longer text. */
|
||||
private function embeds(mixed $value): bool
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user