Files
WP-LIT-MCP/src/Integrations/Divi/Pages/DiviDuplicateNodeAbility.php
T
janniclas.loosenandClaude Opus 5.5 b24eb6c3e9 fix(security): no direct HTTP access to plugin PHP files (audit L2, cheap part)
Every file under src/ exits outside WordPress (defined('ABSPATH') ||
exit; after the use block, so a direct request no longer prints a fatal
error with the server path); divi-convert-fixture.php gets the WP_CLI
guard the other integration scripts have. A unit test keeps both true;
PSR1 side-effects warning excluded for the guard.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
2026-09-26 19:32:12 +02:00

84 lines
2.8 KiB
PHP

<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Pages;
use LIT\MCP\Abilities\Annotations;
use LIT\MCP\Integrations\Divi\BlockTree\AttrMap;
use WP_Error;
defined('ABSPATH') || exit;
/**
* lit/divi-duplicate-node: copies a node with everything inside right behind the original
* (spec §5.7). The copy's admin label gets " (Kopie)"; read-only nodes (global modules,
* foreign blocks) are copied unchanged, as a whole, byte-identical reference.
*/
final class DiviDuplicateNodeAbility extends DiviWriteAbility
{
public function name(): string
{
return 'lit/divi-duplicate-node';
}
public function label(): string
{
return 'Divi-Knoten duplizieren';
}
public function description(): string
{
return 'Dupliziert einen Divi-Knoten samt Inhalt direkt hinter das Original; das Admin-Label der Kopie endet '
. 'auf „(Kopie)“. tree_rev aus lit-divi-outline ist Pflicht.';
}
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => array_merge(DiviSchema::write(), [
'id' => ['type' => 'string', 'description' => 'Pfad-ID des zu duplizierenden Knotens.'],
]),
'required' => ['post_id', 'tree_rev', 'id'],
'additionalProperties' => false,
];
}
public function annotations(): array
{
return Annotations::write(false);
}
public function isDirect(): bool
{
return false;
}
protected function change(DiviPage $page, array $input, DiviComposition $composition): DiviChange|WP_Error
{
$id = is_string($input['id'] ?? null) ? $input['id'] : '';
$node = $page->index->find($id);
if ($node === null) {
return DiviErrors::unknownNode($id, $page->post);
}
$copy = $composition->editor()->duplicate($page->index, $node);
if ($copy->isDivi() && ! $copy->isOpaque()) {
// The raw stored desktop value (never NodeSummary::label()'s display-shortened
// text): a long or multi-line label must be copied verbatim, only " (Kopie)" added.
// Setting only the ".desktop.value" leaf keeps every other breakpoint untouched.
// No client input: the stored label plus a constant suffix, so no choke point (S39).
$raw = AttrMap::desktop($node->attrs(), 'module.meta.adminLabel');
$label = is_string($raw) ? $raw : '';
$copy->setAttrs(AttrMap::set(
$copy->attrs(),
'module.meta.adminLabel.desktop.value',
($label === '' ? $node->shortName() : $label) . ' (Kopie)'
));
}
return (new DiviChange(sprintf('Knoten %s dupliziert', $id)))->record($copy, 'duplicated');
}
}