fix: link targets never reveal library entries by type (ruling Q14)
WpLinkTargets answers the library, internal "_" types and every type without a Divi post_link_url option like a missing ID; replace-text strips library IDs only from global subtrees. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -69,8 +69,9 @@ final class DiviTextSearch
|
||||
|
||||
/**
|
||||
* Every hit of $page, computed without changing it. Without $globalIds (ruling P13: no library
|
||||
* access) the library IDs a global instance stores are no text to find: an opaque node is
|
||||
* matched without globalModule/globalParent, as if they were not stored.
|
||||
* access) the library IDs a global instance stores are no text to find: a global node is
|
||||
* matched without globalModule/globalParent, as if they were not stored; every other opaque
|
||||
* node (e.g. undecodable attributes) is matched as stored, the same with or without access.
|
||||
*
|
||||
* @return array{count: int, samples: list<array<string, string>>,
|
||||
* blocks: list<array{Block, array<string, mixed>}>, skipped_nodes: list<array{id: string, reason: string}>}
|
||||
@@ -89,7 +90,9 @@ final class DiviTextSearch
|
||||
foreach ($page->index->entries() as $entry) {
|
||||
$block = $entry['block'];
|
||||
if ($block->isDivi() && $block->isOpaque()) {
|
||||
$markup = $globalIds ? Serializer::block($block) : self::withoutLibraryIds($block);
|
||||
$markup = $globalIds || ! $block->isGlobal()
|
||||
? Serializer::block($block)
|
||||
: self::withoutLibraryIds($block);
|
||||
if ($replacer->mentions($markup)) {
|
||||
$skipped[] = ['id' => (string) $entry['id'], 'reason' => self::opaqueReason($block)];
|
||||
}
|
||||
@@ -120,8 +123,9 @@ final class DiviTextSearch
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruling P13: $block's markup with every globalModule/globalParent left out, at every depth –
|
||||
* only for matching, never saved.
|
||||
* Ruling P13: the markup of the global node $block (decodable attributes, as isGlobal() needs)
|
||||
* with every globalModule/globalParent left out, at every depth – only for matching, never
|
||||
* saved. Openers are rebuilt from the decoded attributes, so Divi's raw spelling is not kept.
|
||||
*/
|
||||
private static function withoutLibraryIds(Block $block): string
|
||||
{
|
||||
|
||||
@@ -4,32 +4,62 @@ declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Pages;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Integrations\Divi\Compose\LinkTargets;
|
||||
use WP_Post;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* {{link:<ID>}} and post_link_url_* targets from WordPress (ruling P10): the entry exists, lives
|
||||
* (no trash, auto-draft or revision), is no attachment (whatever its status) and the current user
|
||||
* may read it (read_post, like select_post settings, S37). Missing and unreadable entries give the
|
||||
* same null (no existence oracle).
|
||||
* {{link:<ID>}} and post_link_url_* targets from WordPress (rulings P10, Q14): the entry exists,
|
||||
* lives (no trash, auto-draft or revision), is no attachment (whatever its status), its type has a
|
||||
* Divi link option post_link_url_<type> (never the library et_pb_layout or an internal "_" type) and
|
||||
* the current user may read it (read_post, like select_post settings, S37). Missing, unreadable and
|
||||
* unlinkable entries give the same null (no existence or type oracle).
|
||||
*/
|
||||
final class WpLinkTargets implements LinkTargets
|
||||
{
|
||||
private const DEAD = ['trash', 'auto-draft', 'inherit'];
|
||||
|
||||
/** Never link targets, whatever Divi lists: attachments and the Divi Library (ruling Q14). */
|
||||
private const NEVER = ['attachment', 'et_pb_layout'];
|
||||
|
||||
/** @var Closure(string): bool */
|
||||
private Closure $linkable;
|
||||
|
||||
/**
|
||||
* @param (Closure(string): bool)|null $linkable Whether Divi offers post_link_url_<type> (tests);
|
||||
* null = Divi's et_builder_get_public_post_types(), the list its link option uses.
|
||||
*/
|
||||
public function __construct(?Closure $linkable = null)
|
||||
{
|
||||
$this->linkable = $linkable ?? self::diviLinkable(...);
|
||||
}
|
||||
|
||||
public function postTypeOf(int $id): ?string
|
||||
{
|
||||
$post = get_post($id);
|
||||
if (
|
||||
! $post instanceof WP_Post
|
||||
|| in_array((string) $post->post_status, self::DEAD, true)
|
||||
|| $post->post_type === 'attachment'
|
||||
) {
|
||||
if (! $post instanceof WP_Post || in_array((string) $post->post_status, self::DEAD, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return current_user_can('read_post', $id) ? (string) $post->post_type : null;
|
||||
$type = (string) $post->post_type;
|
||||
if (in_array($type, self::NEVER, true) || str_starts_with($type, '_') || ! ($this->linkable)($type)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return current_user_can('read_post', $id) ? $type : null;
|
||||
}
|
||||
|
||||
/** Fail closed: without Divi's list no type has a link option. */
|
||||
private static function diviLinkable(string $type): bool
|
||||
{
|
||||
if (! function_exists('et_builder_get_public_post_types')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$types = et_builder_get_public_post_types();
|
||||
|
||||
return is_array($types) && array_key_exists($type, $types);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Pages;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Compose\LinkTargets;
|
||||
use LIT\MCP\Integrations\Divi\Compose\ValueSanitizer;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
@@ -19,6 +20,11 @@ defined('ABSPATH') || exit;
|
||||
*/
|
||||
final class WpValueSanitizer implements ValueSanitizer
|
||||
{
|
||||
/** @param LinkTargets $links The {{link:}} targets post_link_url_* follow too (ruling P10). */
|
||||
public function __construct(private LinkTargets $links = new WpLinkTargets())
|
||||
{
|
||||
}
|
||||
|
||||
public function text(string $value): string
|
||||
{
|
||||
return sanitize_text_field($value);
|
||||
@@ -51,6 +57,6 @@ final class WpValueSanitizer implements ValueSanitizer
|
||||
|
||||
public function linkTargetType(int $id): ?string
|
||||
{
|
||||
return (new WpLinkTargets())->postTypeOf($id);
|
||||
return $this->links->postTypeOf($id);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user