fix: layout-edit dry runs name only editable live templates (ruling Q22)
ThemeBuilderEdits::showing() splits the live templates that show a layout into editable ones (named under templates) and a hidden_templates count; confirm is still decided on all of them, so a layout shown only by uneditable templates still needs confirm. The live split is read once per write for the confirm check and the field warnings, and field warnings now also survive a non-confirm refusal such as the P9 backup refusal (M-12 a). replace-text passes hidden_templates on; the write schema lists it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -13,7 +13,8 @@ Kopf- und Fußzeile heißen „global“, weil andere Vorlagen sie mitbenutzen.
|
|||||||
Nutzer fragen → dieselbe Anfrage mit `confirm: true`.
|
Nutzer fragen → dieselbe Anfrage mit `confirm: true`.
|
||||||
- Auch alles andere, was Besucher sofort sähen, ist zuerst ein Probelauf (`confirm_required`) und braucht
|
- Auch alles andere, was Besucher sofort sähen, ist zuerst ein Probelauf (`confirm_required`) und braucht
|
||||||
`confirm: true`: Bedingungen einer aktiven Vorlage ändern (`lit/tb-assign`) und ein Layout ändern, das eine
|
`confirm: true`: Bedingungen einer aktiven Vorlage ändern (`lit/tb-assign`) und ein Layout ändern, das eine
|
||||||
aktive Vorlage zeigt (die `lit-divi-…`-Werkzeuge; die Antwort nennt die Vorlagen unter `templates`). Vor dem
|
aktive Vorlage zeigt (die `lit-divi-…`-Werkzeuge; die Antwort nennt die Vorlagen unter `templates`, solche
|
||||||
|
ohne Berechtigung nur als Anzahl `hidden_templates`). Vor dem
|
||||||
Speichern eines sichtbaren Layouts entsteht eine Revision; sind Revisionen abgeschaltet, eine
|
Speichern eines sichtbaren Layouts entsteht eine Revision; sind Revisionen abgeschaltet, eine
|
||||||
Sicherungskopie im Papierkorb (`backup_id`).
|
Sicherungskopie im Papierkorb (`backup_id`).
|
||||||
- `lit/divi-replace-text` ändert sichtbare Layouts nicht; solche Änderungen mit `lit-divi-update` und
|
- `lit/divi-replace-text` ändert sichtbare Layouts nicht; solche Änderungen mit `lit-divi-update` und
|
||||||
@@ -85,7 +86,8 @@ bleiben die Felder ungeprüft (die Antwort sagt das). Kopf- und Fußzeile werden
|
|||||||
- Divi → Rollen-Editor: „Theme Builder“ – standardmäßig nur für Administratoren.
|
- Divi → Rollen-Editor: „Theme Builder“ – standardmäßig nur für Administratoren.
|
||||||
- Fehlt eines davon, gibt es für dieses Konto keine Vorlagen und Layouts (`not_found` wie bei einer unbekannten
|
- Fehlt eines davon, gibt es für dieses Konto keine Vorlagen und Layouts (`not_found` wie bei einer unbekannten
|
||||||
ID bzw. `forbidden`). Vorlagen, die das Konto nicht bearbeiten darf, erscheinen nirgends; aktive davon nur als
|
ID bzw. `forbidden`). Vorlagen, die das Konto nicht bearbeiten darf, erscheinen nirgends; aktive davon nur als
|
||||||
Anzahl (`hidden_live` in `lit/tb-list`, `hidden_overlaps` im Probelauf).
|
Anzahl (`hidden_live` in `lit/tb-list`, `hidden_overlaps` im Probelauf, `hidden_templates` beim Ändern eines
|
||||||
|
Layouts).
|
||||||
- Einstellungen → LIT MCP → Tools: Gruppe „Divi Theme Builder“ auf „nur lesen“ sperrt jedes Schreiben, auch über
|
- Einstellungen → LIT MCP → Tools: Gruppe „Divi Theme Builder“ auf „nur lesen“ sperrt jedes Schreiben, auch über
|
||||||
die `lit-divi-…`-Werkzeuge; deaktiviert gibt es keine Vorlagen und Layouts.
|
die `lit-divi-…`-Werkzeuge; deaktiviert gibt es keine Vorlagen und Layouts.
|
||||||
|
|
||||||
|
|||||||
@@ -242,8 +242,8 @@ Rulings Q1–Q25: `.superpowers/sdd/2026-09-27-etappe-3b-theme-builder/progress.
|
|||||||
- E3 für den Theme Builder: `edit_others_posts`, `edit_theme_options` (Q15) und Divis Rollen-Recht
|
- E3 für den Theme Builder: `edit_others_posts`, `edit_theme_options` (Q15) und Divis Rollen-Recht
|
||||||
„Theme Builder“, je Vorlage/Layout `edit_post`, zum Anlegen die Veröffentlichen-Rechte. Nicht bearbeitbare
|
„Theme Builder“, je Vorlage/Layout `edit_post`, zum Anlegen die Veröffentlichen-Rechte. Nicht bearbeitbare
|
||||||
Vorlagen antworten wie fehlende (Q18); aktive davon nur als Anzahl in Liste und Probeläufen
|
Vorlagen antworten wie fehlende (Q18); aktive davon nur als Anzahl in Liste und Probeläufen
|
||||||
von `lit/tb-assign`/`lit/tb-activate` (Q21; für die Probeläufe der Layout-Änderungen folgt Q22 in der
|
von `lit/tb-assign`/`lit/tb-activate` (Q21) und der Layout-Änderungen (`hidden_templates`; die Rückfrage
|
||||||
Schlussrunde).
|
gilt trotzdem für alle aktiven Vorlagen, Q22).
|
||||||
- Bedingungen nur aus Divis Katalog; Objekte lesbar (Begriffe: `assign_term`); strenger als Divi:
|
- Bedingungen nur aus Divis Katalog; Objekte lesbar (Begriffe: `assign_term`); strenger als Divi:
|
||||||
Autoren-Bedingungen (`archive:user:id:`) nur mit `list_users` (Q10).
|
Autoren-Bedingungen (`archive:user:id:`) nur mit `list_users` (Q10).
|
||||||
- E9: eigene Gruppe „Divi Theme Builder“ (`divi-theme-builder`); alle neuen Tools indirekt, 22 direkte Tools.
|
- E9: eigene Gruppe „Divi Theme Builder“ (`divi-theme-builder`); alle neuen Tools indirekt, 22 direkte Tools.
|
||||||
|
|||||||
@@ -406,7 +406,7 @@ final class DiviReplaceTextAbility implements Ability
|
|||||||
'error' => Errors::clientMessage($error) . ' Sichtbare Theme-Builder-Layouts nicht mit '
|
'error' => Errors::clientMessage($error) . ' Sichtbare Theme-Builder-Layouts nicht mit '
|
||||||
. 'lit/divi-replace-text, sondern nach Rückfrage mit lit-divi-update (confirm: true) ändern.',
|
. 'lit/divi-replace-text, sondern nach Rückfrage mit lit-divi-update (confirm: true) ändern.',
|
||||||
'templates' => $data['templates'],
|
'templates' => $data['templates'],
|
||||||
];
|
] + (isset($data['hidden_templates']) ? ['hidden_templates' => $data['hidden_templates']] : []);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -41,8 +41,10 @@ abstract class DiviWriteAbility implements Ability
|
|||||||
'affected_count' => ['type' => 'integer'],
|
'affected_count' => ['type' => 'integer'],
|
||||||
'hidden_count' => ['type' => 'integer'],
|
'hidden_count' => ['type' => 'integer'],
|
||||||
'backup_id' => ['type' => 'integer'],
|
'backup_id' => ['type' => 'integer'],
|
||||||
// Theme Builder layouts: the enabled templates that show the layout.
|
// Theme Builder layouts: the enabled templates that show the layout – those the user may edit; the
|
||||||
|
// others only as a count (ruling Q22).
|
||||||
'templates' => ['type' => 'array', 'items' => ['type' => 'object']],
|
'templates' => ['type' => 'array', 'items' => ['type' => 'object']],
|
||||||
|
'hidden_templates' => ['type' => 'integer'],
|
||||||
], ['tree_rev']);
|
], ['tree_rev']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -218,22 +218,45 @@ final class DiviWriter
|
|||||||
return $this->save($ability, $page, $change, $input, SaveScope::ThemeBuilderLayout);
|
return $this->save($ability, $page, $change, $input, SaveScope::ThemeBuilderLayout);
|
||||||
}
|
}
|
||||||
|
|
||||||
// E12: fields in a body checked against the templates' post types – warnings only, also in the dry run.
|
// E12: fields in a body checked against the templates' post types – warnings only, also in the dry run
|
||||||
$fieldWarnings = $this->themeBuilder->fieldWarnings($page, $change);
|
// and in any refusal (review M-12 a). The live templates are read once for both checks.
|
||||||
|
$live = $this->themeBuilder->live();
|
||||||
|
$fieldWarnings = $this->themeBuilder->fieldWarnings($page, $change, $live);
|
||||||
foreach ($fieldWarnings as $warning) {
|
foreach ($fieldWarnings as $warning) {
|
||||||
$change->warning($warning);
|
$change->warning($warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
$prepared = $this->themeBuilder->prepare($page, $input);
|
$prepared = $this->themeBuilder->prepare($page, $input, $live);
|
||||||
if ($prepared instanceof WP_Error) {
|
if ($prepared instanceof WP_Error) {
|
||||||
return $fieldWarnings !== [] && Errors::codeOf($prepared) === 'confirm_required'
|
return $fieldWarnings === [] ? $prepared : self::withWarnings($prepared, $fieldWarnings);
|
||||||
? VisibleEdits::merged($prepared, 'Hinweise zu Feldern siehe warnings.', ['warnings' => $fieldWarnings])
|
|
||||||
: $prepared;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->savePrepared($ability, $page, $change, $input, $prepared, 'des Theme-Builder-Layouts');
|
return $this->savePrepared($ability, $page, $change, $input, $prepared, 'des Theme-Builder-Layouts');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* $error with the field warnings of the change (key warnings): a confirm_required also says so in its message.
|
||||||
|
*
|
||||||
|
* @param list<string> $warnings
|
||||||
|
*/
|
||||||
|
private static function withWarnings(WP_Error $error, array $warnings): WP_Error
|
||||||
|
{
|
||||||
|
if (Errors::codeOf($error) === 'confirm_required') {
|
||||||
|
return VisibleEdits::merged($error, 'Hinweise zu Feldern siehe warnings.', ['warnings' => $warnings]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $error->get_error_data();
|
||||||
|
$data = is_array($data) ? $data : [];
|
||||||
|
|
||||||
|
return Errors::make(
|
||||||
|
Errors::codeOf($error),
|
||||||
|
Errors::clientMessage($error),
|
||||||
|
(string) ($data['hint'] ?? ''),
|
||||||
|
(int) ($data['status'] ?? 400),
|
||||||
|
array_merge(array_diff_key($data, ['status' => true, 'hint' => true]), ['warnings' => $warnings])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* save() with the scope GlobalEdits/ThemeBuilderEdits prepared; their details join the result, and a
|
* save() with the scope GlobalEdits/ThemeBuilderEdits prepared; their details join the result, and a
|
||||||
* failure after their backup names it (ruling P14, VisibleEdits). $entry names the entry in that error.
|
* failure after their backup names it (ruling P14, VisibleEdits). $entry names the entry in that error.
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ defined('ABSPATH') || exit;
|
|||||||
* ausdrücklicher Freigabe"): Divi reads TB layouts live (F22), so a layout that a live template (attached to the
|
* ausdrücklicher Freigabe"): Divi reads TB layouts live (F22), so a layout that a live template (attached to the
|
||||||
* live Theme Builder and enabled, whatever its post status – ruling Q8) shows changes the website at once –
|
* live Theme Builder and enabled, whatever its post status – ruling Q8) shows changes the website at once –
|
||||||
* without confirm: true nothing is saved and the answer lists those templates (dry run of the fully validated
|
* without confirm: true nothing is saved and the answer lists those templates (dry run of the fully validated
|
||||||
* change, like global modules, answer F2; ruling Q9). A confirmed edit gets the restore point of every visible
|
* change, like global modules, answer F2; ruling Q9) – by name only those the user may edit, the others as the
|
||||||
* change (VisibleEdits, ruling Q2): a backup when revisions are off, refused when it could not be faithful (P9).
|
* count hidden_templates; confirm is decided on all of them (ruling Q22). A confirmed edit gets the restore point
|
||||||
|
* of every visible change (VisibleEdits, ruling Q2): a backup when revisions are off, refused when it could not be
|
||||||
|
* faithful (P9).
|
||||||
* Layouts of disabled or detached templates save at once. The save scope sets the Divi flags and clears every
|
* Layouts of disabled or detached templates save at once. The save scope sets the Divi flags and clears every
|
||||||
* page's CSS and the Theme Builder cache.
|
* page's CSS and the Theme Builder cache.
|
||||||
*
|
*
|
||||||
@@ -39,28 +41,49 @@ final class ThemeBuilderEdits
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return list<Template> Live templates that show layout $page in its area. */
|
/**
|
||||||
public function showing(DiviPage $page): array
|
* The live templates split by canEditTemplate() (ThemeBuilderAccess::editableLiveTemplates()) – computed once
|
||||||
|
* per write and passed to prepare()/notice()/fieldWarnings().
|
||||||
|
*
|
||||||
|
* @return array{templates: list<Template>, hidden: list<Template>}
|
||||||
|
*/
|
||||||
|
public function live(): array
|
||||||
|
{
|
||||||
|
return $this->access->editableLiveTemplates();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Live templates that show layout $page in its area (ruling Q22, Q21 pattern): "templates" the user may edit
|
||||||
|
* (named in answers), "hidden" the number of the others (never named). Both together decide whether a write
|
||||||
|
* needs confirm – a layout shown only by hidden templates is still visible.
|
||||||
|
*
|
||||||
|
* @param array{templates: list<Template>, hidden: list<Template>}|null $live live(); null = read now
|
||||||
|
* @return array{templates: list<Template>, hidden: int}
|
||||||
|
*/
|
||||||
|
public function showing(DiviPage $page, ?array $live = null): array
|
||||||
{
|
{
|
||||||
$area = (string) $page->themeBuilderArea();
|
$area = (string) $page->themeBuilderArea();
|
||||||
$id = (int) $page->post->ID;
|
$id = (int) $page->post->ID;
|
||||||
|
$shows = static fn (Template $template): bool => $template->showsLayout($area, $id);
|
||||||
|
$live ??= $this->live();
|
||||||
|
|
||||||
return array_values(array_filter(
|
return [
|
||||||
$this->access->store()->liveTemplates(),
|
'templates' => array_values(array_filter($live['templates'], $shows)),
|
||||||
static fn (Template $template): bool => $template->showsLayout($area, $id)
|
'hidden' => count(array_filter($live['hidden'], $shows)),
|
||||||
));
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $input
|
* @param array<string, mixed> $input
|
||||||
|
* @param array{templates: list<Template>, hidden: list<Template>}|null $live live(); null = read now
|
||||||
* @return array{scope: SaveScope, details: array<string, mixed>}|WP_Error
|
* @return array{scope: SaveScope, details: array<string, mixed>}|WP_Error
|
||||||
* @throws WriteFailed When the backup could not be made (nothing was saved).
|
* @throws WriteFailed When the backup could not be made (nothing was saved).
|
||||||
*/
|
*/
|
||||||
public function prepare(DiviPage $page, array $input): array|WP_Error
|
public function prepare(DiviPage $page, array $input, ?array $live = null): array|WP_Error
|
||||||
{
|
{
|
||||||
$templates = $this->showing($page);
|
$showing = $this->showing($page, $live);
|
||||||
$details = ['templates' => self::listing($templates)];
|
$details = self::details($showing);
|
||||||
if ($templates === []) {
|
if (! self::isShown($showing)) {
|
||||||
return ['scope' => SaveScope::ThemeBuilderLayout, 'details' => $details];
|
return ['scope' => SaveScope::ThemeBuilderLayout, 'details' => $details];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +94,7 @@ final class ThemeBuilderEdits
|
|||||||
'Theme-Builder-Layout %d ist auf der Website sichtbar (%s, siehe templates); die Änderung wirkt '
|
'Theme-Builder-Layout %d ist auf der Website sichtbar (%s, siehe templates); die Änderung wirkt '
|
||||||
. 'sofort. Nichts wurde gespeichert (Probelauf).',
|
. 'sofort. Nichts wurde gespeichert (Probelauf).',
|
||||||
$id,
|
$id,
|
||||||
self::count($templates)
|
self::count($showing)
|
||||||
),
|
),
|
||||||
$details
|
$details
|
||||||
);
|
);
|
||||||
@@ -95,15 +118,19 @@ final class ThemeBuilderEdits
|
|||||||
/** A tool's own confirm_required (lit-divi-delete) on a visible layout, extended by the templates. */
|
/** A tool's own confirm_required (lit-divi-delete) on a visible layout, extended by the templates. */
|
||||||
public function notice(DiviPage $page, WP_Error $error): WP_Error
|
public function notice(DiviPage $page, WP_Error $error): WP_Error
|
||||||
{
|
{
|
||||||
$templates = Errors::codeOf($error) === 'confirm_required' ? $this->showing($page) : [];
|
if (Errors::codeOf($error) !== 'confirm_required') {
|
||||||
if ($templates === []) {
|
return $error;
|
||||||
|
}
|
||||||
|
|
||||||
|
$showing = $this->showing($page);
|
||||||
|
if (! self::isShown($showing)) {
|
||||||
return $error;
|
return $error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return VisibleEdits::merged(
|
return VisibleEdits::merged(
|
||||||
$error,
|
$error,
|
||||||
sprintf('Das Layout ist auf der Website sichtbar (%s, siehe templates).', self::count($templates)),
|
sprintf('Das Layout ist auf der Website sichtbar (%s, siehe templates).', self::count($showing)),
|
||||||
['templates' => self::listing($templates)]
|
self::details($showing)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,9 +139,10 @@ final class ThemeBuilderEdits
|
|||||||
* templates that use the layout (their singular conditions). Only templates the user may edit are named or
|
* templates that use the layout (their singular conditions). Only templates the user may edit are named or
|
||||||
* checked; others add a count (ruling Q21 pattern). Warnings only – the layout is saved anyway.
|
* checked; others add a count (ruling Q21 pattern). Warnings only – the layout is saved anyway.
|
||||||
*
|
*
|
||||||
|
* @param array{templates: list<Template>, hidden: list<Template>}|null $live live(); null = read now
|
||||||
* @return list<string>
|
* @return list<string>
|
||||||
*/
|
*/
|
||||||
public function fieldWarnings(DiviPage $page, DiviChange $change): array
|
public function fieldWarnings(DiviPage $page, DiviChange $change, ?array $live = null): array
|
||||||
{
|
{
|
||||||
if ($this->fields === null || $page->themeBuilderArea() !== 'body') {
|
if ($this->fields === null || $page->themeBuilderArea() !== 'body') {
|
||||||
return [];
|
return [];
|
||||||
@@ -127,7 +155,7 @@ final class ThemeBuilderEdits
|
|||||||
|
|
||||||
$id = (int) $page->post->ID;
|
$id = (int) $page->post->ID;
|
||||||
$uses = static fn (Template $template): bool => $template->layouts['body']['id'] === $id;
|
$uses = static fn (Template $template): bool => $template->layouts['body']['id'] === $id;
|
||||||
$live = $this->access->editableLiveTemplates();
|
$live ??= $this->live();
|
||||||
$types = [];
|
$types = [];
|
||||||
foreach (array_filter($live['templates'], $uses) as $template) {
|
foreach (array_filter($live['templates'], $uses) as $template) {
|
||||||
array_push($types, ...$template->postTypes());
|
array_push($types, ...$template->postTypes());
|
||||||
@@ -261,10 +289,38 @@ final class ThemeBuilderEdits
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param list<Template> $templates */
|
/** @param array{templates: list<Template>, hidden: int} $showing */
|
||||||
private static function count(array $templates): string
|
private static function isShown(array $showing): bool
|
||||||
{
|
{
|
||||||
return sprintf('%d aktive %s', count($templates), count($templates) === 1 ? 'Vorlage' : 'Vorlagen');
|
return $showing['templates'] !== [] || $showing['hidden'] > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The answer fields: "templates" always (also [] – DiviReplaceTextAbility tells a layout by it), and the
|
||||||
|
* hidden count only when there are any.
|
||||||
|
*
|
||||||
|
* @param array{templates: list<Template>, hidden: int} $showing
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private static function details(array $showing): array
|
||||||
|
{
|
||||||
|
return ['templates' => self::listing($showing['templates'])]
|
||||||
|
+ ($showing['hidden'] > 0 ? ['hidden_templates' => $showing['hidden']] : []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array{templates: list<Template>, hidden: int} $showing */
|
||||||
|
private static function count(array $showing): string
|
||||||
|
{
|
||||||
|
$total = count($showing['templates']) + $showing['hidden'];
|
||||||
|
$text = sprintf('%d aktive %s', $total, $total === 1 ? 'Vorlage' : 'Vorlagen');
|
||||||
|
|
||||||
|
return $showing['hidden'] > 0
|
||||||
|
? sprintf(
|
||||||
|
'%s, davon %d ohne Berechtigung – nicht aufgeführt, siehe hidden_templates',
|
||||||
|
$text,
|
||||||
|
$showing['hidden']
|
||||||
|
)
|
||||||
|
: $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -194,6 +194,58 @@ final class DiviWriterThemeBuilderTest extends TestCase
|
|||||||
self::assertSame([], $this->saved);
|
self::assertSame([], $this->saved);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ruling Q22: replace-text on a layout shown only by a template the user may not edit names nothing. */
|
||||||
|
public function testReplaceTextOnALayoutOfAHiddenTemplateLeaksNothing(): void
|
||||||
|
{
|
||||||
|
Functions\when('get_post_type_object')->justReturn(
|
||||||
|
(object) ['cap' => (object) ['edit_posts' => 'edit_pages', 'edit_others_posts' => 'edit_others_pages']]
|
||||||
|
);
|
||||||
|
Functions\when('current_user_can')->alias(static fn (string $cap, mixed $id = null): bool => $id !== 1065);
|
||||||
|
$access = ThemeBuilderFixtures::access(
|
||||||
|
[],
|
||||||
|
true,
|
||||||
|
ThemeBuilderFixtures::store([ThemeBuilderFixtures::template(1065, ['enabled' => true])])
|
||||||
|
);
|
||||||
|
$loader = ThemeBuilderFixtures::loader($access);
|
||||||
|
$writer = DiviWriterFixture::writer($this->saved, $this->failures, false, $loader, null, new ThemeBuilderEdits($access));
|
||||||
|
$ability = new DiviReplaceTextAbility(DiviFixtures::schema(), $loader, $writer, Mockery::mock(\wpdb::class)->makePartial());
|
||||||
|
|
||||||
|
$result = $ability->execute([
|
||||||
|
'search' => 'Globaler Header der Standard-Vorlage',
|
||||||
|
'replace' => 'Kopfzeile',
|
||||||
|
'dry_run' => false,
|
||||||
|
'confirm' => true,
|
||||||
|
'targets' => [['post_id' => 1061, 'tree_rev' => Rev::of((string) $this->header->post_content)]],
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::assertIsArray($result, $result instanceof \WP_Error ? $result->get_error_message() : '');
|
||||||
|
$entry = $result['results'][0];
|
||||||
|
self::assertFalse($entry['ok']);
|
||||||
|
self::assertStringContainsString('Theme-Builder-Layout 1061', $entry['error']);
|
||||||
|
self::assertSame([], $entry['templates']);
|
||||||
|
self::assertSame(1, $entry['hidden_templates']);
|
||||||
|
self::assertStringNotContainsString('1065', (string) json_encode($entry));
|
||||||
|
self::assertStringNotContainsString('Standard-Website-Template', (string) json_encode($entry));
|
||||||
|
self::assertSame([], $this->saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Review M-12 (a): the field warnings stay in the answer when the live edit is refused for its backup (P9). */
|
||||||
|
public function testFieldWarningsSurviveABackupRefusal(): void
|
||||||
|
{
|
||||||
|
Functions\when('wp_revisions_enabled')->justReturn(false);
|
||||||
|
$field = ['nodes' => [['type' => 'text', 'content' => '{{field:telefon}}']], 'confirm' => true];
|
||||||
|
$live = ThemeBuilderFixtures::template(1066, ['enabled' => true, 'use_on' => ['singular:post_type:lit_person:all']]);
|
||||||
|
$visible = new VisibleEdits(static fn (WP_Post $post): int => 1071, static fn (): bool => false);
|
||||||
|
|
||||||
|
$refused = (new DiviInsertAbility($this->writer([$live], $visible, self::unknownFields())))
|
||||||
|
->execute($this->insert($this->body, $field));
|
||||||
|
|
||||||
|
self::assertToolError($refused, 'unsupported');
|
||||||
|
self::assertStringContainsString('Papierkorb', self::clientError($refused)['message']);
|
||||||
|
self::assertStringContainsString('„telefon“', implode(' ', $refused->get_error_data()['warnings']));
|
||||||
|
self::assertSame([], $this->saved);
|
||||||
|
}
|
||||||
|
|
||||||
private static function unknownFields(): DynamicFields
|
private static function unknownFields(): DynamicFields
|
||||||
{
|
{
|
||||||
$fields = Mockery::mock(DynamicFields::class);
|
$fields = Mockery::mock(DynamicFields::class);
|
||||||
|
|||||||
@@ -92,8 +92,11 @@ final class ThemeBuilderEditsTest extends TestCase
|
|||||||
{
|
{
|
||||||
$page = self::page(ThemeBuilderFixtures::HEADER, 'header');
|
$page = self::page(ThemeBuilderFixtures::HEADER, 'header');
|
||||||
|
|
||||||
self::assertSame([], self::edits([ThemeBuilderFixtures::template(1065)])->showing($page));
|
self::assertSame(['templates' => [], 'hidden' => 0], self::edits([ThemeBuilderFixtures::template(1065)])->showing($page));
|
||||||
self::assertCount(1, self::edits([ThemeBuilderFixtures::template(1065, ['enabled' => true])])->showing($page));
|
self::assertCount(
|
||||||
|
1,
|
||||||
|
self::edits([ThemeBuilderFixtures::template(1065, ['enabled' => true])])->showing($page)['templates']
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testLayoutsOfDisabledOrDetachedTemplatesSaveAtOnce(): void
|
public function testLayoutsOfDisabledOrDetachedTemplatesSaveAtOnce(): void
|
||||||
@@ -117,6 +120,53 @@ final class ThemeBuilderEditsTest extends TestCase
|
|||||||
self::assertToolError($edits->prepare($page, []), 'confirm_required');
|
self::assertToolError($edits->prepare($page, []), 'confirm_required');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ruling Q22 (Q21 pattern): a live template the user may not edit is only counted – never its ID, title or
|
||||||
|
* conditions – but still decides that confirm is needed.
|
||||||
|
*/
|
||||||
|
public function testLiveTemplatesTheUserMayNotEditAreOnlyCounted(): void
|
||||||
|
{
|
||||||
|
Functions\when('current_user_can')->alias(static fn (string $cap, mixed $id = null): bool => $id !== 1067);
|
||||||
|
$page = self::page(ThemeBuilderFixtures::BODY, 'body');
|
||||||
|
$mine = ThemeBuilderFixtures::template(1066, ['enabled' => true]);
|
||||||
|
$theirs = ThemeBuilderFixtures::template(1067, ['enabled' => true, 'title' => 'Geheim', 'use_on' => ['singular:post_type:page:id:4711']]);
|
||||||
|
|
||||||
|
$dry = self::edits([$mine, $theirs])->prepare($page, []);
|
||||||
|
|
||||||
|
self::assertToolError($dry, 'confirm_required');
|
||||||
|
$data = $dry->get_error_data();
|
||||||
|
self::assertSame([1066], array_column($data['templates'], 'id'));
|
||||||
|
self::assertSame(1, $data['hidden_templates']);
|
||||||
|
self::assertStringContainsString('2 aktive Vorlagen, davon 1 ohne Berechtigung', self::clientError($dry)['message']);
|
||||||
|
$json = (string) json_encode($data) . self::clientError($dry)['message'];
|
||||||
|
self::assertStringNotContainsString('1067', $json);
|
||||||
|
self::assertStringNotContainsString('Geheim', $json);
|
||||||
|
self::assertStringNotContainsString('4711', $json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ruling Q22: a layout shown only by templates the user may not edit still needs confirm (fail closed, F1/Q9). */
|
||||||
|
public function testALayoutShownOnlyByHiddenTemplatesStillNeedsConfirm(): void
|
||||||
|
{
|
||||||
|
Functions\when('current_user_can')->alias(static fn (string $cap, mixed $id = null): bool => $id !== 1067);
|
||||||
|
$page = self::page(ThemeBuilderFixtures::BODY, 'body');
|
||||||
|
$edits = self::edits([ThemeBuilderFixtures::template(1067, ['enabled' => true])]);
|
||||||
|
|
||||||
|
$dry = $edits->prepare($page, []);
|
||||||
|
self::assertToolError($dry, 'confirm_required');
|
||||||
|
self::assertSame([], $dry->get_error_data()['templates']);
|
||||||
|
self::assertSame(1, $dry->get_error_data()['hidden_templates']);
|
||||||
|
|
||||||
|
$confirmed = $edits->prepare($page, ['confirm' => true]);
|
||||||
|
self::assertSame(SaveScope::ThemeBuilderLayout, $confirmed['scope']);
|
||||||
|
self::assertSame(['templates' => [], 'hidden_templates' => 1], $confirmed['details']);
|
||||||
|
|
||||||
|
$own = Errors::confirmRequired('3 Knoten löschen? Nichts wurde gelöscht.', ['ids' => ['s0']]);
|
||||||
|
$merged = $edits->notice($page, $own);
|
||||||
|
self::assertStringContainsString('sichtbar', self::clientError($merged)['message']);
|
||||||
|
self::assertSame([], $merged->get_error_data()['templates']);
|
||||||
|
self::assertSame(1, $merged->get_error_data()['hidden_templates']);
|
||||||
|
}
|
||||||
|
|
||||||
public function testNoticeExtendsAToolsOwnConfirm(): void
|
public function testNoticeExtendsAToolsOwnConfirm(): void
|
||||||
{
|
{
|
||||||
$page = self::page(ThemeBuilderFixtures::BODY, 'body');
|
$page = self::page(ThemeBuilderFixtures::BODY, 'body');
|
||||||
|
|||||||
Reference in New Issue
Block a user