refactor: Kern ohne Divi – Ports BuilderDetector, MediaUsageSource, EnvironmentComponent und ausgeschlossene Post-Typen im ContentSchema; Divi-Implementierungen und Verdrahtung über DiviIntegration/Plugin; CoreIndependenceTest
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
abeb4b0eed
commit
06148fb8c1
@@ -12,7 +12,8 @@ defined('ABSPATH') || exit;
|
||||
|
||||
final class CoreIntegration implements Integration
|
||||
{
|
||||
public function __construct(private string $guidesDirectory)
|
||||
/** @param list<EnvironmentComponent> $components What lit-site-status reports besides Pods and Events Calendar. */
|
||||
public function __construct(private string $guidesDirectory, private array $components = [])
|
||||
{
|
||||
}
|
||||
|
||||
@@ -35,7 +36,7 @@ final class CoreIntegration implements Integration
|
||||
{
|
||||
return [
|
||||
new GuideAbility(new GuideRepository($this->guidesDirectory)),
|
||||
new SiteStatusAbility(new EnvironmentProbe(), new Access()),
|
||||
new SiteStatusAbility(new EnvironmentProbe($this->components), new Access()),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Core;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Port: one theme or plugin LIT MCP integrates with, as lit-site-status reports it. EnvironmentProbe lists the
|
||||
* components it is given; the integration that owns the component (Divi) implements it, Plugin wires it in.
|
||||
*/
|
||||
interface EnvironmentComponent
|
||||
{
|
||||
/**
|
||||
* @return array{id: string, active: bool, version: ?string, problems: list<string>, release: ?array<string, mixed>}
|
||||
* problems: what the component misses or has changed; release: whether LIT MCP may write with it
|
||||
* (null for components without such a lock).
|
||||
*/
|
||||
public function describe(): array;
|
||||
}
|
||||
@@ -4,19 +4,16 @@ declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Core;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\DiviCompatibility;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviEnvironment;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Detects the themes/plugins LIT MCP integrates with; for Divi also the internals that are missing
|
||||
* (DiviCompatibility).
|
||||
* Detects the themes/plugins LIT MCP integrates with: the components it is given (Divi, wired in Plugin) first, then
|
||||
* Pods and The Events Calendar.
|
||||
*/
|
||||
class EnvironmentProbe
|
||||
{
|
||||
public function __construct(private ?DiviCompatibility $divi = null)
|
||||
/** @param list<EnvironmentComponent> $components Components of integrations, reported in this order. */
|
||||
public function __construct(private array $components = [])
|
||||
{
|
||||
}
|
||||
|
||||
@@ -26,19 +23,14 @@ class EnvironmentProbe
|
||||
*/
|
||||
public function components(): array
|
||||
{
|
||||
$diviVersion = DiviEnvironment::version();
|
||||
$isDivi = $diviVersion !== null;
|
||||
|
||||
$tecActive = class_exists('Tribe__Events__Main');
|
||||
$described = array_map(
|
||||
static fn (EnvironmentComponent $component): array => $component->describe(),
|
||||
$this->components
|
||||
);
|
||||
|
||||
return [
|
||||
[
|
||||
'id' => 'divi',
|
||||
'active' => $isDivi,
|
||||
'version' => $diviVersion,
|
||||
'problems' => $isDivi ? ($this->divi ?? new DiviCompatibility())->problems() : [],
|
||||
'release' => $isDivi ? ReleaseGate::status()->toArray() : null,
|
||||
],
|
||||
...$described,
|
||||
[
|
||||
'id' => 'pods',
|
||||
'active' => defined('PODS_VERSION'),
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviPages;
|
||||
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||||
use LIT\MCP\Integrations\WordPress\BuilderDetector;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** The BuilderDetector for Divi 5: tools exist while Divi 5 is loaded, a layout carries Divi's builder flag. */
|
||||
final class DiviBuilderDetector implements BuilderDetector
|
||||
{
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return DiviModules::isAvailable();
|
||||
}
|
||||
|
||||
public function usesBuilder(int $postId): bool
|
||||
{
|
||||
return DiviPages::usesBuilder($postId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi;
|
||||
|
||||
use LIT\MCP\Integrations\Core\EnvironmentComponent;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviEnvironment;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Divi in lit-site-status: version, the internals that are missing (DiviCompatibility) and the release status
|
||||
* (spec §5.5); inactive without problems and release when Divi is not the template.
|
||||
*/
|
||||
final class DiviEnvironmentComponent implements EnvironmentComponent
|
||||
{
|
||||
public function __construct(private ?DiviCompatibility $compatibility = null)
|
||||
{
|
||||
}
|
||||
|
||||
public function describe(): array
|
||||
{
|
||||
$version = DiviEnvironment::version();
|
||||
$active = $version !== null;
|
||||
|
||||
return [
|
||||
'id' => 'divi',
|
||||
'active' => $active,
|
||||
'version' => $version,
|
||||
'problems' => $active ? ($this->compatibility ?? new DiviCompatibility())->problems() : [],
|
||||
'release' => $active ? ReleaseGate::status()->toArray() : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,10 @@ use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\ThemeBuilderAccess;
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\ThemeBuilderEdits;
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\ThemeBuilderIntegration;
|
||||
use LIT\MCP\Integrations\Core\EnvironmentComponent;
|
||||
use LIT\MCP\Integrations\Integration;
|
||||
use LIT\MCP\Integrations\Media\MediaUsageSource;
|
||||
use LIT\MCP\Integrations\WordPress\BuilderDetector;
|
||||
use LIT\MCP\Integrations\WordPress\Content\ContentComposer;
|
||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||
|
||||
@@ -131,6 +134,45 @@ final class DiviIntegration implements Integration
|
||||
return DiviModules::isAvailable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Divi's own post types (library, Theme Builder, canvases, speculation), which the content tools leave alone:
|
||||
* ContentSchema's excluded post types. Static because the schema is built before this integration (Plugin).
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function internalPostTypes(): array
|
||||
{
|
||||
return DiviModules::INTERNAL_POST_TYPES;
|
||||
}
|
||||
|
||||
/** Whether Divi's builder is switched on for $postType: ContentSchema::diviEnabled() (static, like above). */
|
||||
public static function builderEnabledFor(string $postType): bool
|
||||
{
|
||||
return DiviModules::builderEnabledFor($postType);
|
||||
}
|
||||
|
||||
/** Port for the content tools: Divi's builder flag and availability (PostPresenter). */
|
||||
public function builderDetector(): BuilderDetector
|
||||
{
|
||||
return new DiviBuilderDetector();
|
||||
}
|
||||
|
||||
/**
|
||||
* Port for the media tools: places outside the library where Divi stores an attachment (its logo).
|
||||
*
|
||||
* @return list<MediaUsageSource>
|
||||
*/
|
||||
public function mediaUsageSources(): array
|
||||
{
|
||||
return [new DiviLogoUsage()];
|
||||
}
|
||||
|
||||
/** Port for lit-site-status: Divi's version, missing internals and release status. */
|
||||
public function environmentComponent(): EnvironmentComponent
|
||||
{
|
||||
return new DiviEnvironmentComponent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Divi short form for lit/content-create; available() reports whether Divi 5 is loaded,
|
||||
* writable() asks $writable (the Tools-tab gate, security M2).
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
||||
use LIT\MCP\Integrations\Media\AttachmentFile;
|
||||
use LIT\MCP\Integrations\Media\ContentMediaReferences;
|
||||
use LIT\MCP\Integrations\Media\MediaUsageFinder;
|
||||
use LIT\MCP\Integrations\Media\MediaUsageSource;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* The logo in Divi's theme options as a usage of an attachment: "possible", because only the file URL is stored
|
||||
* (it may belong to another copy of the file); none without a file or a logo.
|
||||
*/
|
||||
final class DiviLogoUsage implements MediaUsageSource
|
||||
{
|
||||
public function usagesOf(int $attachmentId): array
|
||||
{
|
||||
$stem = AttachmentFile::stem($attachmentId);
|
||||
$logo = DesignSystemReader::diviLogo();
|
||||
if ($stem === '' || $logo === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (ContentMediaReferences::matches(ContentMediaReferences::fileRegex($stem), $logo) === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [MediaUsageFinder::siteUsage('Divi-Logo', 'option', 'divi_logo', MediaUsageFinder::POSSIBLE)];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Media;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** The stored file of an attachment, as far as usage checks need it. */
|
||||
final class AttachmentFile
|
||||
{
|
||||
/**
|
||||
* Upload-relative path without extension and without WordPress' suffixes for big, rotated or edited images:
|
||||
* 2026/09/foto-scaled.jpg → 2026/09/foto. '' when the attachment has no file.
|
||||
*/
|
||||
public static function stem(int $attachmentId): string
|
||||
{
|
||||
$file = ltrim((string) get_post_meta($attachmentId, '_wp_attached_file', true), '/');
|
||||
$stem = (string) preg_replace('/\.[^.\/]+$/', '', $file);
|
||||
|
||||
return (string) preg_replace('/(?:-scaled|-rotated|-e\d{10,})+$/', '', $stem);
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,13 @@ defined('ABSPATH') || exit;
|
||||
*/
|
||||
final class MediaIntegration implements Integration
|
||||
{
|
||||
public function __construct(private ContentSchema $schema, private ChangeLog $log, private Limits $limits)
|
||||
{
|
||||
/** @param list<MediaUsageSource> $usageSources Further places an attachment can be used (e.g. the Divi logo). */
|
||||
public function __construct(
|
||||
private ContentSchema $schema,
|
||||
private ChangeLog $log,
|
||||
private Limits $limits,
|
||||
private array $usageSources = []
|
||||
) {
|
||||
}
|
||||
|
||||
public function id(): string
|
||||
@@ -39,7 +44,11 @@ final class MediaIntegration implements Integration
|
||||
public function abilities(): array
|
||||
{
|
||||
$presenter = new MediaPresenter();
|
||||
$finder = new MediaUsageFinder(fn (): array => $this->schema->mediaFieldKeys());
|
||||
$finder = new MediaUsageFinder(
|
||||
fn (): array => $this->schema->mediaFieldKeys(),
|
||||
null,
|
||||
$this->usageSources
|
||||
);
|
||||
$importer = new MediaImporter($this->limits);
|
||||
$uploads = new MediaUploads($presenter, $this->log, $this->schema);
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Integrations\Media;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
||||
use LIT\MCP\Integrations\WordPress\PostStatuses;
|
||||
use LIT\MCP\Log;
|
||||
|
||||
@@ -59,9 +58,14 @@ class MediaUsageFinder
|
||||
|
||||
/**
|
||||
* @param Closure(): list<string> $mediaMetaKeys Keys of file/image fields (ContentSchema::mediaFieldKeys()).
|
||||
* @param list<MediaUsageSource> $sources Further site-wide places (e.g. the Divi logo), asked after the
|
||||
* core options and theme mods.
|
||||
*/
|
||||
public function __construct(private Closure $mediaMetaKeys, private ?\wpdb $db = null)
|
||||
{
|
||||
public function __construct(
|
||||
private Closure $mediaMetaKeys,
|
||||
private ?\wpdb $db = null,
|
||||
private array $sources = []
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,7 +106,7 @@ class MediaUsageFinder
|
||||
private function collect(int $attachmentId, int $max, int $limit): array
|
||||
{
|
||||
$wpdb = $this->db ?? $GLOBALS['wpdb'];
|
||||
$stem = $this->fileStem($attachmentId);
|
||||
$stem = AttachmentFile::stem($attachmentId);
|
||||
$fileRegex = $stem === '' ? null : ContentMediaReferences::fileRegex($stem);
|
||||
|
||||
$found = [];
|
||||
@@ -257,14 +261,10 @@ class MediaUsageFinder
|
||||
);
|
||||
}
|
||||
|
||||
$diviLogo = DesignSystemReader::diviLogo();
|
||||
if ($diviLogo !== '' && ContentMediaReferences::matches($fileRegex, $diviLogo) !== false) {
|
||||
$found['site:divi_logo'] = self::siteUsage(
|
||||
'Divi-Logo',
|
||||
'option',
|
||||
'divi_logo',
|
||||
self::POSSIBLE
|
||||
);
|
||||
foreach ($this->sources as $source) {
|
||||
foreach ($source->usagesOf($attachmentId) as $usage) {
|
||||
$found['site:' . $usage['where']] = $usage;
|
||||
}
|
||||
}
|
||||
|
||||
$this->addWidgetUsages($found, $attachmentId, $fileRegex);
|
||||
@@ -420,18 +420,6 @@ class MediaUsageFinder
|
||||
return $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload-relative path without extension and without WordPress' suffixes for big,
|
||||
* rotated or edited images: 2026/09/foto-scaled.jpg → 2026/09/foto.
|
||||
*/
|
||||
private function fileStem(int $attachmentId): string
|
||||
{
|
||||
$file = ltrim((string) get_post_meta($attachmentId, '_wp_attached_file', true), '/');
|
||||
$stem = (string) preg_replace('/\.[^.\/]+$/', '', $file);
|
||||
|
||||
return (string) preg_replace('/(?:-scaled|-rotated|-e\d{10,})+$/', '', $stem);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, mixed>> $found
|
||||
* @param list<object> $rows
|
||||
@@ -470,8 +458,12 @@ class MediaUsageFinder
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array{post_id: int, title: string, post_type: string, where: string, certainty: string} */
|
||||
private static function siteUsage(string $title, string $type, string $where, string $certainty): array
|
||||
/**
|
||||
* A site-wide usage (post_id 0): option, theme mod, widget or a MediaUsageSource's place.
|
||||
*
|
||||
* @return array{post_id: int, title: string, post_type: string, where: string, certainty: string}
|
||||
*/
|
||||
public static function siteUsage(string $title, string $type, string $where, string $certainty): array
|
||||
{
|
||||
return ['post_id' => 0, 'title' => $title, 'post_type' => $type, 'where' => $where, 'certainty' => $certainty];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Media;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Port: a site-wide place outside the media library's own knowledge where an attachment can be used (e.g. the
|
||||
* logo in Divi's theme options). MediaUsageFinder asks every source it is given after the core options and
|
||||
* theme mods; the integration that owns the place implements it, Plugin wires it in.
|
||||
*/
|
||||
interface MediaUsageSource
|
||||
{
|
||||
/**
|
||||
* @return list<array{post_id: int, title: string, post_type: string, where: string, certainty: string}>
|
||||
* Usages of attachment $attachmentId in the format of MediaUsageFinder::usages(); use
|
||||
* MediaUsageFinder::siteUsage() for site-wide ones (post_id 0). "where" names the place and is
|
||||
* unique per source (it keys the usage, so a place is reported once).
|
||||
*/
|
||||
public function usagesOf(int $attachmentId): array;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\WordPress;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Port: what the WordPress content tools need to know about a page builder (today only Divi) without knowing the
|
||||
* builder – implemented by the builder's integration, wired in Plugin.
|
||||
*/
|
||||
interface BuilderDetector
|
||||
{
|
||||
/** Whether the builder's tools exist on this site (the builder is loaded). */
|
||||
public function isAvailable(): bool;
|
||||
|
||||
/** Whether the builder's own flag marks $postId as a builder layout. */
|
||||
public function usesBuilder(int $postId): bool;
|
||||
}
|
||||
@@ -45,11 +45,16 @@ final class ContentUpdateAbility implements Ability
|
||||
/** Post columns a revision stores (and wp_restore_post_revision() restores). */
|
||||
private const REVISIONED = ['post_title', 'post_content', 'post_excerpt'];
|
||||
|
||||
private PostPresenter $presenter;
|
||||
|
||||
/** @param PostPresenter|null $presenter Knows the page builder (BuilderDetector); null = no builder. */
|
||||
public function __construct(
|
||||
private ContentSchema $schema,
|
||||
private PostWriter $writer,
|
||||
private ChangeLog $log
|
||||
private ChangeLog $log,
|
||||
?PostPresenter $presenter = null
|
||||
) {
|
||||
$this->presenter = $presenter ?? new PostPresenter($schema);
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
@@ -123,10 +128,10 @@ final class ContentUpdateAbility implements Ability
|
||||
return $unstable;
|
||||
}
|
||||
|
||||
if (array_key_exists('content', $input) && PostPresenter::usesDivi($post)) {
|
||||
if (array_key_exists('content', $input) && $this->presenter->usesDivi($post)) {
|
||||
return Errors::unsupported(
|
||||
'Divi-Inhalte können nicht als Rohtext überschrieben werden; nichts wurde gespeichert.',
|
||||
PostPresenter::diviNote() . ' Titel, Auszug, Felder und Begriffe hier ohne content ändern.'
|
||||
$this->presenter->diviNote() . ' Titel, Auszug, Felder und Begriffe hier ohne content ändern.'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ namespace LIT\MCP\Integrations\WordPress\Content;
|
||||
|
||||
use LIT\MCP\Abilities\JsonMap;
|
||||
use LIT\MCP\Abilities\Rev;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviPages;
|
||||
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||||
use LIT\MCP\Integrations\WordPress\BuilderDetector;
|
||||
use LIT\MCP\Integrations\WordPress\NoBuilderDetector;
|
||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
|
||||
use WP_Post;
|
||||
@@ -18,12 +18,9 @@ defined('ABSPATH') || exit;
|
||||
* Output format of an entry for the content tools. Not final so abilities can be
|
||||
* unit-tested with a mock.
|
||||
*
|
||||
* Layering exception: diviNote()/diviHint() call DiviModules::isAvailable() (Divi
|
||||
* integration) to decide whether the Divi tools exist – the WordPress integration
|
||||
* otherwise never depends on Divi. Kept because the alternative (injecting
|
||||
* availability through every constructor that reaches PostPresenter) would spread the
|
||||
* same one-line check across content-get, content-update and revisions-list for no
|
||||
* behavioral gain; $toolsAvailable can still be passed in explicitly for tests.
|
||||
* The builder (Divi) is known only through the BuilderDetector port: usesDivi() asks it for the builder flag
|
||||
* of a post, diviNote()/diviHint() for whether the builder's tools exist; $toolsAvailable can still be passed in
|
||||
* explicitly. Without a detector no builder is assumed (NoBuilderDetector).
|
||||
*/
|
||||
class PostPresenter
|
||||
{
|
||||
@@ -36,23 +33,26 @@ class PostPresenter
|
||||
public const DIVI_INACTIVE_NOTE = 'Der Divi-5-Builder ist auf dieser Website nicht aktiv, daher gibt es keine '
|
||||
. 'Divi-Werkzeuge; Divi-Inhalte nur im Divi-Builder bearbeiten.';
|
||||
|
||||
/** $toolsAvailable defaults to whether the Divi integration is available (Divi 5 loaded). */
|
||||
public static function diviNote(?bool $toolsAvailable = null): string
|
||||
private BuilderDetector $builders;
|
||||
|
||||
public function __construct(private ContentSchema $schema, ?BuilderDetector $builders = null)
|
||||
{
|
||||
return ($toolsAvailable ?? DiviModules::isAvailable()) ? self::DIVI_NOTE : self::DIVI_INACTIVE_NOTE;
|
||||
$this->builders = $builders ?? new NoBuilderDetector();
|
||||
}
|
||||
|
||||
public static function diviHint(?bool $toolsAvailable = null): string
|
||||
/** $toolsAvailable defaults to whether the builder is available (Divi 5 loaded). */
|
||||
public function diviNote(?bool $toolsAvailable = null): string
|
||||
{
|
||||
return ($toolsAvailable ?? $this->builders->isAvailable()) ? self::DIVI_NOTE : self::DIVI_INACTIVE_NOTE;
|
||||
}
|
||||
|
||||
public function diviHint(?bool $toolsAvailable = null): string
|
||||
{
|
||||
return 'Dieser Inhalt ist mit Divi gebaut; content wird deshalb nicht ausgegeben. '
|
||||
. self::diviNote($toolsAvailable) . ' Titel, Auszug, Felder und Begriffe sind über lit/content-update '
|
||||
. $this->diviNote($toolsAvailable) . ' Titel, Auszug, Felder und Begriffe sind über lit/content-update '
|
||||
. '(direkt: lit-content-update) änderbar.';
|
||||
}
|
||||
|
||||
public function __construct(private ContentSchema $schema)
|
||||
{
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function summary(WP_Post $post): array
|
||||
{
|
||||
@@ -67,14 +67,14 @@ class PostPresenter
|
||||
'modified' => (string) $post->post_modified,
|
||||
'url' => (string) get_permalink($post),
|
||||
'edit_url' => (string) get_edit_post_link($post->ID, 'raw'),
|
||||
'divi' => self::usesDivi($post),
|
||||
'divi' => $this->usesDivi($post),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function full(WP_Post $post): array
|
||||
{
|
||||
$divi = self::usesDivi($post);
|
||||
$divi = $this->usesDivi($post);
|
||||
|
||||
return array_merge($this->summary($post), [
|
||||
'excerpt' => (string) $post->post_excerpt,
|
||||
@@ -90,13 +90,13 @@ class PostPresenter
|
||||
'fields' => JsonMap::of($this->fields($post)),
|
||||
'featured_image' => $this->featuredImage($post),
|
||||
'rev' => Rev::ofPost($post),
|
||||
'hints' => $divi ? [self::diviHint()] : [],
|
||||
'hints' => $divi ? [$this->diviHint()] : [],
|
||||
]);
|
||||
}
|
||||
|
||||
public static function usesDivi(WP_Post $post): bool
|
||||
public function usesDivi(WP_Post $post): bool
|
||||
{
|
||||
return DiviPages::usesBuilder((int) $post->ID)
|
||||
return $this->builders->usesBuilder((int) $post->ID)
|
||||
|| str_contains((string) $post->post_content, '<!-- wp:divi/');
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\WordPress;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** The BuilderDetector of a site without page-builder integration: no builder, no layouts. */
|
||||
final class NoBuilderDetector implements BuilderDetector
|
||||
{
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function usesBuilder(int $postId): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,12 @@ defined('ABSPATH') || exit;
|
||||
*/
|
||||
final class RevisionsListAbility implements Ability
|
||||
{
|
||||
public function __construct(private ContentSchema $schema)
|
||||
private PostPresenter $presenter;
|
||||
|
||||
/** @param PostPresenter|null $presenter Knows the page builder (BuilderDetector); null = no builder. */
|
||||
public function __construct(private ContentSchema $schema, ?PostPresenter $presenter = null)
|
||||
{
|
||||
$this->presenter = $presenter ?? new PostPresenter($schema);
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
@@ -127,7 +131,7 @@ final class RevisionsListAbility implements Ability
|
||||
'post_id' => (int) $post->ID,
|
||||
'items' => $items,
|
||||
'next_cursor' => $page['next_cursor'],
|
||||
'hints' => in_array(true, array_column($items, 'divi'), true) ? [PostPresenter::diviHint()] : [],
|
||||
'hints' => in_array(true, array_column($items, 'divi'), true) ? [$this->presenter->diviHint()] : [],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -146,7 +150,7 @@ final class RevisionsListAbility implements Ability
|
||||
'title' => (string) $revision->post_title,
|
||||
'autosave' => (bool) wp_is_post_autosave($revision),
|
||||
'excerpt' => mb_substr($text, 0, 160),
|
||||
'divi' => PostPresenter::usesDivi($revision),
|
||||
'divi' => $this->presenter->usesDivi($revision),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\WordPress\Schema;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||||
use LogicException;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
@@ -16,11 +15,10 @@ defined('ABSPATH') || exit;
|
||||
*/
|
||||
class ContentSchema
|
||||
{
|
||||
/** Post types handled elsewhere (media tools, Divi tools, WordPress internals). */
|
||||
/** WordPress' own post types and those of the media tools; integrations add theirs (the constructor). */
|
||||
public const EXCLUDED = [
|
||||
'attachment', 'nav_menu_item', 'revision', 'wp_block', 'wp_template', 'wp_template_part',
|
||||
'wp_navigation', 'wp_global_styles', 'wp_font_family', 'wp_font_face',
|
||||
...DiviModules::INTERNAL_POST_TYPES,
|
||||
];
|
||||
|
||||
private const COUNTED_STATUSES = ['publish', 'draft', 'pending', 'private', 'future'];
|
||||
@@ -31,19 +29,27 @@ class ContentSchema
|
||||
/** @var array<string, array<string, FieldDefinition>> */
|
||||
private array $fieldCache = [];
|
||||
|
||||
/** @var list<string> */
|
||||
private array $excluded;
|
||||
|
||||
/**
|
||||
* @param list<FieldSource> $sources Highest priority first.
|
||||
* @param list<FieldSource> $sources Highest priority first.
|
||||
* @param callable(string): bool|null $diviEnabled Whether the page builder is switched on for a post type;
|
||||
* null = never (Plugin passes Divi's check).
|
||||
* @param list<string> $excludedPostTypes Post types of integrations that the content tools leave
|
||||
* alone (Plugin passes Divi's library, Theme Builder, canvas and speculation types), added to EXCLUDED.
|
||||
*/
|
||||
public function __construct(private array $sources, ?callable $diviEnabled = null)
|
||||
public function __construct(private array $sources, ?callable $diviEnabled = null, array $excludedPostTypes = [])
|
||||
{
|
||||
$this->diviEnabled = $diviEnabled ?? DiviModules::builderEnabledFor(...);
|
||||
$this->diviEnabled = $diviEnabled ?? static fn (string $postType): bool => false;
|
||||
$this->excluded = array_values(array_unique([...self::EXCLUDED, ...$excludedPostTypes]));
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
public function postTypeNames(): array
|
||||
{
|
||||
$excluded = apply_filters('lit_mcp_excluded_post_types', self::EXCLUDED);
|
||||
$excluded = is_array($excluded) ? $excluded : self::EXCLUDED;
|
||||
$excluded = apply_filters('lit_mcp_excluded_post_types', $this->excluded);
|
||||
$excluded = is_array($excluded) ? $excluded : $this->excluded;
|
||||
$names = array_values(array_diff(
|
||||
array_map('strval', array_keys(get_post_types(['show_ui' => true], 'names'))),
|
||||
$excluded
|
||||
|
||||
@@ -53,12 +53,15 @@ final class WordPressIntegration implements Integration
|
||||
* @param ContentComposer|null $divi Divi short form for lit/content-create (DiviIntegration).
|
||||
* @param (Closure(): bool)|null $globalIdsVisible Ruling P13: whether lit-page-render-summary may name
|
||||
* global module IDs (the Divi integration's library access); null = always.
|
||||
* @param BuilderDetector|null $builders The page builder behind the Divi flag of a post (Divi's
|
||||
* BuilderDetector); null = no builder.
|
||||
*/
|
||||
public function __construct(
|
||||
private ContentSchema $schema,
|
||||
private ChangeLog $log,
|
||||
private ?ContentComposer $divi = null,
|
||||
private ?Closure $globalIdsVisible = null
|
||||
private ?Closure $globalIdsVisible = null,
|
||||
private ?BuilderDetector $builders = null
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -79,7 +82,7 @@ final class WordPressIntegration implements Integration
|
||||
|
||||
public function abilities(): array
|
||||
{
|
||||
$presenter = new PostPresenter($this->schema);
|
||||
$presenter = new PostPresenter($this->schema, $this->builders);
|
||||
$writer = new PostWriter($this->schema, new FieldValidator(), new TermAssigner());
|
||||
$cloner = new PostCloner();
|
||||
$whitelist = new SettingsWhitelist();
|
||||
@@ -89,14 +92,14 @@ final class WordPressIntegration implements Integration
|
||||
new ContentQueryAbility($this->schema, $presenter),
|
||||
new ContentGetAbility($this->schema, $presenter),
|
||||
new ContentCreateAbility($this->schema, $writer, $this->log, $this->divi),
|
||||
new ContentUpdateAbility($this->schema, $writer, $this->log),
|
||||
new ContentUpdateAbility($this->schema, $writer, $this->log, $presenter),
|
||||
new PostPublishAbility($this->schema, $this->log),
|
||||
new PostUnpublishAbility($this->schema, $this->log),
|
||||
new PostScheduleAbility($this->schema, $this->log),
|
||||
new PostTrashAbility($this->schema, $this->log),
|
||||
new PostDuplicateAbility($this->schema, $cloner, $this->log),
|
||||
new PostReplaceWithCopyAbility($this->schema, $cloner, $this->log),
|
||||
new RevisionsListAbility($this->schema),
|
||||
new RevisionsListAbility($this->schema, $presenter),
|
||||
new RevisionRestoreAbility($this->schema, $this->log),
|
||||
new MenuListAbility(),
|
||||
new MenuGetAbility(),
|
||||
|
||||
Reference in New Issue
Block a user