feat: Divi adapters for roles and library with a live compatibility check
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Library;
|
||||
|
||||
use WP_Post;
|
||||
use WP_Term;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Adapter for the Divi Library (E4, E15): post type et_pb_layout, the taxonomies scope,
|
||||
* layout_type, module_width, layout_category, layout_tag and the meta Divi's et_pb_submit_layout()
|
||||
* writes (F1, F2, fixtures stage3). "Global" is Divi's own definition
|
||||
* GlobalLayout::is_global_layout_template() (F5) with the scope term as fallback when the class is
|
||||
* gone. find() filters by the scope term only, so an entry Divi counts as global through its D4
|
||||
* meta fallback can show up under scope "normal" (preflight M-12, accepted). Library entries are
|
||||
* always "publish" (F2). Not final so abilities can be tested with a mock.
|
||||
*/
|
||||
class DiviLibraryStore
|
||||
{
|
||||
public const POST_TYPE = 'et_pb_layout';
|
||||
|
||||
public const GLOBAL_LAYOUT = 'ET\Builder\Packages\GlobalLayout\GlobalLayout';
|
||||
|
||||
public const LAYOUT_TYPES = ['layout', 'section', 'row', 'module'];
|
||||
|
||||
/** Taxonomies every new entry needs (et_pb_submit_layout). */
|
||||
public const TAXONOMIES = ['scope', 'layout_type', 'module_width'];
|
||||
|
||||
public function isGlobal(int $id): bool
|
||||
{
|
||||
if (method_exists(self::GLOBAL_LAYOUT, 'is_global_layout_template')) {
|
||||
return (bool) call_user_func([self::GLOBAL_LAYOUT, 'is_global_layout_template'], $id);
|
||||
}
|
||||
|
||||
return in_array('global', $this->termSlugs($id, 'scope'), true);
|
||||
}
|
||||
|
||||
/** "layout", "section", "row" or "module"; null when the entry has no known type. */
|
||||
public function layoutType(int $id): ?string
|
||||
{
|
||||
foreach ($this->termSlugs($id, 'layout_type') as $slug) {
|
||||
if (in_array($slug, self::LAYOUT_TYPES, true)) {
|
||||
return $slug;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
public function termSlugs(int $id, string $taxonomy): array
|
||||
{
|
||||
return array_map(static fn (WP_Term $term): string => (string) $term->slug, $this->terms($id, $taxonomy));
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
public function termNames(int $id, string $taxonomy): array
|
||||
{
|
||||
return array_map(static fn (WP_Term $term): string => (string) $term->name, $this->terms($id, $taxonomy));
|
||||
}
|
||||
|
||||
/**
|
||||
* Published entries, $limit + 1 of them (Cursor::page() tells whether there are more).
|
||||
*
|
||||
* @param array{layout_type?: string, scope?: string, search?: string, category?: string} $filters
|
||||
* scope: "global" or "normal"; category: name of a layout_category term.
|
||||
* @return list<WP_Post>
|
||||
*/
|
||||
public function find(array $filters, int $limit, int $offset): array
|
||||
{
|
||||
$taxQuery = [];
|
||||
if (in_array($filters['layout_type'] ?? '', self::LAYOUT_TYPES, true)) {
|
||||
$taxQuery[] = ['taxonomy' => 'layout_type', 'field' => 'slug', 'terms' => [$filters['layout_type']]];
|
||||
}
|
||||
if (($filters['scope'] ?? '') === 'global') {
|
||||
$taxQuery[] = ['taxonomy' => 'scope', 'field' => 'slug', 'terms' => ['global']];
|
||||
} elseif (($filters['scope'] ?? '') === 'normal') {
|
||||
$taxQuery[] = ['taxonomy' => 'scope', 'field' => 'slug', 'terms' => ['global'], 'operator' => 'NOT IN'];
|
||||
}
|
||||
if (($filters['category'] ?? '') !== '') {
|
||||
$taxQuery[] = ['taxonomy' => 'layout_category', 'field' => 'name', 'terms' => [$filters['category']]];
|
||||
}
|
||||
|
||||
$args = [
|
||||
'post_type' => self::POST_TYPE,
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => $limit + 1,
|
||||
'offset' => $offset,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
'no_found_rows' => true,
|
||||
'suppress_filters' => false,
|
||||
];
|
||||
if (($filters['search'] ?? '') !== '') {
|
||||
$args['s'] = $filters['search'];
|
||||
}
|
||||
if ($taxQuery !== []) {
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- the library is small.
|
||||
$args['tax_query'] = array_merge(['relation' => 'AND'], $taxQuery);
|
||||
}
|
||||
|
||||
return array_values(array_filter(get_posts($args), static fn (mixed $post): bool => $post instanceof WP_Post));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{id: int, title: string, layout_type: ?string, scope: string, module_type: ?string,
|
||||
* categories: list<string>, tags: list<string>, modified: string}
|
||||
*/
|
||||
public function describe(WP_Post $post): array
|
||||
{
|
||||
$id = (int) $post->ID;
|
||||
$moduleType = (string) get_post_meta($id, '_et_pb_module_type', true);
|
||||
|
||||
return [
|
||||
'id' => $id,
|
||||
'title' => (string) $post->post_title,
|
||||
'layout_type' => $this->layoutType($id),
|
||||
'scope' => $this->isGlobal($id) ? 'global' : 'normal',
|
||||
'module_type' => $moduleType === '' ? null : $moduleType,
|
||||
'categories' => $this->termNames($id, 'layout_category'),
|
||||
'tags' => $this->termNames($id, 'layout_tag'),
|
||||
'modified' => (string) $post->post_modified_gmt,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Library taxonomies not registered in this request (F2: Divi registers them in
|
||||
* ET_Builder_Library; fixture finding 6: not in every context).
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public function missingTaxonomies(): array
|
||||
{
|
||||
return array_values(array_filter(
|
||||
self::TAXONOMIES,
|
||||
static fn (string $taxonomy): bool => ! taxonomy_exists($taxonomy)
|
||||
));
|
||||
}
|
||||
|
||||
/** @return list<WP_Term> */
|
||||
private function terms(int $id, string $taxonomy): array
|
||||
{
|
||||
$terms = get_the_terms($id, $taxonomy);
|
||||
|
||||
return is_array($terms)
|
||||
? array_values(array_filter($terms, static fn (mixed $term): bool => $term instanceof WP_Term))
|
||||
: [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Library;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Adapter for Divi's role editor (E3, E15): et_pb_is_allowed() (Divi 5.2.1 builder/core.php:1128)
|
||||
* answers for the current user's role, "dangerous" options (theme_builder,
|
||||
* read_dynamic_content_custom_fields) default to off for every role but administrator. When
|
||||
* Divi no longer has the function, nothing is allowed – DiviCompatibility reports it.
|
||||
* Not final so tests can pass a check.
|
||||
*/
|
||||
class DiviRoles
|
||||
{
|
||||
private const LABELS = [
|
||||
'divi_library' => 'Divi-Bibliothek',
|
||||
'save_library' => 'In der Bibliothek speichern',
|
||||
'add_library' => 'Aus der Bibliothek hinzufügen',
|
||||
'edit_global_library' => 'Globale Bibliothekselemente bearbeiten',
|
||||
'read_dynamic_content_custom_fields' => 'Dynamic Content: benutzerdefinierte Felder lesen',
|
||||
];
|
||||
|
||||
/** @param (Closure(string): bool)|null $check Test seam; null = Divi's et_pb_is_allowed(). */
|
||||
public function __construct(private ?Closure $check = null)
|
||||
{
|
||||
}
|
||||
|
||||
public function allows(string $capability): bool
|
||||
{
|
||||
if ($this->check !== null) {
|
||||
return ($this->check)($capability);
|
||||
}
|
||||
|
||||
return function_exists('et_pb_is_allowed') && (bool) et_pb_is_allowed($capability);
|
||||
}
|
||||
|
||||
/** Null when the role allows $capability, else a forbidden error naming the option. */
|
||||
public function refusal(string $capability): ?WP_Error
|
||||
{
|
||||
if ($this->allows($capability)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Errors::forbidden(
|
||||
sprintf('Die Divi-Rolle dieses Kontos erlaubt „%s“ nicht.', self::LABELS[$capability] ?? $capability),
|
||||
'Einen Administrator bitten, das Recht unter Divi → Rollen-Editor freizugeben.'
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user