feat: Theme Builder conditions only from Divi's list, objects readable
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\ThemeBuilder;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Compose\PostIdParser;
|
||||
use WP_Post;
|
||||
use WP_Term;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Theme Builder conditions (_et_use_on/_et_exclude_from, F21, E7): only IDs from Divi's own list
|
||||
* (et_theme_builder_get_flat_template_settings_options()) – exactly, or for an entry ending in ":" that entry
|
||||
* plus one object segment, split the way Divi reads it back (et_theme_builder_load_template_setting_options():
|
||||
* the last segment is the object). The object must be readable by the user (ruling P10: PostIdParser): a post
|
||||
* of that type with read_post (not trashed, no auto-draft/attachment status), a term of that taxonomy with
|
||||
* assign_term, a user only with list_users (ruling Q10, stricter than Divi), an existing role. Refusals for
|
||||
* missing and unreadable objects read the same and never name an object's title (P12). Nothing is written here.
|
||||
*/
|
||||
final class TemplateConditions
|
||||
{
|
||||
public const MAX = 50;
|
||||
|
||||
private const SEPARATOR = ':';
|
||||
|
||||
private const DEAD = ['trash', 'auto-draft', 'inherit'];
|
||||
|
||||
/** @var array<string, array{label: string, type: ?string, value: ?string}>|null */
|
||||
private ?array $flat = null;
|
||||
|
||||
public function __construct(private DiviThemeBuilder $store)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a client's condition list for field $field: null = no conditions; errors are keyed "<field>"
|
||||
* (the list itself) or "<field>.<index>". Duplicates are dropped.
|
||||
*
|
||||
* @return array{ids: list<string>, errors: array<string, string>}
|
||||
*/
|
||||
public function check(mixed $conditions, string $field): array
|
||||
{
|
||||
if ($conditions === null) {
|
||||
return ['ids' => [], 'errors' => []];
|
||||
}
|
||||
|
||||
if (! is_array($conditions) || ! array_is_list($conditions)) {
|
||||
return ['ids' => [], 'errors' => [$field => 'Liste von Bedingungs-IDs erwartet']];
|
||||
}
|
||||
|
||||
if (count($conditions) > self::MAX) {
|
||||
return ['ids' => [], 'errors' => [$field => sprintf('höchstens %d Bedingungen', self::MAX)]];
|
||||
}
|
||||
|
||||
$ids = [];
|
||||
$errors = [];
|
||||
foreach ($conditions as $index => $condition) {
|
||||
$problem = is_string($condition) ? $this->problem($condition) : 'Bedingungs-ID als Text erwartet';
|
||||
if ($problem === null) {
|
||||
$ids[] = (string) $condition;
|
||||
} else {
|
||||
$errors[$field . '.' . $index] = $problem;
|
||||
}
|
||||
}
|
||||
|
||||
return ['ids' => array_values(array_unique($ids)), 'errors' => $errors];
|
||||
}
|
||||
|
||||
/** Divi's label, for an ID with object "<label>: <object>" (the object as stored, never its title). */
|
||||
public function label(string $id): string
|
||||
{
|
||||
$flat = $this->flat();
|
||||
if (isset($flat[$id]) && ! str_ends_with($id, self::SEPARATOR)) {
|
||||
return $flat[$id]['label'];
|
||||
}
|
||||
|
||||
$split = $this->split($id);
|
||||
|
||||
return $split === null ? $id : sprintf('%s: %s', $flat[$split[0]]['label'], $split[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Divi's conditions for lit/tb-list: "needs" names what an ID ending in ":" takes, e.g. "post_type:page".
|
||||
*
|
||||
* @return list<array{id: string, label: string, needs: ?string}>
|
||||
*/
|
||||
public function available(): array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($this->flat() as $id => $setting) {
|
||||
$id = (string) $id;
|
||||
$list[] = [
|
||||
'id' => $id,
|
||||
'label' => $setting['label'],
|
||||
'needs' => str_ends_with($id, self::SEPARATOR)
|
||||
? trim(($setting['type'] ?? '') . self::SEPARATOR . ($setting['value'] ?? ''), self::SEPARATOR)
|
||||
: null,
|
||||
];
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
private function problem(string $id): ?string
|
||||
{
|
||||
if (preg_match('/^[A-Za-z0-9_:\-]{1,200}$/D', $id) !== 1) {
|
||||
return 'ungültige Bedingungs-ID (erlaubt: Buchstaben, Ziffern, „_“, „-“ und „:“)';
|
||||
}
|
||||
|
||||
$flat = $this->flat();
|
||||
if (isset($flat[$id]) && ! str_ends_with($id, self::SEPARATOR)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isset($flat[$id])) {
|
||||
return sprintf('Bedingung %s braucht ein Objekt am Ende (siehe „needs“ in lit/tb-list)', $id);
|
||||
}
|
||||
|
||||
$split = $this->split($id);
|
||||
if ($split === null) {
|
||||
return 'unbekannte Bedingung (verfügbare: lit/tb-list mit conditions: true)';
|
||||
}
|
||||
|
||||
return $this->objectProblem($flat[$split[0]], $split[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* [Divi's parent ID ending in ":", object] as Divi reads the stored ID back (last segment = object),
|
||||
* or null when that parent is not in Divi's list or the object is empty.
|
||||
*
|
||||
* @return array{string, string}|null
|
||||
*/
|
||||
private function split(string $id): ?array
|
||||
{
|
||||
$cut = strrpos($id, self::SEPARATOR);
|
||||
if ($cut === false || $cut === strlen($id) - 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parent = substr($id, 0, $cut + 1);
|
||||
|
||||
return isset($this->flat()[$parent]) ? [$parent, substr($id, $cut + 1)] : null;
|
||||
}
|
||||
|
||||
/** @param array{label: string, type: ?string, value: ?string} $setting */
|
||||
private function objectProblem(array $setting, string $object): ?string
|
||||
{
|
||||
$value = (string) $setting['value'];
|
||||
$id = PostIdParser::parse($object);
|
||||
|
||||
if ($setting['type'] === 'post_type') {
|
||||
return $this->postProblem($value, $id, $object);
|
||||
}
|
||||
if ($setting['type'] === 'taxonomy') {
|
||||
$valid = $id !== null && get_term($id, $value) instanceof WP_Term && current_user_can('assign_term', $id);
|
||||
|
||||
return $valid
|
||||
? null
|
||||
: sprintf('Begriff %s gibt es in „%s“ nicht oder er ist nicht zuweisbar', $object, $value);
|
||||
}
|
||||
if ($setting['type'] === 'user') {
|
||||
$valid = $id !== null && current_user_can('list_users') && get_userdata($id) !== false;
|
||||
|
||||
return $valid ? null : 'Autoren-Bedingungen nur mit dem Recht list_users und einer vorhandenen Benutzer-ID';
|
||||
}
|
||||
if ($setting['type'] === 'user_role') {
|
||||
return wp_roles()->is_role($object) ? null : sprintf('Rolle „%s“ gibt es nicht', $object);
|
||||
}
|
||||
|
||||
return 'diese Bedingung nur im Divi Theme Builder setzen';
|
||||
}
|
||||
|
||||
private function postProblem(string $postType, ?int $id, string $object): ?string
|
||||
{
|
||||
$post = $id === null ? null : get_post($id);
|
||||
$valid = $post instanceof WP_Post
|
||||
&& (string) $post->post_type === $postType
|
||||
&& ! in_array((string) $post->post_status, self::DEAD, true)
|
||||
&& current_user_can('read_post', $id);
|
||||
|
||||
return $valid
|
||||
? null
|
||||
: sprintf('Eintrag %s vom Typ „%s“ gibt es nicht oder er ist nicht lesbar', $object, $postType);
|
||||
}
|
||||
|
||||
/** @return array<string, array{label: string, type: ?string, value: ?string}> */
|
||||
private function flat(): array
|
||||
{
|
||||
return $this->flat ??= $this->store->flatConditions();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user