fix: global usages follow the Theme Builder access (ruling Q17 M1)
GlobalUsages gets the Theme Builder access injected as a closure (ThemeBuilderAccess::usageState(), wired at both construction sites through DiviIntegration::globalUsages()). Theme Builder posts are named only while the Theme Builder is visible to the account, otherwise only counted (also in blocked_ids); a global edit that changes them needs canUse(), a writable "Divi Theme Builder" group and edit_post. Without an injected access the fail-safe default checks what canUse() checks. The duplicate THEME_BUILDER_TYPES list gives way to DiviThemeBuilder::postTypes(). 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:
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Library;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
|
||||
use WP_Post;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
@@ -16,8 +18,10 @@ defined('ABSPATH') || exit;
|
||||
* rights of the caller are then checked in chunks with a bounded post cache:
|
||||
* - readable posts are listed (at most LIMIT, with title); unreadable ones only count (audit L4);
|
||||
* - live posts (ruling P8a) the caller may not edit_post block a confirmed edit – by ID when
|
||||
* readable, else as a count; Theme Builder layouts also without Divi's theme_builder role option
|
||||
* (audit L6); above HARD_CAP live posts the edit is refused as a whole.
|
||||
* readable, else as a count; above HARD_CAP live posts the edit is refused as a whole;
|
||||
* - Theme Builder posts follow the Theme Builder access (ruling Q17 M1, review I-2, audit L1/L6): readable
|
||||
* only while the Theme Builder is visible to the caller (else counted, never named – P12, like
|
||||
* ThemeBuilderAccess), editable only while the caller may use it (canUse()) plus edit_post.
|
||||
* Not final so tests can mock rows().
|
||||
*/
|
||||
class GlobalUsages
|
||||
@@ -46,13 +50,23 @@ class GlobalUsages
|
||||
/** Every post that exists for the user: listed in the dry run. */
|
||||
private const LISTED = "post_status NOT IN ('trash', 'auto-draft', 'inherit')";
|
||||
|
||||
/** Theme Builder post types (Divi's et_theme_builder_get_layout_post_types() plus the templates). */
|
||||
public const THEME_BUILDER_TYPES = ['et_header_layout', 'et_body_layout', 'et_footer_layout', 'et_template'];
|
||||
|
||||
private DiviRoles $roles;
|
||||
|
||||
public function __construct(private ?\wpdb $db = null, ?DiviRoles $roles = null)
|
||||
{
|
||||
/** @var array{visible: bool, usable: bool}|null The Theme Builder access of the current report(). */
|
||||
private ?array $themeBuilderState = null;
|
||||
|
||||
/**
|
||||
* @param (Closure(): array{visible: bool, usable: bool})|null $themeBuilder The caller's Theme Builder access
|
||||
* (DiviIntegration wraps ThemeBuilderAccess – a closure keeps Library free of it): visible = TB objects exist
|
||||
* for the caller, usable = the caller may change them; asked at most once per report(). null = what
|
||||
* ThemeBuilderAccess::canUse() needs, for both (Divi's role option theme_builder, edit_others_posts,
|
||||
* edit_theme_options).
|
||||
*/
|
||||
public function __construct(
|
||||
private ?\wpdb $db = null,
|
||||
?DiviRoles $roles = null,
|
||||
private ?Closure $themeBuilder = null
|
||||
) {
|
||||
$this->roles = $roles ?? new DiviRoles();
|
||||
}
|
||||
|
||||
@@ -67,6 +81,7 @@ class GlobalUsages
|
||||
$tooMany = $withBlocked && $live > self::HARD_CAP;
|
||||
$checkEdit = $withBlocked && ! $tooMany;
|
||||
|
||||
$this->themeBuilderState = null;
|
||||
$items = [];
|
||||
$hidden = 0;
|
||||
$blocked = [];
|
||||
@@ -95,7 +110,7 @@ class GlobalUsages
|
||||
_prime_post_caches($ids, false, false);
|
||||
foreach ($todo as $row) {
|
||||
$id = $row['id'];
|
||||
$readable = current_user_can('read_post', $id);
|
||||
$readable = $this->readable($row);
|
||||
if ($readable && count($items) < self::LIMIT) {
|
||||
$post = get_post($id);
|
||||
$items[] = [
|
||||
@@ -209,21 +224,63 @@ class GlobalUsages
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruling P8a: whether the caller may edit_post the live post $row; a Theme Builder layout or
|
||||
* template additionally needs Divi's role option theme_builder (audit L6 – as Divi's own
|
||||
* et_pb_is_allowed check for TB layouts, builder/functions.php).
|
||||
* Whether $row may be named to the caller: read_post; a Theme Builder post only while the Theme Builder is
|
||||
* visible to the caller (review I-2 B: otherwise it reads like a missing ID elsewhere, P12).
|
||||
*
|
||||
* @param array{id: int, status: string, type: string} $row
|
||||
*/
|
||||
private function readable(array $row): bool
|
||||
{
|
||||
if (self::isThemeBuilder($row) && ! $this->themeBuilder()['visible']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return current_user_can('read_post', $row['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruling P8a: whether the caller may edit_post the live post $row; a Theme Builder post additionally needs the
|
||||
* Theme Builder rights (ruling Q17 M1 – as every Theme Builder gate, ThemeBuilderAccess::canUse(); audit L6).
|
||||
*
|
||||
* @param array{id: int, status: string, type: string} $row
|
||||
*/
|
||||
private function editable(array $row): bool
|
||||
{
|
||||
if (in_array($row['type'], self::THEME_BUILDER_TYPES, true) && ! $this->roles->allows('theme_builder')) {
|
||||
if (self::isThemeBuilder($row) && ! $this->themeBuilder()['usable']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return current_user_can('edit_post', $row['id']);
|
||||
}
|
||||
|
||||
/** @param array{id: int, status: string, type: string} $row */
|
||||
private static function isThemeBuilder(array $row): bool
|
||||
{
|
||||
return in_array($row['type'], DiviThemeBuilder::postTypes(), true);
|
||||
}
|
||||
|
||||
/** @return array{visible: bool, usable: bool} */
|
||||
private function themeBuilder(): array
|
||||
{
|
||||
if ($this->themeBuilderState !== null) {
|
||||
return $this->themeBuilderState;
|
||||
}
|
||||
|
||||
if ($this->themeBuilder !== null) {
|
||||
$state = ($this->themeBuilder)();
|
||||
} else {
|
||||
$open = $this->roles->allows('theme_builder')
|
||||
&& current_user_can('edit_others_posts')
|
||||
&& current_user_can('edit_theme_options');
|
||||
$state = ['visible' => $open, 'usable' => $open];
|
||||
}
|
||||
|
||||
return $this->themeBuilderState = [
|
||||
'visible' => (bool) ($state['visible'] ?? false),
|
||||
'usable' => (bool) ($state['usable'] ?? false),
|
||||
];
|
||||
}
|
||||
|
||||
private function db(): \wpdb
|
||||
{
|
||||
return $this->db ?? $GLOBALS['wpdb'];
|
||||
|
||||
Reference in New Issue
Block a user