fix: bounded usage scan and count-only unreadable posts in global edits (L1, L4)
GlobalUsages runs one query per request (ID, status, type – no content), checks read_post/edit_post in chunks of 200 with a primed and released post cache, lists at most 50 readable posts and refuses a confirmed global edit above 2000 live references (unsupported, edit in the Divi builder). Posts the caller may not read appear only as hidden_count / blocked_hidden_count; blocked_ids names readable posts only. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,7 +28,9 @@ defined('ABSPATH') || exit;
|
||||
* 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
|
||||
* not edit_post one of the published, private or scheduled posts that show the entry (rulings
|
||||
* P8/P8a, stricter than Divi); the dry run names them already (blocked_ids).
|
||||
* P8/P8a, stricter than Divi); the dry run names them already (blocked_ids – posts the user may
|
||||
* not read only as counts, audit L4). One GlobalUsages report per call (audit L1); above
|
||||
* GlobalUsages::HARD_CAP live posts a confirmed edit is refused (Divi builder instead).
|
||||
* The backup (PostCloner, wp_insert_post with kses) is refused when it could not be faithful: for
|
||||
* users without unfiltered_html, and when the trash is disabled (ruling P9, S37).
|
||||
*/
|
||||
@@ -89,27 +91,26 @@ final class GlobalEdits
|
||||
return ['scope' => SaveScope::LibraryItem, 'details' => []];
|
||||
}
|
||||
|
||||
$usages = $this->usages->of($id);
|
||||
$details = ['affected' => $usages['items'], 'affected_count' => $usages['count']];
|
||||
$blocked = $this->blocked($id);
|
||||
$report = $this->usages->report($id);
|
||||
if (($input['confirm'] ?? false) !== true) {
|
||||
return Errors::confirmRequired(
|
||||
sprintf(
|
||||
'Globaler Bibliothekseintrag %d: die Änderung wirkt sofort auf %d %s (siehe affected), auch '
|
||||
. 'veröffentlichte. Nichts wurde gespeichert (Probelauf).',
|
||||
$id,
|
||||
$usages['count'],
|
||||
self::pages($usages['count'])
|
||||
) . self::blockedNote($blocked),
|
||||
$details + self::blockedDetails($blocked)
|
||||
$report->count,
|
||||
self::pages($report->count)
|
||||
) . self::hiddenNote($report) . self::blockedNote($report),
|
||||
$report->details()
|
||||
);
|
||||
}
|
||||
|
||||
$refusal = $this->pagesRefusal($id, $blocked) ?? $this->backupRefusal($item);
|
||||
$refusal = $this->pagesRefusal($id, $report) ?? $this->backupRefusal($item);
|
||||
if ($refusal !== null) {
|
||||
return $refusal;
|
||||
}
|
||||
|
||||
$details = $report->listing();
|
||||
if (! wp_revisions_enabled($item)) {
|
||||
$details['backup_id'] = ($this->backup)($item);
|
||||
}
|
||||
@@ -127,9 +128,7 @@ final class GlobalEdits
|
||||
return $error;
|
||||
}
|
||||
|
||||
$id = (int) $page->post->ID;
|
||||
$usages = $this->usages->of($id);
|
||||
$blocked = $this->blocked($id);
|
||||
$report = $this->usages->report((int) $page->post->ID);
|
||||
$data = $error->get_error_data();
|
||||
$details = is_array($data) ? array_diff_key($data, ['status' => true, 'hint' => true]) : [];
|
||||
|
||||
@@ -137,79 +136,93 @@ final class GlobalEdits
|
||||
Errors::clientMessage($error)
|
||||
. sprintf(
|
||||
' Globaler Bibliothekseintrag: wirkt auf %d %s (siehe affected).',
|
||||
$usages['count'],
|
||||
self::pages($usages['count'])
|
||||
$report->count,
|
||||
self::pages($report->count)
|
||||
)
|
||||
. self::blockedNote($blocked),
|
||||
array_merge(
|
||||
$details,
|
||||
['affected' => $usages['items'], 'affected_count' => $usages['count']],
|
||||
self::blockedDetails($blocked)
|
||||
)
|
||||
. self::hiddenNote($report)
|
||||
. self::blockedNote($report),
|
||||
array_merge($details, $report->details())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruling P8a: the live posts (published, private, scheduled – they render now or will) that show
|
||||
* entry $id and that the user may not edit_post. Drafts and pending posts never block.
|
||||
*
|
||||
* @return list<int>
|
||||
* 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).
|
||||
*/
|
||||
private function blocked(int $id): array
|
||||
private function pagesRefusal(int $id, UsageReport $report): ?WP_Error
|
||||
{
|
||||
$live = $this->usages->liveIds($id);
|
||||
if ($live === []) {
|
||||
return [];
|
||||
if ($report->tooMany) {
|
||||
return Errors::unsupported(
|
||||
sprintf(
|
||||
'Globaler Bibliothekseintrag %d erscheint auf %d veröffentlichten, privaten oder geplanten '
|
||||
. 'Einträgen – mehr als %d; so große Änderungen nimmt LIT MCP nicht vor. Nichts wurde '
|
||||
. 'gespeichert.',
|
||||
$id,
|
||||
$report->live,
|
||||
GlobalUsages::HARD_CAP
|
||||
),
|
||||
'Den Eintrag im Divi-Builder bearbeiten.'
|
||||
);
|
||||
}
|
||||
|
||||
_prime_post_caches($live, false, false);
|
||||
|
||||
return array_values(array_filter(
|
||||
$live,
|
||||
static fn (int $postId): bool => ! current_user_can('edit_post', $postId)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruling P8a: a confirmed edit needs edit_post on every live post that shows entry $id.
|
||||
*
|
||||
* @param list<int> $blocked
|
||||
*/
|
||||
private function pagesRefusal(int $id, array $blocked): ?WP_Error
|
||||
{
|
||||
if ($blocked === []) {
|
||||
if (! $report->blocks()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Errors::forbidden(
|
||||
sprintf(
|
||||
'Globaler Bibliothekseintrag %d erscheint auf veröffentlichten, privaten oder geplanten Einträgen, '
|
||||
. 'die dieses Konto nicht bearbeiten darf (IDs %s); nichts wurde gespeichert.',
|
||||
. 'die dieses Konto nicht bearbeiten darf (%s); nichts wurde gespeichert.',
|
||||
$id,
|
||||
self::listed($blocked)
|
||||
self::blockedList($report)
|
||||
),
|
||||
'Die Änderung von jemandem vornehmen lassen, der alle diese Einträge bearbeiten darf (z. B. Redakteur '
|
||||
. 'oder Administrator).'
|
||||
);
|
||||
}
|
||||
|
||||
/** @param list<int> $blocked */
|
||||
private static function blockedNote(array $blocked): string
|
||||
/** Audit L4: posts the caller may not read appear only as a count. */
|
||||
private static function hiddenNote(UsageReport $report): string
|
||||
{
|
||||
return $blocked === [] ? '' : sprintf(
|
||||
' Achtung: Die Bestätigung würde abgelehnt – veröffentlichte, private oder geplante Einträge, die dieses '
|
||||
. 'Konto nicht bearbeiten darf, zeigen den Eintrag (IDs %s, siehe blocked_ids).',
|
||||
self::listed($blocked)
|
||||
return $report->hidden === 0 ? '' : sprintf(
|
||||
' %d davon darf dieses Konto nicht lesen (nur gezählt, siehe hidden_count).',
|
||||
$report->hidden
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $blocked
|
||||
* @return array<string, list<int>>
|
||||
*/
|
||||
private static function blockedDetails(array $blocked): array
|
||||
private static function blockedNote(UsageReport $report): string
|
||||
{
|
||||
return $blocked === [] ? [] : ['blocked_ids' => $blocked];
|
||||
if ($report->tooMany) {
|
||||
return sprintf(
|
||||
' Achtung: Die Bestätigung würde abgelehnt – mehr als %d veröffentlichte, private oder geplante '
|
||||
. 'Einträge zeigen den Eintrag; ihn im Divi-Builder bearbeiten.',
|
||||
GlobalUsages::HARD_CAP
|
||||
);
|
||||
}
|
||||
|
||||
return ! $report->blocks() ? '' : sprintf(
|
||||
' Achtung: Die Bestätigung würde abgelehnt – veröffentlichte, private oder geplante Einträge, die dieses '
|
||||
. 'Konto nicht bearbeiten darf, zeigen den Eintrag (%s).',
|
||||
self::blockedList($report)
|
||||
);
|
||||
}
|
||||
|
||||
/** Readable blocking posts by ID (at most LISTED_IDS, see blocked_ids), unreadable ones as a count. */
|
||||
private static function blockedList(UsageReport $report): string
|
||||
{
|
||||
$parts = [];
|
||||
if ($report->blocked !== []) {
|
||||
$parts[] = 'IDs ' . self::listed($report->blocked) . ', siehe blocked_ids';
|
||||
}
|
||||
if ($report->blockedHidden > 0) {
|
||||
$parts[] = sprintf(
|
||||
'%d %s, die dieses Konto nicht lesen darf',
|
||||
$report->blockedHidden,
|
||||
$report->blockedHidden === 1 ? 'Eintrag' : 'Einträge'
|
||||
);
|
||||
}
|
||||
|
||||
return implode('; ', $parts);
|
||||
}
|
||||
|
||||
/** @param list<int> $ids At most LISTED_IDS of them, then "und N weitere". */
|
||||
|
||||
Reference in New Issue
Block a user