fix: global-edit rules for every library entry that pages show (L2)
Divi renders any referenced et_pb_layout, whatever its scope term. A normal entry with usages now gets the dry run, confirm, P8a, backup and the site-wide GlobalItem scope; lit-divi-delete's notice and the library-list detail name its pages too. The usage scan is the same single query. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,8 +12,8 @@ Die Divi-Bibliothek speichert Layouts, Abschnitte, Zeilen und Module zur Wiederv
|
|||||||
- `lit/library-list` (über `mcp-adapter-execute-ability`): Einträge auflisten, Filter `layout_type`
|
- `lit/library-list` (über `mcp-adapter-execute-ability`): Einträge auflisten, Filter `layout_type`
|
||||||
(`layout`, `section`, `row`, `module`), `scope` (`normal`, `global`), `search` (Titel), `category`
|
(`layout`, `section`, `row`, `module`), `scope` (`normal`, `global`), `search` (Titel), `category`
|
||||||
(Name), weiterblättern mit `cursor`. Gelistet werden nur Einträge, die das Konto bearbeiten darf. Mit `id`:
|
(Name), weiterblättern mit `cursor`. Gelistet werden nur Einträge, die das Konto bearbeiten darf. Mit `id`:
|
||||||
ein Eintrag im Detail – Knoten-Übersicht (`outline`), `tree_rev` und bei globalen Einträgen die Seiten, die
|
ein Eintrag im Detail – Knoten-Übersicht (`outline`), `tree_rev` und die Seiten, die ihn als globales Modul
|
||||||
ihn zeigen (`affected`, `affected_count`).
|
zeigen (`affected`, `affected_count`, `hidden_count`).
|
||||||
- `lit-library-insert`: einen Eintrag in eine Divi-Seite einfügen. `mode: "global"` (Standard bei globalen
|
- `lit-library-insert`: einen Eintrag in eine Divi-Seite einfügen. `mode: "global"` (Standard bei globalen
|
||||||
Einträgen) fügt eine verknüpfte Instanz ein, `mode: "copy"` (Standard sonst) eine unabhängige Kopie.
|
Einträgen) fügt eine verknüpfte Instanz ein, `mode: "copy"` (Standard sonst) eine unabhängige Kopie.
|
||||||
Position wie bei `lit-divi-insert` (`parent`, `before`, `after`, ohne Angabe ans Seitenende); `tree_rev`
|
Position wie bei `lit-divi-insert` (`parent`, `before`, `after`, ohne Angabe ans Seitenende); `tree_rev`
|
||||||
@@ -42,7 +42,8 @@ Die Divi-Bibliothek speichert Layouts, Abschnitte, Zeilen und Module zur Wiederv
|
|||||||
(nur lesbar)`; `lit-divi-node-get` nennt unter `global` die ID des Bibliothekseintrags. Auf der Seite
|
(nur lesbar)`; `lit-divi-node-get` nennt unter `global` die ID des Bibliothekseintrags. Auf der Seite
|
||||||
lassen sie sich nur als Ganzes verschieben, duplizieren oder löschen.
|
lassen sie sich nur als Ganzes verschieben, duplizieren oder löschen.
|
||||||
2. `lit-divi-outline` mit `post_id` = ID des Eintrags lesen und die Änderung wie auf einer Seite senden.
|
2. `lit-divi-outline` mit `post_id` = ID des Eintrags lesen und die Änderung wie auf einer Seite senden.
|
||||||
3. Ohne `confirm` ist jede Änderung eines globalen Eintrags ein Probelauf: Antwort `confirm_required` mit
|
3. Ohne `confirm` ist jede Änderung eines globalen Eintrags ein Probelauf – ebenso bei einem normalen Eintrag,
|
||||||
|
den Seiten als globales Modul zeigen (Divi zeigt dort dessen aktuellen Inhalt): Antwort `confirm_required` mit
|
||||||
`affected` (betroffene Seiten, höchstens 50), `affected_count` (alle) und `hidden_count` (Seiten, die das
|
`affected` (betroffene Seiten, höchstens 50), `affected_count` (alle) und `hidden_count` (Seiten, die das
|
||||||
Konto nicht lesen darf – sie werden nur gezählt, ohne ID); gespeichert wird nichts.
|
Konto nicht lesen darf – sie werden nur gezählt, ohne ID); gespeichert wird nichts.
|
||||||
4. Dem Nutzer die betroffenen Seiten nennen und fragen. Erst nach Zustimmung dieselbe Änderung mit
|
4. Dem Nutzer die betroffenen Seiten nennen und fragen. Erst nach Zustimmung dieselbe Änderung mit
|
||||||
@@ -62,7 +63,8 @@ Werkzeug die Änderung außerdem ab (`unsupported`), wenn der Papierkorb deaktiv
|
|||||||
`unfiltered_html` hat (die Sicherungskopie wäre dann nicht verlustfrei).
|
`unfiltered_html` hat (die Sicherungskopie wäre dann nicht verlustfrei).
|
||||||
|
|
||||||
`lit/divi-replace-text` nimmt Bibliothekseinträge als `targets` (ID und `tree_rev` aus `lit-divi-outline`;
|
`lit/divi-replace-text` nimmt Bibliothekseinträge als `targets` (ID und `tree_rev` aus `lit-divi-outline`;
|
||||||
der Probelauf listet nur Seiten des Inhaltstyps): normale Einträge werden gespeichert, globale nie – ihr
|
der Probelauf listet nur Seiten des Inhaltstyps): normale Einträge werden gespeichert, globale (und normale,
|
||||||
|
die Seiten als globales Modul zeigen) nie – ihr
|
||||||
Ergebnis nennt stattdessen die betroffenen Seiten; die Änderung dann nach Rückfrage mit `lit-divi-update`
|
Ergebnis nennt stattdessen die betroffenen Seiten; die Änderung dann nach Rückfrage mit `lit-divi-update`
|
||||||
und `confirm: true` vornehmen.
|
und `confirm: true` vornehmen.
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ defined('ABSPATH') || exit;
|
|||||||
* The extra rules of a Divi write to a library entry (E2, answer F2), checked after the whole
|
* The extra rules of a Divi write to a library entry (E2, answer F2), checked after the whole
|
||||||
* change validated and before anything is saved:
|
* change validated and before anything is saved:
|
||||||
* - every entry keeps its shape (LibraryContent::shapeViolation) – instances expect their root type;
|
* - every entry keeps its shape (LibraryContent::shapeViolation) – instances expect their root type;
|
||||||
* - a global entry changes every page that shows it at once (F7): without confirm: true nothing is
|
* - a global entry – and any entry that pages show as an instance, global scope or not (audit L2:
|
||||||
* saved and the answer lists those pages (a dry run of the fully validated change); with it the
|
* Divi renders every referenced entry) – changes every page that shows it at once (F7): without
|
||||||
|
* confirm: true nothing is saved and the answer lists those pages (a dry run of the fully
|
||||||
|
* validated change); with it the
|
||||||
* save scope is GlobalItem (site-wide cache) and, when revisions are off for the entry, a backup
|
* save scope is GlobalItem (site-wide cache) and, when revisions are off for the entry, a backup
|
||||||
* copy is made first – the revision itself is DiviSaver's restore point, as for every page.
|
* copy is made first – the revision itself is DiviSaver's restore point, as for every page.
|
||||||
* Confirm stays honour-based (ruling P9, like D10). A confirmed edit is refused while the user may
|
* Confirm stays honour-based (ruling P9, like D10). A confirmed edit is refused while the user may
|
||||||
@@ -87,17 +89,18 @@ final class GlobalEdits
|
|||||||
return Errors::invalidInput($violation . ' Nichts wurde gespeichert.', ['post_id' => $violation]);
|
return Errors::invalidInput($violation . ' Nichts wurde gespeichert.', ['post_id' => $violation]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $global) {
|
// Audit L2: Divi renders every entry an instance names, global scope or not.
|
||||||
|
$report = $this->usages->report($id);
|
||||||
|
if (! $global && ! $report->exists()) {
|
||||||
return ['scope' => SaveScope::LibraryItem, 'details' => []];
|
return ['scope' => SaveScope::LibraryItem, 'details' => []];
|
||||||
}
|
}
|
||||||
|
|
||||||
$report = $this->usages->report($id);
|
|
||||||
if (($input['confirm'] ?? false) !== true) {
|
if (($input['confirm'] ?? false) !== true) {
|
||||||
return Errors::confirmRequired(
|
return Errors::confirmRequired(
|
||||||
sprintf(
|
sprintf(
|
||||||
'Globaler Bibliothekseintrag %d: die Änderung wirkt sofort auf %d %s (siehe affected), auch '
|
'%s: die Änderung wirkt sofort auf %d %s (siehe affected), auch veröffentlichte. Nichts wurde '
|
||||||
. 'veröffentlichte. Nichts wurde gespeichert (Probelauf).',
|
. 'gespeichert (Probelauf).',
|
||||||
$id,
|
self::label($id, $global),
|
||||||
$report->count,
|
$report->count,
|
||||||
self::pages($report->count)
|
self::pages($report->count)
|
||||||
) . self::hiddenNote($report) . self::blockedNote($report),
|
) . self::hiddenNote($report) . self::blockedNote($report),
|
||||||
@@ -105,7 +108,7 @@ final class GlobalEdits
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$refusal = $this->pagesRefusal($id, $report) ?? $this->backupRefusal($item);
|
$refusal = $this->pagesRefusal(self::label($id, $global), $report) ?? $this->backupRefusal($item);
|
||||||
if ($refusal !== null) {
|
if ($refusal !== null) {
|
||||||
return $refusal;
|
return $refusal;
|
||||||
}
|
}
|
||||||
@@ -119,23 +122,29 @@ final class GlobalEdits
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A tool's own confirm_required (lit-divi-delete) on a global entry, extended by the affected
|
* A tool's own confirm_required (lit-divi-delete) on a global or referenced entry (audit L2),
|
||||||
* pages – so the one confirm the client sends covers both questions.
|
* extended by the affected pages – so the one confirm the client sends covers both questions.
|
||||||
*/
|
*/
|
||||||
public function notice(DiviPage $page, WP_Error $error): WP_Error
|
public function notice(DiviPage $page, WP_Error $error): WP_Error
|
||||||
{
|
{
|
||||||
if (Errors::codeOf($error) !== 'confirm_required' || ! $this->access->isGlobal($page->post)) {
|
if (Errors::codeOf($error) !== 'confirm_required') {
|
||||||
return $error;
|
return $error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$global = $this->access->isGlobal($page->post);
|
||||||
$report = $this->usages->report((int) $page->post->ID);
|
$report = $this->usages->report((int) $page->post->ID);
|
||||||
|
if (! $global && ! $report->exists()) {
|
||||||
|
return $error;
|
||||||
|
}
|
||||||
|
|
||||||
$data = $error->get_error_data();
|
$data = $error->get_error_data();
|
||||||
$details = is_array($data) ? array_diff_key($data, ['status' => true, 'hint' => true]) : [];
|
$details = is_array($data) ? array_diff_key($data, ['status' => true, 'hint' => true]) : [];
|
||||||
|
|
||||||
return Errors::confirmRequired(
|
return Errors::confirmRequired(
|
||||||
Errors::clientMessage($error)
|
Errors::clientMessage($error)
|
||||||
. sprintf(
|
. sprintf(
|
||||||
' Globaler Bibliothekseintrag: wirkt auf %d %s (siehe affected).',
|
' %s: wirkt auf %d %s (siehe affected).',
|
||||||
|
$global ? 'Globaler Bibliothekseintrag' : 'Bibliothekseintrag (als globales Modul verwendet)',
|
||||||
$report->count,
|
$report->count,
|
||||||
self::pages($report->count)
|
self::pages($report->count)
|
||||||
)
|
)
|
||||||
@@ -149,15 +158,14 @@ final class GlobalEdits
|
|||||||
* Ruling P8a: a confirmed edit needs edit_post on every live post that shows entry $id; above
|
* Ruling P8a: a confirmed edit needs edit_post on every live post that shows entry $id; above
|
||||||
* GlobalUsages::HARD_CAP live posts it is refused as a whole (audit L1).
|
* GlobalUsages::HARD_CAP live posts it is refused as a whole (audit L1).
|
||||||
*/
|
*/
|
||||||
private function pagesRefusal(int $id, UsageReport $report): ?WP_Error
|
private function pagesRefusal(string $label, UsageReport $report): ?WP_Error
|
||||||
{
|
{
|
||||||
if ($report->tooMany) {
|
if ($report->tooMany) {
|
||||||
return Errors::unsupported(
|
return Errors::unsupported(
|
||||||
sprintf(
|
sprintf(
|
||||||
'Globaler Bibliothekseintrag %d erscheint auf %d veröffentlichten, privaten oder geplanten '
|
'%s erscheint auf %d veröffentlichten, privaten oder geplanten Einträgen – mehr als %d; so große '
|
||||||
. 'Einträgen – mehr als %d; so große Änderungen nimmt LIT MCP nicht vor. Nichts wurde '
|
. 'Änderungen nimmt LIT MCP nicht vor. Nichts wurde gespeichert.',
|
||||||
. 'gespeichert.',
|
$label,
|
||||||
$id,
|
|
||||||
$report->live,
|
$report->live,
|
||||||
GlobalUsages::HARD_CAP
|
GlobalUsages::HARD_CAP
|
||||||
),
|
),
|
||||||
@@ -171,9 +179,9 @@ final class GlobalEdits
|
|||||||
|
|
||||||
return Errors::forbidden(
|
return Errors::forbidden(
|
||||||
sprintf(
|
sprintf(
|
||||||
'Globaler Bibliothekseintrag %d erscheint auf veröffentlichten, privaten oder geplanten Einträgen, '
|
'%s erscheint auf veröffentlichten, privaten oder geplanten Einträgen, die dieses Konto nicht '
|
||||||
. 'die dieses Konto nicht bearbeiten darf (%s); nichts wurde gespeichert.',
|
. 'bearbeiten darf (%s); nichts wurde gespeichert.',
|
||||||
$id,
|
$label,
|
||||||
self::blockedList($report)
|
self::blockedList($report)
|
||||||
),
|
),
|
||||||
'Die Änderung von jemandem vornehmen lassen, der alle diese Einträge bearbeiten darf (z. B. Redakteur '
|
'Die Änderung von jemandem vornehmen lassen, der alle diese Einträge bearbeiten darf (z. B. Redakteur '
|
||||||
@@ -234,6 +242,14 @@ final class GlobalEdits
|
|||||||
. ($more > 0 ? sprintf(' und %d weitere', $more) : '');
|
. ($more > 0 ? sprintf(' und %d weitere', $more) : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** How messages name entry $id: global, or a normal entry that pages show as a global module (L2). */
|
||||||
|
private static function label(int $id, bool $global): string
|
||||||
|
{
|
||||||
|
return $global
|
||||||
|
? sprintf('Globaler Bibliothekseintrag %d', $id)
|
||||||
|
: sprintf('Bibliothekseintrag %d (auf Seiten als globales Modul verwendet)', $id);
|
||||||
|
}
|
||||||
|
|
||||||
private static function pages(int $count): string
|
private static function pages(int $count): string
|
||||||
{
|
{
|
||||||
return $count === 1 ? 'Seite' : 'Seiten';
|
return $count === 1 ? 'Seite' : 'Seiten';
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ defined('ABSPATH') || exit;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* lit/library-list: the Divi Library (spec §5.7, E9 – replaces lit/library-get). Without id a page
|
* lit/library-list: the Divi Library (spec §5.7, E9 – replaces lit/library-get). Without id a page
|
||||||
* of published entries (filters, cursor); with id one entry with its node outline, tree_rev and,
|
* of published entries (filters, cursor); with id one entry with its node outline, tree_rev and
|
||||||
* for a global entry, the pages that show it. Rights: LibraryAccess (edit_posts + divi_library;
|
* the pages that show it as an instance (global entries always, others when any page does, L2).
|
||||||
* every listed entry also edit_post – others are left out, without title or count). With id,
|
* Rights: LibraryAccess (edit_posts + divi_library; every listed entry also edit_post – others are
|
||||||
|
* left out, without title or count). With id,
|
||||||
* everything but a library entry the user may read answers like a missing ID (ruling P12).
|
* everything but a library entry the user may read answers like a missing ID (ruling P12).
|
||||||
*/
|
*/
|
||||||
final class LibraryListAbility implements Ability
|
final class LibraryListAbility implements Ability
|
||||||
@@ -53,7 +54,7 @@ final class LibraryListAbility implements Ability
|
|||||||
{
|
{
|
||||||
return 'Listet Einträge der Divi-Bibliothek (Layouts, Abschnitte, Zeilen, Module; normal oder global), '
|
return 'Listet Einträge der Divi-Bibliothek (Layouts, Abschnitte, Zeilen, Module; normal oder global), '
|
||||||
. 'filterbar nach layout_type, scope, search und category, blätterbar mit cursor. Mit id: ein Eintrag '
|
. 'filterbar nach layout_type, scope, search und category, blätterbar mit cursor. Mit id: ein Eintrag '
|
||||||
. 'im Detail – Knoten-Übersicht, tree_rev und bei globalen Einträgen die Seiten, die ihn zeigen '
|
. 'im Detail – Knoten-Übersicht, tree_rev und die Seiten, die ihn als globales Modul zeigen '
|
||||||
. '(affected). Einfügen mit lit-library-insert; bearbeiten mit den Divi-Werkzeugen und der ID als post_id.';
|
. '(affected). Einfügen mit lit-library-insert; bearbeiten mit den Divi-Werkzeugen und der ID als post_id.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,8 +233,10 @@ final class LibraryListAbility implements Ability
|
|||||||
'rev' => Rev::ofPost($page->post),
|
'rev' => Rev::ofPost($page->post),
|
||||||
'edit_url' => $page->editUrl(),
|
'edit_url' => $page->editUrl(),
|
||||||
];
|
];
|
||||||
if ($item['scope'] === 'global') {
|
// Audit L2: a normal entry that pages show as an instance names them too.
|
||||||
$result += $this->usages->report((int) $page->post->ID, false)->listing();
|
$report = $this->usages->report((int) $page->post->ID, false);
|
||||||
|
if ($item['scope'] === 'global' || $report->exists()) {
|
||||||
|
$result += $report->listing();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
|
|||||||
@@ -113,10 +113,10 @@ final class LibraryFixtures
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GlobalUsages whose query answers $rows (a closure is asked on every call); the rights checks
|
* GlobalUsages whose query answers $rows (a closure gets the entry ID on every call); the rights checks
|
||||||
* are the real ones, with the post-cache functions stubbed.
|
* are the real ones, with the post-cache functions stubbed.
|
||||||
*
|
*
|
||||||
* @param list<array{id: int, status: string, type: string}>|\Closure(): list<array{id: int, status: string, type: string}> $rows
|
* @param list<array{id: int, status: string, type: string}>|\Closure(int): list<array{id: int, status: string, type: string}> $rows
|
||||||
*/
|
*/
|
||||||
public static function usages(array|\Closure $rows, ?DiviRoles $roles = null): GlobalUsages
|
public static function usages(array|\Closure $rows, ?DiviRoles $roles = null): GlobalUsages
|
||||||
{
|
{
|
||||||
@@ -124,7 +124,7 @@ final class LibraryFixtures
|
|||||||
Functions\when('_prime_post_caches')->justReturn(null);
|
Functions\when('_prime_post_caches')->justReturn(null);
|
||||||
Functions\when('wp_cache_delete')->justReturn(true);
|
Functions\when('wp_cache_delete')->justReturn(true);
|
||||||
$usages = Mockery::mock(GlobalUsages::class, [null, $roles ?? self::roles()])->makePartial();
|
$usages = Mockery::mock(GlobalUsages::class, [null, $roles ?? self::roles()])->makePartial();
|
||||||
$usages->allows('rows')->andReturnUsing(static fn (): array => is_array($rows) ? $rows : $rows());
|
$usages->allows('rows')->andReturnUsing(static fn (int $id): array => is_array($rows) ? $rows : $rows($id));
|
||||||
|
|
||||||
return $usages;
|
return $usages;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,10 +50,17 @@ final class GlobalEditsTest extends TestCase
|
|||||||
Functions\when('_prime_post_caches')->justReturn(null);
|
Functions\when('_prime_post_caches')->justReturn(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The live posts ($this->live; 1053 published, the others private) and draft 1070. */
|
/** @var list<int> Normal (non-global) entries that pages nevertheless show (audit L2). */
|
||||||
|
private array $referenced = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For the global entries and $this->referenced: the live posts ($this->live; 1053 published, the
|
||||||
|
* others private) and draft 1070. Other entries are shown nowhere.
|
||||||
|
*/
|
||||||
private function usages(): GlobalUsages
|
private function usages(): GlobalUsages
|
||||||
{
|
{
|
||||||
return LibraryFixtures::usages(fn (): array => array_merge(
|
return LibraryFixtures::usages(fn (int $id): array => ! LibraryFixtures::ITEMS[$id][2]
|
||||||
|
&& ! in_array($id, $this->referenced, true) ? [] : array_merge(
|
||||||
array_map(
|
array_map(
|
||||||
static fn (int $id): array => ['id' => $id, 'status' => $id === 1053 ? 'publish' : 'private', 'type' => 'page'],
|
static fn (int $id): array => ['id' => $id, 'status' => $id === 1053 ? 'publish' : 'private', 'type' => 'page'],
|
||||||
$this->live
|
$this->live
|
||||||
@@ -133,6 +140,45 @@ final class GlobalEditsTest extends TestCase
|
|||||||
self::assertArrayNotHasKey('affected', $result);
|
self::assertArrayNotHasKey('affected', $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audit L2: Divi renders every referenced entry, global scope or not – a normal entry that pages
|
||||||
|
* show gets the dry run, P8a and the site-wide scope of a global one.
|
||||||
|
*/
|
||||||
|
public function testAReferencedNormalEntryIsEditedLikeAGlobalOne(): void
|
||||||
|
{
|
||||||
|
LibraryFixtures::given([LibraryFixtures::item(1057)]);
|
||||||
|
$this->referenced = [1057];
|
||||||
|
$this->live = [1053, 1080];
|
||||||
|
|
||||||
|
$dryRun = (new DiviUpdateAbility($this->writer()))->execute(self::relabel(1057));
|
||||||
|
self::denyOn('edit_post', [1080]);
|
||||||
|
$blocked = (new DiviUpdateAbility($this->writer()))->execute(self::relabel(1057, true));
|
||||||
|
LibraryFixtures::given([LibraryFixtures::item(1057)]);
|
||||||
|
$done = (new DiviUpdateAbility($this->writer()))->execute(self::relabel(1057, true));
|
||||||
|
|
||||||
|
self::assertToolError($dryRun, 'confirm_required');
|
||||||
|
self::assertStringContainsString('Bibliothekseintrag 1057', $dryRun->get_error_message());
|
||||||
|
self::assertStringContainsString('als globales Modul', $dryRun->get_error_message());
|
||||||
|
self::assertSame(3, $dryRun->get_error_data()['affected_count']);
|
||||||
|
self::assertToolError($blocked, 'forbidden');
|
||||||
|
self::assertTrue($done['ok']);
|
||||||
|
self::assertCount(1, $this->saved);
|
||||||
|
self::assertSame(SaveScope::GlobalItem, $this->saved[0][1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeleteInAReferencedNormalEntryNamesTheAffectedPages(): void
|
||||||
|
{
|
||||||
|
LibraryFixtures::given([LibraryFixtures::item(1055)]);
|
||||||
|
$this->referenced = [1055];
|
||||||
|
|
||||||
|
$result = (new DiviDeleteAbility($this->writer()))->execute(
|
||||||
|
['post_id' => 1055, 'tree_rev' => self::rev(1055), 'ids' => ['s0.r0.c0.m0']]
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertToolError($result, 'confirm_required');
|
||||||
|
self::assertSame(2, $result->get_error_data()['affected_count']);
|
||||||
|
}
|
||||||
|
|
||||||
public function testGlobalItemDryRunNamesTheAffectedPagesAndSavesNothing(): void
|
public function testGlobalItemDryRunNamesTheAffectedPagesAndSavesNothing(): void
|
||||||
{
|
{
|
||||||
LibraryFixtures::given([LibraryFixtures::item(1059)]);
|
LibraryFixtures::given([LibraryFixtures::item(1059)]);
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ final class LibraryListAbilityTest extends TestCase
|
|||||||
));
|
));
|
||||||
$access = LibraryFixtures::access($deniedRoles, true, $store);
|
$access = LibraryFixtures::access($deniedRoles, true, $store);
|
||||||
$usages = LibraryFixtures::usages(
|
$usages = LibraryFixtures::usages(
|
||||||
static fn (): array => [['id' => 1053, 'status' => 'draft', 'type' => 'page']]
|
static fn (int $id): array => in_array($id, [1058, 1056], true)
|
||||||
|
? [['id' => 1053, 'status' => 'draft', 'type' => 'page']]
|
||||||
|
: []
|
||||||
);
|
);
|
||||||
|
|
||||||
return new LibraryListAbility($access, LibraryFixtures::loader($access), DiviWriterFixture::designReader(), $usages);
|
return new LibraryListAbility($access, LibraryFixtures::loader($access), DiviWriterFixture::designReader(), $usages);
|
||||||
@@ -185,6 +187,18 @@ final class LibraryListAbilityTest extends TestCase
|
|||||||
self::assertToolError($this->ability()->permission(['id' => 1058]), 'forbidden');
|
self::assertToolError($this->ability()->permission(['id' => 1058]), 'forbidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Audit L2: a normal entry that a page shows as an instance names that page too. */
|
||||||
|
public function testDetailOfAReferencedNormalEntryNamesItsPages(): void
|
||||||
|
{
|
||||||
|
LibraryFixtures::given([LibraryFixtures::item(1056)]);
|
||||||
|
|
||||||
|
$result = $this->ability([], [1056])->execute(['id' => 1056]);
|
||||||
|
|
||||||
|
self::assertSame('normal', $result['items'][0]['scope']);
|
||||||
|
self::assertSame(1, $result['affected_count']);
|
||||||
|
self::assertSame(1053, $result['affected'][0]['post_id']);
|
||||||
|
}
|
||||||
|
|
||||||
public function testDetailOfANormalEntryHasNoAffectedPages(): void
|
public function testDetailOfANormalEntryHasNoAffectedPages(): void
|
||||||
{
|
{
|
||||||
LibraryFixtures::given([LibraryFixtures::item(1055)]);
|
LibraryFixtures::given([LibraryFixtures::item(1055)]);
|
||||||
|
|||||||
Reference in New Issue
Block a user