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
@@ -19,6 +19,9 @@ defined('ABSPATH') || exit;
|
|||||||
* form holds a delimiter WordPress' block parser accepts for divi/* blocks ("<!--", whitespace,
|
* form holds a delimiter WordPress' block parser accepts for divi/* blocks ("<!--", whitespace,
|
||||||
* optional "/", lower-case "wp:divi/" – Divi's BlockParser extends it) or a Divi 4 shortcode.
|
* optional "/", lower-case "wp:divi/" – Divi's BlockParser extends it) or a Divi 4 shortcode.
|
||||||
* Other block markup stays allowed and goes through WordPress' kses. Pure PHP.
|
* Other block markup stays allowed and goes through WordPress' kses. Pure PHP.
|
||||||
|
*
|
||||||
|
* Stays in Abilities although it knows Divi's markup: it only recognizes text patterns (no Divi class, no import,
|
||||||
|
* CoreIndependenceTest), and every writer of client text – Divi or not – needs the same check.
|
||||||
*/
|
*/
|
||||||
final class DiviMarkup
|
final class DiviMarkup
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ defined('ABSPATH') || exit;
|
|||||||
|
|
||||||
final class CoreIntegration implements Integration
|
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 [
|
return [
|
||||||
new GuideAbility(new GuideRepository($this->guidesDirectory)),
|
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;
|
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;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detects the themes/plugins LIT MCP integrates with; for Divi also the internals that are missing
|
* Detects the themes/plugins LIT MCP integrates with: the components it is given (Divi, wired in Plugin) first, then
|
||||||
* (DiviCompatibility).
|
* Pods and The Events Calendar.
|
||||||
*/
|
*/
|
||||||
class EnvironmentProbe
|
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
|
public function components(): array
|
||||||
{
|
{
|
||||||
$diviVersion = DiviEnvironment::version();
|
|
||||||
$isDivi = $diviVersion !== null;
|
|
||||||
|
|
||||||
$tecActive = class_exists('Tribe__Events__Main');
|
$tecActive = class_exists('Tribe__Events__Main');
|
||||||
|
$described = array_map(
|
||||||
|
static fn (EnvironmentComponent $component): array => $component->describe(),
|
||||||
|
$this->components
|
||||||
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
[
|
...$described,
|
||||||
'id' => 'divi',
|
|
||||||
'active' => $isDivi,
|
|
||||||
'version' => $diviVersion,
|
|
||||||
'problems' => $isDivi ? ($this->divi ?? new DiviCompatibility())->problems() : [],
|
|
||||||
'release' => $isDivi ? ReleaseGate::status()->toArray() : null,
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
'id' => 'pods',
|
'id' => 'pods',
|
||||||
'active' => defined('PODS_VERSION'),
|
'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\ThemeBuilderAccess;
|
||||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\ThemeBuilderEdits;
|
use LIT\MCP\Integrations\Divi\ThemeBuilder\ThemeBuilderEdits;
|
||||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\ThemeBuilderIntegration;
|
use LIT\MCP\Integrations\Divi\ThemeBuilder\ThemeBuilderIntegration;
|
||||||
|
use LIT\MCP\Integrations\Core\EnvironmentComponent;
|
||||||
use LIT\MCP\Integrations\Integration;
|
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\Content\ContentComposer;
|
||||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||||
|
|
||||||
@@ -131,6 +134,45 @@ final class DiviIntegration implements Integration
|
|||||||
return DiviModules::isAvailable();
|
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,
|
* Divi short form for lit/content-create; available() reports whether Divi 5 is loaded,
|
||||||
* writable() asks $writable (the Tools-tab gate, security M2).
|
* 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
|
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
|
public function id(): string
|
||||||
@@ -39,7 +44,11 @@ final class MediaIntegration implements Integration
|
|||||||
public function abilities(): array
|
public function abilities(): array
|
||||||
{
|
{
|
||||||
$presenter = new MediaPresenter();
|
$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);
|
$importer = new MediaImporter($this->limits);
|
||||||
$uploads = new MediaUploads($presenter, $this->log, $this->schema);
|
$uploads = new MediaUploads($presenter, $this->log, $this->schema);
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||||||
namespace LIT\MCP\Integrations\Media;
|
namespace LIT\MCP\Integrations\Media;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
|
||||||
use LIT\MCP\Integrations\WordPress\PostStatuses;
|
use LIT\MCP\Integrations\WordPress\PostStatuses;
|
||||||
use LIT\MCP\Log;
|
use LIT\MCP\Log;
|
||||||
|
|
||||||
@@ -59,9 +58,14 @@ class MediaUsageFinder
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Closure(): list<string> $mediaMetaKeys Keys of file/image fields (ContentSchema::mediaFieldKeys()).
|
* @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
|
private function collect(int $attachmentId, int $max, int $limit): array
|
||||||
{
|
{
|
||||||
$wpdb = $this->db ?? $GLOBALS['wpdb'];
|
$wpdb = $this->db ?? $GLOBALS['wpdb'];
|
||||||
$stem = $this->fileStem($attachmentId);
|
$stem = AttachmentFile::stem($attachmentId);
|
||||||
$fileRegex = $stem === '' ? null : ContentMediaReferences::fileRegex($stem);
|
$fileRegex = $stem === '' ? null : ContentMediaReferences::fileRegex($stem);
|
||||||
|
|
||||||
$found = [];
|
$found = [];
|
||||||
@@ -257,14 +261,10 @@ class MediaUsageFinder
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$diviLogo = DesignSystemReader::diviLogo();
|
foreach ($this->sources as $source) {
|
||||||
if ($diviLogo !== '' && ContentMediaReferences::matches($fileRegex, $diviLogo) !== false) {
|
foreach ($source->usagesOf($attachmentId) as $usage) {
|
||||||
$found['site:divi_logo'] = self::siteUsage(
|
$found['site:' . $usage['where']] = $usage;
|
||||||
'Divi-Logo',
|
}
|
||||||
'option',
|
|
||||||
'divi_logo',
|
|
||||||
self::POSSIBLE
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->addWidgetUsages($found, $attachmentId, $fileRegex);
|
$this->addWidgetUsages($found, $attachmentId, $fileRegex);
|
||||||
@@ -420,18 +420,6 @@ class MediaUsageFinder
|
|||||||
return $contents;
|
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 array<string, array<string, mixed>> $found
|
||||||
* @param list<object> $rows
|
* @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];
|
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). */
|
/** Post columns a revision stores (and wp_restore_post_revision() restores). */
|
||||||
private const REVISIONED = ['post_title', 'post_content', 'post_excerpt'];
|
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(
|
public function __construct(
|
||||||
private ContentSchema $schema,
|
private ContentSchema $schema,
|
||||||
private PostWriter $writer,
|
private PostWriter $writer,
|
||||||
private ChangeLog $log
|
private ChangeLog $log,
|
||||||
|
?PostPresenter $presenter = null
|
||||||
) {
|
) {
|
||||||
|
$this->presenter = $presenter ?? new PostPresenter($schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function name(): string
|
public function name(): string
|
||||||
@@ -123,10 +128,10 @@ final class ContentUpdateAbility implements Ability
|
|||||||
return $unstable;
|
return $unstable;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (array_key_exists('content', $input) && PostPresenter::usesDivi($post)) {
|
if (array_key_exists('content', $input) && $this->presenter->usesDivi($post)) {
|
||||||
return Errors::unsupported(
|
return Errors::unsupported(
|
||||||
'Divi-Inhalte können nicht als Rohtext überschrieben werden; nichts wurde gespeichert.',
|
'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\JsonMap;
|
||||||
use LIT\MCP\Abilities\Rev;
|
use LIT\MCP\Abilities\Rev;
|
||||||
use LIT\MCP\Integrations\Divi\Pages\DiviPages;
|
use LIT\MCP\Integrations\WordPress\BuilderDetector;
|
||||||
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
use LIT\MCP\Integrations\WordPress\NoBuilderDetector;
|
||||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||||
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
|
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
|
||||||
use WP_Post;
|
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
|
* Output format of an entry for the content tools. Not final so abilities can be
|
||||||
* unit-tested with a mock.
|
* unit-tested with a mock.
|
||||||
*
|
*
|
||||||
* Layering exception: diviNote()/diviHint() call DiviModules::isAvailable() (Divi
|
* The builder (Divi) is known only through the BuilderDetector port: usesDivi() asks it for the builder flag
|
||||||
* integration) to decide whether the Divi tools exist – the WordPress integration
|
* of a post, diviNote()/diviHint() for whether the builder's tools exist; $toolsAvailable can still be passed in
|
||||||
* otherwise never depends on Divi. Kept because the alternative (injecting
|
* explicitly. Without a detector no builder is assumed (NoBuilderDetector).
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
class PostPresenter
|
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 '
|
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.';
|
. 'Divi-Werkzeuge; Divi-Inhalte nur im Divi-Builder bearbeiten.';
|
||||||
|
|
||||||
/** $toolsAvailable defaults to whether the Divi integration is available (Divi 5 loaded). */
|
private BuilderDetector $builders;
|
||||||
public static function diviNote(?bool $toolsAvailable = null): string
|
|
||||||
|
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. '
|
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.';
|
. '(direkt: lit-content-update) änderbar.';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __construct(private ContentSchema $schema)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array<string, mixed> */
|
/** @return array<string, mixed> */
|
||||||
public function summary(WP_Post $post): array
|
public function summary(WP_Post $post): array
|
||||||
{
|
{
|
||||||
@@ -67,14 +67,14 @@ class PostPresenter
|
|||||||
'modified' => (string) $post->post_modified,
|
'modified' => (string) $post->post_modified,
|
||||||
'url' => (string) get_permalink($post),
|
'url' => (string) get_permalink($post),
|
||||||
'edit_url' => (string) get_edit_post_link($post->ID, 'raw'),
|
'edit_url' => (string) get_edit_post_link($post->ID, 'raw'),
|
||||||
'divi' => self::usesDivi($post),
|
'divi' => $this->usesDivi($post),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<string, mixed> */
|
/** @return array<string, mixed> */
|
||||||
public function full(WP_Post $post): array
|
public function full(WP_Post $post): array
|
||||||
{
|
{
|
||||||
$divi = self::usesDivi($post);
|
$divi = $this->usesDivi($post);
|
||||||
|
|
||||||
return array_merge($this->summary($post), [
|
return array_merge($this->summary($post), [
|
||||||
'excerpt' => (string) $post->post_excerpt,
|
'excerpt' => (string) $post->post_excerpt,
|
||||||
@@ -90,13 +90,13 @@ class PostPresenter
|
|||||||
'fields' => JsonMap::of($this->fields($post)),
|
'fields' => JsonMap::of($this->fields($post)),
|
||||||
'featured_image' => $this->featuredImage($post),
|
'featured_image' => $this->featuredImage($post),
|
||||||
'rev' => Rev::ofPost($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/');
|
|| 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
|
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
|
public function name(): string
|
||||||
@@ -127,7 +131,7 @@ final class RevisionsListAbility implements Ability
|
|||||||
'post_id' => (int) $post->ID,
|
'post_id' => (int) $post->ID,
|
||||||
'items' => $items,
|
'items' => $items,
|
||||||
'next_cursor' => $page['next_cursor'],
|
'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,
|
'title' => (string) $revision->post_title,
|
||||||
'autosave' => (bool) wp_is_post_autosave($revision),
|
'autosave' => (bool) wp_is_post_autosave($revision),
|
||||||
'excerpt' => mb_substr($text, 0, 160),
|
'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;
|
namespace LIT\MCP\Integrations\WordPress\Schema;
|
||||||
|
|
||||||
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
|
||||||
use LogicException;
|
use LogicException;
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
@@ -16,11 +15,10 @@ defined('ABSPATH') || exit;
|
|||||||
*/
|
*/
|
||||||
class ContentSchema
|
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 = [
|
public const EXCLUDED = [
|
||||||
'attachment', 'nav_menu_item', 'revision', 'wp_block', 'wp_template', 'wp_template_part',
|
'attachment', 'nav_menu_item', 'revision', 'wp_block', 'wp_template', 'wp_template_part',
|
||||||
'wp_navigation', 'wp_global_styles', 'wp_font_family', 'wp_font_face',
|
'wp_navigation', 'wp_global_styles', 'wp_font_family', 'wp_font_face',
|
||||||
...DiviModules::INTERNAL_POST_TYPES,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
private const COUNTED_STATUSES = ['publish', 'draft', 'pending', 'private', 'future'];
|
private const COUNTED_STATUSES = ['publish', 'draft', 'pending', 'private', 'future'];
|
||||||
@@ -31,19 +29,27 @@ class ContentSchema
|
|||||||
/** @var array<string, array<string, FieldDefinition>> */
|
/** @var array<string, array<string, FieldDefinition>> */
|
||||||
private array $fieldCache = [];
|
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> */
|
/** @return list<string> */
|
||||||
public function postTypeNames(): array
|
public function postTypeNames(): array
|
||||||
{
|
{
|
||||||
$excluded = apply_filters('lit_mcp_excluded_post_types', self::EXCLUDED);
|
$excluded = apply_filters('lit_mcp_excluded_post_types', $this->excluded);
|
||||||
$excluded = is_array($excluded) ? $excluded : self::EXCLUDED;
|
$excluded = is_array($excluded) ? $excluded : $this->excluded;
|
||||||
$names = array_values(array_diff(
|
$names = array_values(array_diff(
|
||||||
array_map('strval', array_keys(get_post_types(['show_ui' => true], 'names'))),
|
array_map('strval', array_keys(get_post_types(['show_ui' => true], 'names'))),
|
||||||
$excluded
|
$excluded
|
||||||
|
|||||||
@@ -53,12 +53,15 @@ final class WordPressIntegration implements Integration
|
|||||||
* @param ContentComposer|null $divi Divi short form for lit/content-create (DiviIntegration).
|
* @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
|
* @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.
|
* 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(
|
public function __construct(
|
||||||
private ContentSchema $schema,
|
private ContentSchema $schema,
|
||||||
private ChangeLog $log,
|
private ChangeLog $log,
|
||||||
private ?ContentComposer $divi = null,
|
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
|
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());
|
$writer = new PostWriter($this->schema, new FieldValidator(), new TermAssigner());
|
||||||
$cloner = new PostCloner();
|
$cloner = new PostCloner();
|
||||||
$whitelist = new SettingsWhitelist();
|
$whitelist = new SettingsWhitelist();
|
||||||
@@ -89,14 +92,14 @@ final class WordPressIntegration implements Integration
|
|||||||
new ContentQueryAbility($this->schema, $presenter),
|
new ContentQueryAbility($this->schema, $presenter),
|
||||||
new ContentGetAbility($this->schema, $presenter),
|
new ContentGetAbility($this->schema, $presenter),
|
||||||
new ContentCreateAbility($this->schema, $writer, $this->log, $this->divi),
|
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 PostPublishAbility($this->schema, $this->log),
|
||||||
new PostUnpublishAbility($this->schema, $this->log),
|
new PostUnpublishAbility($this->schema, $this->log),
|
||||||
new PostScheduleAbility($this->schema, $this->log),
|
new PostScheduleAbility($this->schema, $this->log),
|
||||||
new PostTrashAbility($this->schema, $this->log),
|
new PostTrashAbility($this->schema, $this->log),
|
||||||
new PostDuplicateAbility($this->schema, $cloner, $this->log),
|
new PostDuplicateAbility($this->schema, $cloner, $this->log),
|
||||||
new PostReplaceWithCopyAbility($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 RevisionRestoreAbility($this->schema, $this->log),
|
||||||
new MenuListAbility(),
|
new MenuListAbility(),
|
||||||
new MenuGetAbility(),
|
new MenuGetAbility(),
|
||||||
|
|||||||
+10
-4
@@ -125,14 +125,15 @@ final class Plugin
|
|||||||
themeBuilderVisible: $themeBuilderVisible
|
themeBuilderVisible: $themeBuilderVisible
|
||||||
);
|
);
|
||||||
$registry = new IntegrationRegistry(
|
$registry = new IntegrationRegistry(
|
||||||
new CoreIntegration(self::pluginDir() . '/docs/guides'),
|
new CoreIntegration(self::pluginDir() . '/docs/guides', [$divi->environmentComponent()]),
|
||||||
new WordPressIntegration(
|
new WordPressIntegration(
|
||||||
$schema,
|
$schema,
|
||||||
$changeLog,
|
$changeLog,
|
||||||
$divi->contentComposer($diviWritable),
|
$divi->contentComposer($diviWritable),
|
||||||
$divi->libraryIdsVisible(...)
|
$divi->libraryIdsVisible(...),
|
||||||
|
$divi->builderDetector()
|
||||||
),
|
),
|
||||||
new MediaIntegration($schema, $changeLog, $limits),
|
new MediaIntegration($schema, $changeLog, $limits, $divi->mediaUsageSources()),
|
||||||
$divi,
|
$divi,
|
||||||
$divi->library(),
|
$divi->library(),
|
||||||
$divi->themeBuilder()
|
$divi->themeBuilder()
|
||||||
@@ -217,6 +218,11 @@ final class Plugin
|
|||||||
array_unshift($sources, new PodsFieldSource());
|
array_unshift($sources, new PodsFieldSource());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ContentSchema($sources);
|
// Divi's post types and builder check come in statically: the schema exists before the Divi integration.
|
||||||
|
return new ContentSchema(
|
||||||
|
$sources,
|
||||||
|
DiviIntegration::builderEnabledFor(...),
|
||||||
|
DiviIntegration::internalPostTypes()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Support;
|
||||||
|
|
||||||
|
use LIT\MCP\Integrations\WordPress\BuilderDetector;
|
||||||
|
|
||||||
|
/** A BuilderDetector for tests: fixed availability and a fixed set of builder layouts. */
|
||||||
|
final class FakeBuilderDetector implements BuilderDetector
|
||||||
|
{
|
||||||
|
/** @param list<int> $layouts IDs of the posts the builder flag marks. */
|
||||||
|
public function __construct(private bool $available = true, private array $layouts = [])
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isAvailable(): bool
|
||||||
|
{
|
||||||
|
return $this->available;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function usesBuilder(int $postId): bool
|
||||||
|
{
|
||||||
|
return in_array($postId, $this->layouts, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Architecture;
|
||||||
|
|
||||||
|
use FilesystemIterator;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use RecursiveDirectoryIterator;
|
||||||
|
use RecursiveIteratorIterator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The generic core knows no Divi class: nothing below the CORE_PATHS imports or names a class of
|
||||||
|
* LIT\MCP\Integrations\Divi. Divi plugs in through ports (AbilityGuard, ToolRefusal, BuilderDetector,
|
||||||
|
* MediaUsageSource, EnvironmentComponent, ContentSchema's excluded post types) that Plugin.php – the composition
|
||||||
|
* root, outside these paths – wires up. Divi's own names (et_* …) are DiviBoundaryTest's business, not this test's.
|
||||||
|
*/
|
||||||
|
final class CoreIndependenceTest extends TestCase
|
||||||
|
{
|
||||||
|
/** Directories below src/ that must not use Divi classes. */
|
||||||
|
private const CORE_PATHS = [
|
||||||
|
'Abilities',
|
||||||
|
'Integrations/Core',
|
||||||
|
'Integrations/WordPress',
|
||||||
|
'Integrations/Media',
|
||||||
|
'Admin',
|
||||||
|
'Guides',
|
||||||
|
'Server',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Files inside CORE_PATHS that may use Divi classes, each with its reason. */
|
||||||
|
private const EXCEPTIONS = [
|
||||||
|
'Admin/DiviTab.php' => 'The settings tab "Divi" is Divi UI: release state, admin check and release/withdraw.',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function testTheCoreUsesNoDiviClass(): void
|
||||||
|
{
|
||||||
|
$offenders = [];
|
||||||
|
foreach (self::coreFiles() as $relative => $path) {
|
||||||
|
if (isset(self::EXCEPTIONS[$relative])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$names = self::diviNames((string) file_get_contents($path));
|
||||||
|
if ($names !== []) {
|
||||||
|
$offenders[] = $relative . ': ' . implode(', ', $names);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self::assertSame([], $offenders, "Der Kern darf keine Divi-Klasse benutzen (Port über Plugin.php verdrahten):\n"
|
||||||
|
. implode("\n", $offenders));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEveryExceptionExistsAndStillNeedsIt(): void
|
||||||
|
{
|
||||||
|
$files = self::coreFiles();
|
||||||
|
foreach (array_keys(self::EXCEPTIONS) as $relative) {
|
||||||
|
self::assertArrayHasKey($relative, $files, $relative . ' gibt es nicht mehr – Ausnahme streichen');
|
||||||
|
self::assertNotSame(
|
||||||
|
[],
|
||||||
|
self::diviNames((string) file_get_contents($files[$relative])),
|
||||||
|
$relative . ' braucht die Ausnahme nicht mehr – streichen'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheScanFindsImportsQualifiedNamesGroupUsesAndClassStrings(): void
|
||||||
|
{
|
||||||
|
$source = <<<'PHP'
|
||||||
|
<?php
|
||||||
|
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||||
|
use LIT\MCP\Integrations\{Media\Finder, Divi\Pages\DiviPages};
|
||||||
|
$a = \LIT\MCP\Integrations\Divi\Schema\DiviModules::isAvailable();
|
||||||
|
$b = 'LIT\\MCP\\Integrations\\Divi\\Design\\DesignSystemReader';
|
||||||
|
$c = "LIT\\MCP\\Integrations\\Divi\\Library\\LibraryAccess";
|
||||||
|
PHP;
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'Divi\Pages\DiviPages',
|
||||||
|
'LIT\MCP\Integrations\Divi\Design\DesignSystemReader',
|
||||||
|
'LIT\MCP\Integrations\Divi\Library\LibraryAccess',
|
||||||
|
'LIT\MCP\Integrations\Divi\Release\ReleaseGate',
|
||||||
|
'LIT\MCP\Integrations\Divi\Schema\DiviModules',
|
||||||
|
],
|
||||||
|
self::diviNames($source)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheScanIgnoresCommentsOtherNamespacesAndPlainWords(): void
|
||||||
|
{
|
||||||
|
$source = <<<'PHP'
|
||||||
|
<?php
|
||||||
|
// use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||||
|
/** @see \LIT\MCP\Integrations\Divi\Pages\DiviPages */
|
||||||
|
use LIT\MCP\Integrations\Media\MediaUsageSource;
|
||||||
|
use LIT\MCP\Abilities\DiviMarkup;
|
||||||
|
$x = 'Divi';
|
||||||
|
$y = 'Ein Divi-Seitenaufbau';
|
||||||
|
PHP;
|
||||||
|
|
||||||
|
self::assertSame([], self::diviNames($source));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<string> Sorted, unique names of Divi classes the source uses. */
|
||||||
|
private static function diviNames(string $source): array
|
||||||
|
{
|
||||||
|
$names = [];
|
||||||
|
foreach (token_get_all($source) as $token) {
|
||||||
|
if (! is_array($token)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$text = match ($token[0]) {
|
||||||
|
T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED => ltrim($token[1], '\\'),
|
||||||
|
T_CONSTANT_ENCAPSED_STRING => ltrim(str_replace('\\\\', '\\', substr($token[1], 1, -1)), '\\'),
|
||||||
|
default => '',
|
||||||
|
};
|
||||||
|
if (preg_match('/^(?:LIT\\\\MCP\\\\Integrations\\\\)?Divi\\\\[A-Za-z0-9_\\\\]+$/', $text) === 1) {
|
||||||
|
$names[] = $text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$names = array_values(array_unique($names));
|
||||||
|
sort($names);
|
||||||
|
|
||||||
|
return $names;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, string> Path below src/ => absolute path of every PHP file in CORE_PATHS. */
|
||||||
|
private static function coreFiles(): array
|
||||||
|
{
|
||||||
|
$root = dirname(__DIR__, 3) . '/src/';
|
||||||
|
$files = [];
|
||||||
|
foreach (self::CORE_PATHS as $directory) {
|
||||||
|
$iterator = new RecursiveIteratorIterator(
|
||||||
|
new RecursiveDirectoryIterator($root . $directory, FilesystemIterator::SKIP_DOTS)
|
||||||
|
);
|
||||||
|
foreach ($iterator as $file) {
|
||||||
|
if ($file->getExtension() === 'php') {
|
||||||
|
$files[substr((string) $file->getPathname(), strlen($root))] = (string) $file->getPathname();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ksort($files);
|
||||||
|
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,11 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace LIT\MCP\Tests\Unit\Integrations\Core;
|
namespace LIT\MCP\Tests\Unit\Integrations\Core;
|
||||||
|
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
use LIT\MCP\Integrations\Core\CoreIntegration;
|
use LIT\MCP\Integrations\Core\CoreIntegration;
|
||||||
|
use LIT\MCP\Integrations\Core\EnvironmentComponent;
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
use WP_User;
|
||||||
|
|
||||||
final class CoreIntegrationTest extends TestCase
|
final class CoreIntegrationTest extends TestCase
|
||||||
{
|
{
|
||||||
@@ -20,4 +23,23 @@ final class CoreIntegrationTest extends TestCase
|
|||||||
array_map(static fn ($a) => $a->name(), $core->abilities())
|
array_map(static fn ($a) => $a->name(), $core->abilities())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testSiteStatusReportsTheComponentsOfTheIntegrations(): void
|
||||||
|
{
|
||||||
|
Functions\when('get_bloginfo')->justReturn('x');
|
||||||
|
Functions\when('home_url')->justReturn('https://kunde.de');
|
||||||
|
Functions\when('wp_get_current_user')->justReturn(new WP_User(3, ['editor']));
|
||||||
|
Functions\when('get_option')->justReturn(['editor']);
|
||||||
|
$component = new class implements EnvironmentComponent {
|
||||||
|
public function describe(): array
|
||||||
|
{
|
||||||
|
return ['id' => 'builder', 'active' => true, 'version' => '9', 'problems' => [], 'release' => null];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$core = new CoreIntegration(dirname(__DIR__, 3) . '/Fixtures/guides', [$component]);
|
||||||
|
|
||||||
|
$status = $core->abilities()[1]->execute([]);
|
||||||
|
|
||||||
|
self::assertSame(['builder', 'pods', 'the-events-calendar'], array_column($status['components'], 'id'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,44 +4,51 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace LIT\MCP\Tests\Unit\Integrations\Core;
|
namespace LIT\MCP\Tests\Unit\Integrations\Core;
|
||||||
|
|
||||||
use Brain\Monkey\Functions;
|
use LIT\MCP\Integrations\Core\EnvironmentComponent;
|
||||||
use LIT\MCP\Integrations\Core\EnvironmentProbe;
|
use LIT\MCP\Integrations\Core\EnvironmentProbe;
|
||||||
use LIT\MCP\Integrations\Divi\DiviCompatibility;
|
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
use Mockery;
|
|
||||||
|
|
||||||
final class EnvironmentProbeTest extends TestCase
|
final class EnvironmentProbeTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testDiviComponentCarriesTheCompatibilityFindings(): void
|
private function component(string $id, array $problems = []): EnvironmentComponent
|
||||||
{
|
{
|
||||||
$theme = Mockery::mock();
|
return new class ($id, $problems) implements EnvironmentComponent {
|
||||||
$theme->allows('get_template')->andReturn('Divi');
|
public function __construct(private string $id, private array $problems)
|
||||||
$theme->allows('parent')->andReturn(false);
|
{
|
||||||
$theme->allows('get')->with('Version')->andReturn('5.2.1');
|
}
|
||||||
Functions\when('wp_get_theme')->justReturn($theme);
|
|
||||||
|
|
||||||
$probe = new EnvironmentProbe(new DiviCompatibility(['GlobalLayout' => static fn (): bool => false]));
|
public function describe(): array
|
||||||
$components = array_column($probe->components(), null, 'id');
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'active' => true,
|
||||||
|
'version' => '1.2.3',
|
||||||
|
'problems' => $this->problems,
|
||||||
|
'release' => ['state' => 'released'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
self::assertSame(['GlobalLayout'], $components['divi']['problems']);
|
public function testGivenComponentsComeFirstInTheOrderGivenFollowedByPodsAndTheEventsCalendar(): void
|
||||||
|
{
|
||||||
|
$probe = new EnvironmentProbe([$this->component('builder', ['Etwas fehlt']), $this->component('shop')]);
|
||||||
|
|
||||||
|
$components = $probe->components();
|
||||||
|
|
||||||
|
self::assertSame(['builder', 'shop', 'pods', 'the-events-calendar'], array_column($components, 'id'));
|
||||||
|
self::assertSame(['Etwas fehlt'], $components[0]['problems']);
|
||||||
|
self::assertSame(['state' => 'released'], $components[1]['release']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWithoutComponentsOnlyPodsAndTheEventsCalendarAreReported(): void
|
||||||
|
{
|
||||||
|
$components = array_column((new EnvironmentProbe())->components(), null, 'id');
|
||||||
|
|
||||||
|
self::assertSame(['pods', 'the-events-calendar'], array_keys($components));
|
||||||
|
self::assertFalse($components['pods']['active'], 'no Pods in unit tests');
|
||||||
self::assertSame([], $components['pods']['problems']);
|
self::assertSame([], $components['pods']['problems']);
|
||||||
}
|
self::assertNull($components['pods']['release']);
|
||||||
|
self::assertNull($components['the-events-calendar']['version']);
|
||||||
public function testDiviComponentCarriesTheReleaseStatus(): void
|
|
||||||
{
|
|
||||||
$theme = Mockery::mock();
|
|
||||||
$theme->allows('get_template')->andReturn('Divi');
|
|
||||||
$theme->allows('parent')->andReturn(false);
|
|
||||||
$theme->allows('get')->with('Version')->andReturn('5.2.1');
|
|
||||||
Functions\when('wp_get_theme')->justReturn($theme);
|
|
||||||
|
|
||||||
$components = array_column(
|
|
||||||
(new EnvironmentProbe(new DiviCompatibility([])))->components(),
|
|
||||||
null,
|
|
||||||
'id'
|
|
||||||
);
|
|
||||||
|
|
||||||
self::assertSame('released', $components['divi']['release']['state'] ?? null);
|
|
||||||
self::assertNull($components['pods']['release'] ?? null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi;
|
||||||
|
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
use LIT\MCP\Integrations\Divi\DiviBuilderDetector;
|
||||||
|
use LIT\MCP\Integrations\WordPress\BuilderDetector;
|
||||||
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
|
||||||
|
final class DiviBuilderDetectorTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testIsABuilderDetectorThatFollowsDiviFiveBeingLoaded(): void
|
||||||
|
{
|
||||||
|
$detector = new DiviBuilderDetector();
|
||||||
|
|
||||||
|
self::assertInstanceOf(BuilderDetector::class, $detector);
|
||||||
|
self::assertFalse($detector->isAvailable(), 'no Divi classes in unit tests');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testALayoutIsMarkedByDivisBuilderFlag(): void
|
||||||
|
{
|
||||||
|
Functions\when('get_post_meta')->alias(
|
||||||
|
static fn (int $postId, string $key): string => $postId === 7 && $key === '_et_pb_use_builder' ? 'on' : ''
|
||||||
|
);
|
||||||
|
$detector = new DiviBuilderDetector();
|
||||||
|
|
||||||
|
self::assertTrue($detector->usesBuilder(7));
|
||||||
|
self::assertFalse($detector->usesBuilder(8));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi;
|
||||||
|
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
use LIT\MCP\Integrations\Core\EnvironmentComponent;
|
||||||
|
use LIT\MCP\Integrations\Divi\DiviCompatibility;
|
||||||
|
use LIT\MCP\Integrations\Divi\DiviEnvironmentComponent;
|
||||||
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
use Mockery;
|
||||||
|
|
||||||
|
final class DiviEnvironmentComponentTest extends TestCase
|
||||||
|
{
|
||||||
|
private function theme(string $template, string $version = '5.2.1'): void
|
||||||
|
{
|
||||||
|
$theme = Mockery::mock();
|
||||||
|
$theme->allows('get_template')->andReturn($template);
|
||||||
|
$theme->allows('parent')->andReturn(false);
|
||||||
|
$theme->allows('get')->with('Version')->andReturn($version);
|
||||||
|
Functions\when('wp_get_theme')->justReturn($theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsAnEnvironmentComponent(): void
|
||||||
|
{
|
||||||
|
self::assertInstanceOf(EnvironmentComponent::class, new DiviEnvironmentComponent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCarriesTheCompatibilityFindings(): void
|
||||||
|
{
|
||||||
|
$this->theme('Divi');
|
||||||
|
$component = new DiviEnvironmentComponent(
|
||||||
|
new DiviCompatibility(['GlobalLayout' => static fn (): bool => false])
|
||||||
|
);
|
||||||
|
|
||||||
|
$described = $component->describe();
|
||||||
|
|
||||||
|
self::assertSame('divi', $described['id']);
|
||||||
|
self::assertTrue($described['active']);
|
||||||
|
self::assertSame('5.2.1', $described['version']);
|
||||||
|
self::assertSame(['GlobalLayout'], $described['problems']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCarriesTheReleaseStatus(): void
|
||||||
|
{
|
||||||
|
$this->theme('Divi');
|
||||||
|
|
||||||
|
$described = (new DiviEnvironmentComponent(new DiviCompatibility([])))->describe();
|
||||||
|
|
||||||
|
self::assertSame([], $described['problems']);
|
||||||
|
self::assertSame('released', $described['release']['state'] ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsInactiveWithoutDiviAndHasNoProblemsOrRelease(): void
|
||||||
|
{
|
||||||
|
$this->theme('twentytwentysix', '1.0');
|
||||||
|
|
||||||
|
$described = (new DiviEnvironmentComponent(
|
||||||
|
new DiviCompatibility(['GlobalLayout' => static fn (): bool => false])
|
||||||
|
))->describe();
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
['id' => 'divi', 'active' => false, 'version' => null, 'problems' => [], 'release' => null],
|
||||||
|
$described
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,14 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi;
|
|||||||
|
|
||||||
use Brain\Monkey\Functions;
|
use Brain\Monkey\Functions;
|
||||||
use LIT\MCP\Abilities\Ability;
|
use LIT\MCP\Abilities\Ability;
|
||||||
|
use LIT\MCP\Integrations\Core\EnvironmentComponent;
|
||||||
|
use LIT\MCP\Integrations\Divi\DiviBuilderDetector;
|
||||||
|
use LIT\MCP\Integrations\Divi\DiviEnvironmentComponent;
|
||||||
use LIT\MCP\Integrations\Divi\DiviIntegration;
|
use LIT\MCP\Integrations\Divi\DiviIntegration;
|
||||||
|
use LIT\MCP\Integrations\Divi\DiviLogoUsage;
|
||||||
use LIT\MCP\Integrations\Divi\Pages\DiviContentComposer;
|
use LIT\MCP\Integrations\Divi\Pages\DiviContentComposer;
|
||||||
|
use LIT\MCP\Integrations\Media\MediaUsageSource;
|
||||||
|
use LIT\MCP\Integrations\WordPress\BuilderDetector;
|
||||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||||
use LIT\MCP\Tests\Support\FakeChangeLog;
|
use LIT\MCP\Tests\Support\FakeChangeLog;
|
||||||
use LIT\MCP\Tests\Support\LibraryFixtures;
|
use LIT\MCP\Tests\Support\LibraryFixtures;
|
||||||
@@ -58,6 +64,43 @@ final class DiviIntegrationTest extends TestCase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Spec §3: the ports the core uses (Plugin wires them) are Divi's own implementations. */
|
||||||
|
public function testProvidesThePortsTheCoreUses(): void
|
||||||
|
{
|
||||||
|
$integration = new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden());
|
||||||
|
|
||||||
|
self::assertInstanceOf(BuilderDetector::class, $integration->builderDetector());
|
||||||
|
self::assertInstanceOf(DiviBuilderDetector::class, $integration->builderDetector());
|
||||||
|
$sources = $integration->mediaUsageSources();
|
||||||
|
self::assertCount(1, $sources);
|
||||||
|
self::assertInstanceOf(MediaUsageSource::class, $sources[0]);
|
||||||
|
self::assertInstanceOf(DiviLogoUsage::class, $sources[0]);
|
||||||
|
self::assertInstanceOf(EnvironmentComponent::class, $integration->environmentComponent());
|
||||||
|
self::assertInstanceOf(DiviEnvironmentComponent::class, $integration->environmentComponent());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The content tools leave the same post types alone as before the schema stopped knowing Divi. */
|
||||||
|
public function testDiviPostTypesAddedToTheSchemaGiveTheFormerExcludedList(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'attachment', 'nav_menu_item', 'revision', 'wp_block', 'wp_template', 'wp_template_part',
|
||||||
|
'wp_navigation', 'wp_global_styles', 'wp_font_family', 'wp_font_face', 'et_pb_layout',
|
||||||
|
'et_theme_builder', 'et_template', 'et_header_layout', 'et_body_layout', 'et_footer_layout',
|
||||||
|
'et_pb_canvas', '_et_pb_speculation',
|
||||||
|
],
|
||||||
|
array_merge(ContentSchema::EXCLUDED, DiviIntegration::internalPostTypes())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheSchemaAsksDiviWhetherTheBuilderIsOnForAPostType(): void
|
||||||
|
{
|
||||||
|
Functions\when('et_builder_enabled_for_post_type')->alias(static fn (string $type): bool => $type === 'page');
|
||||||
|
|
||||||
|
self::assertTrue(DiviIntegration::builderEnabledFor('page'));
|
||||||
|
self::assertFalse(DiviIntegration::builderEnabledFor('post'));
|
||||||
|
}
|
||||||
|
|
||||||
public function testProvidesTheLibraryGroup(): void
|
public function testProvidesTheLibraryGroup(): void
|
||||||
{
|
{
|
||||||
Functions\when('__')->returnArg();
|
Functions\when('__')->returnArg();
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi;
|
||||||
|
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
use LIT\MCP\Integrations\Divi\DiviLogoUsage;
|
||||||
|
use LIT\MCP\Integrations\Media\MediaUsageSource;
|
||||||
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
|
||||||
|
final class DiviLogoUsageTest extends TestCase
|
||||||
|
{
|
||||||
|
private const POSSIBLE_LOGO = [
|
||||||
|
'post_id' => 0,
|
||||||
|
'title' => 'Divi-Logo',
|
||||||
|
'post_type' => 'option',
|
||||||
|
'where' => 'divi_logo',
|
||||||
|
'certainty' => 'möglicherweise',
|
||||||
|
];
|
||||||
|
|
||||||
|
private function stub(string $logo, string $file = '2026/09/foto-scaled.jpg'): void
|
||||||
|
{
|
||||||
|
Functions\when('get_post_meta')->justReturn($file);
|
||||||
|
Functions\when('get_option')->alias(
|
||||||
|
static fn (string $name): mixed => $name === 'et_divi' ? ['divi_logo' => $logo] : false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsAMediaUsageSource(): void
|
||||||
|
{
|
||||||
|
self::assertInstanceOf(MediaUsageSource::class, new DiviLogoUsage());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheLogoThatIsTheFileIsAPossibleUsage(): void
|
||||||
|
{
|
||||||
|
$this->stub('https://k.de/wp-content/uploads/2026/09/foto.jpg');
|
||||||
|
|
||||||
|
self::assertSame([self::POSSIBLE_LOGO], (new DiviLogoUsage())->usagesOf(33));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAGeneratedSizeOfTheFileCountsToo(): void
|
||||||
|
{
|
||||||
|
$this->stub('https://k.de/wp-content/uploads/2026/09/foto-300x200.jpg');
|
||||||
|
|
||||||
|
self::assertSame([self::POSSIBLE_LOGO], (new DiviLogoUsage())->usagesOf(33));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAnotherFileIsNoUsage(): void
|
||||||
|
{
|
||||||
|
$this->stub('https://k.de/wp-content/uploads/2026/09/anderes.jpg');
|
||||||
|
|
||||||
|
self::assertSame([], (new DiviLogoUsage())->usagesOf(33));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWithoutALogoOrWithoutAFileThereIsNoUsage(): void
|
||||||
|
{
|
||||||
|
$this->stub('');
|
||||||
|
self::assertSame([], (new DiviLogoUsage())->usagesOf(33));
|
||||||
|
|
||||||
|
$this->stub('https://k.de/wp-content/uploads/2026/09/foto.jpg', '');
|
||||||
|
self::assertSame([], (new DiviLogoUsage())->usagesOf(33));
|
||||||
|
|
||||||
|
Functions\when('get_option')->justReturn(false);
|
||||||
|
self::assertSame([], (new DiviLogoUsage())->usagesOf(33));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ namespace LIT\MCP\Tests\Unit\Integrations\Media;
|
|||||||
|
|
||||||
use Brain\Monkey\Functions;
|
use Brain\Monkey\Functions;
|
||||||
use LIT\MCP\Integrations\Media\MediaUsageFinder;
|
use LIT\MCP\Integrations\Media\MediaUsageFinder;
|
||||||
|
use LIT\MCP\Integrations\Media\MediaUsageSource;
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
use Mockery;
|
use Mockery;
|
||||||
use Mockery\MockInterface;
|
use Mockery\MockInterface;
|
||||||
@@ -21,6 +22,7 @@ final class MediaUsageFinderTest extends TestCase
|
|||||||
* @param list<object> $fields
|
* @param list<object> $fields
|
||||||
* @param array<string, mixed> $options Option name => value.
|
* @param array<string, mixed> $options Option name => value.
|
||||||
* @param array<string, mixed> $themeMods Theme mod name => value.
|
* @param array<string, mixed> $themeMods Theme mod name => value.
|
||||||
|
* @param list<MediaUsageSource> $sources
|
||||||
*/
|
*/
|
||||||
private function finder(
|
private function finder(
|
||||||
array $contents = [],
|
array $contents = [],
|
||||||
@@ -29,7 +31,8 @@ final class MediaUsageFinderTest extends TestCase
|
|||||||
array $options = [],
|
array $options = [],
|
||||||
array $themeMods = [],
|
array $themeMods = [],
|
||||||
array $mediaKeys = [],
|
array $mediaKeys = [],
|
||||||
string $file = '2026/09/foto-scaled.jpg'
|
string $file = '2026/09/foto-scaled.jpg',
|
||||||
|
array $sources = []
|
||||||
): MediaUsageFinder {
|
): MediaUsageFinder {
|
||||||
Functions\when('get_post_meta')->justReturn($file);
|
Functions\when('get_post_meta')->justReturn($file);
|
||||||
Functions\when('get_option')->alias(static fn (string $name) => $options[$name] ?? false);
|
Functions\when('get_option')->alias(static fn (string $name) => $options[$name] ?? false);
|
||||||
@@ -70,7 +73,7 @@ final class MediaUsageFinderTest extends TestCase
|
|||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
return new MediaUsageFinder(static fn (): array => $mediaKeys, $db);
|
return new MediaUsageFinder(static fn (): array => $mediaKeys, $db, $sources);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<int|string, string> post_id (or title for site usages) => certainty */
|
/** @return array<int|string, string> post_id (or title for site usages) => certainty */
|
||||||
@@ -269,23 +272,78 @@ final class MediaUsageFinderTest extends TestCase
|
|||||||
self::assertSame(['header_image', 'background_image', 'widget', 'widget', 'widget', 'widget'], array_column($usages, 'where'));
|
self::assertSame(['header_image', 'background_image', 'widget', 'widget', 'widget', 'widget'], array_column($usages, 'where'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testUnreadableEntriesHideTheirTitleAndDiviLogoIsPossible(): void
|
/**
|
||||||
|
* @param list<array<string, mixed>> $usages
|
||||||
|
* @param list<int> $asked Collects the attachment IDs the source is asked about.
|
||||||
|
*/
|
||||||
|
private static function source(array $usages, array &$asked = []): MediaUsageSource
|
||||||
{
|
{
|
||||||
|
return new class ($usages, $asked) implements MediaUsageSource {
|
||||||
|
public function __construct(private array $usages, private array &$asked)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function usagesOf(int $attachmentId): array
|
||||||
|
{
|
||||||
|
$this->asked[] = $attachmentId;
|
||||||
|
|
||||||
|
return $this->usages;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnreadableEntriesHideTheirTitleAndASourceUsageIsListedAfterTheCoreOnes(): void
|
||||||
|
{
|
||||||
|
$logo = MediaUsageFinder::siteUsage('Divi-Logo', 'option', 'divi_logo', MediaUsageFinder::POSSIBLE);
|
||||||
|
$asked = [];
|
||||||
|
|
||||||
$usages = $this->finder(
|
$usages = $this->finder(
|
||||||
[13 => '<img class="wp-image-33">'],
|
[13 => '<img class="wp-image-33">'],
|
||||||
[self::post(20, 'Geist', 'ghost')],
|
[self::post(20, 'Geist', 'ghost')],
|
||||||
[],
|
[],
|
||||||
['et_divi' => ['divi_logo' => 'https://k.de/wp-content/uploads/2026/09/foto.jpg']]
|
[],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
'2026/09/foto-scaled.jpg',
|
||||||
|
[self::source([$logo], $asked)]
|
||||||
)->usages(33);
|
)->usages(33);
|
||||||
|
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
[
|
[
|
||||||
['post_id' => 20, 'title' => '(nicht lesbar)', 'post_type' => 'ghost', 'where' => 'featured', 'certainty' => 'sicher'],
|
[
|
||||||
['post_id' => 0, 'title' => 'Divi-Logo', 'post_type' => 'option', 'where' => 'divi_logo', 'certainty' => 'möglicherweise'],
|
'post_id' => 20, 'title' => '(nicht lesbar)', 'post_type' => 'ghost',
|
||||||
['post_id' => 13, 'title' => '(nicht lesbar)', 'post_type' => 'page', 'where' => 'content', 'certainty' => 'sicher'],
|
'where' => 'featured', 'certainty' => 'sicher',
|
||||||
|
],
|
||||||
|
$logo,
|
||||||
|
[
|
||||||
|
'post_id' => 13, 'title' => '(nicht lesbar)', 'post_type' => 'page',
|
||||||
|
'where' => 'content', 'certainty' => 'sicher',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
$usages
|
$usages
|
||||||
);
|
);
|
||||||
|
self::assertSame([33], $asked);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testASourcePlaceIsReportedOnceAndSourcesAreSkippedOnceTheLimitIsReached(): void
|
||||||
|
{
|
||||||
|
$first = MediaUsageFinder::siteUsage('Ort', 'option', 'place', MediaUsageFinder::POSSIBLE);
|
||||||
|
$second = MediaUsageFinder::siteUsage('Ort neu', 'option', 'place', MediaUsageFinder::CERTAIN);
|
||||||
|
$asked = [];
|
||||||
|
|
||||||
|
$usages = $this->finder([], [], [], [], [], [], '2026/09/foto.jpg', [
|
||||||
|
self::source([$first]),
|
||||||
|
self::source([$second]),
|
||||||
|
])->usages(33);
|
||||||
|
|
||||||
|
self::assertSame([$second], $usages);
|
||||||
|
|
||||||
|
$finder = $this->finder([], [self::post(5, 'Team')], [], [], [], [], '2026/09/foto.jpg', [
|
||||||
|
self::source([$first], $asked),
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::assertTrue($finder->isUsed(33));
|
||||||
|
self::assertSame([], $asked, 'a usage was found before: the sources are not asked');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testIdReferenceWinsOverAFileMatchInTheSameContent(): void
|
public function testIdReferenceWinsOverAFileMatchInTheSameContent(): void
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use LIT\MCP\Integrations\WordPress\Content\ContentUpdateAbility;
|
|||||||
use LIT\MCP\Integrations\WordPress\Content\PostPresenter;
|
use LIT\MCP\Integrations\WordPress\Content\PostPresenter;
|
||||||
use LIT\MCP\Integrations\WordPress\Content\PostWriter;
|
use LIT\MCP\Integrations\WordPress\Content\PostWriter;
|
||||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||||
|
use LIT\MCP\Tests\Support\FakeBuilderDetector;
|
||||||
use LIT\MCP\Tests\Support\FakeChangeLog;
|
use LIT\MCP\Tests\Support\FakeChangeLog;
|
||||||
use LIT\MCP\Tests\Support\SchemaFixture;
|
use LIT\MCP\Tests\Support\SchemaFixture;
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
@@ -46,9 +47,12 @@ final class ContentUpdateAbilityTest extends TestCase
|
|||||||
return $schema;
|
return $schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function ability(PostWriter $writer, ?ChangeLog $log = null): ContentUpdateAbility
|
private function ability(
|
||||||
{
|
PostWriter $writer,
|
||||||
return new ContentUpdateAbility($this->schema(), $writer, $log ?? FakeChangeLog::forbidden());
|
?ChangeLog $log = null,
|
||||||
|
?PostPresenter $presenter = null
|
||||||
|
): ContentUpdateAbility {
|
||||||
|
return new ContentUpdateAbility($this->schema(), $writer, $log ?? FakeChangeLog::forbidden(), $presenter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRevIsRequired(): void
|
public function testRevIsRequired(): void
|
||||||
@@ -110,11 +114,11 @@ final class ContentUpdateAbilityTest extends TestCase
|
|||||||
|
|
||||||
public function testRawContentOnDiviPagesIsUnsupported(): void
|
public function testRawContentOnDiviPagesIsUnsupported(): void
|
||||||
{
|
{
|
||||||
Functions\when('get_post_meta')->justReturn('on');
|
|
||||||
$writer = Mockery::mock(PostWriter::class);
|
$writer = Mockery::mock(PostWriter::class);
|
||||||
$writer->expects('prepare')->never();
|
$writer->expects('prepare')->never();
|
||||||
|
$presenter = new PostPresenter($this->schema(), new FakeBuilderDetector(false, [5]));
|
||||||
|
|
||||||
$result = $this->ability($writer)
|
$result = $this->ability($writer, null, $presenter)
|
||||||
->execute(['post_id' => 5, 'rev' => Rev::ofPost($this->post), 'content' => '<p>Neu</p>']);
|
->execute(['post_id' => 5, 'rev' => Rev::ofPost($this->post), 'content' => '<p>Neu</p>']);
|
||||||
|
|
||||||
self::assertToolError($result, 'unsupported');
|
self::assertToolError($result, 'unsupported');
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use LIT\MCP\Integrations\WordPress\Content\PostPresenter;
|
|||||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||||
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
|
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
|
||||||
use LIT\MCP\Integrations\WordPress\Schema\FieldSource;
|
use LIT\MCP\Integrations\WordPress\Schema\FieldSource;
|
||||||
|
use LIT\MCP\Tests\Support\FakeBuilderDetector;
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
use Mockery;
|
use Mockery;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
@@ -75,19 +76,21 @@ final class PostPresenterTest extends TestCase
|
|||||||
public function testDiviContentIsWithheldAndPointsToTheDiviOutline(): void
|
public function testDiviContentIsWithheldAndPointsToTheDiviOutline(): void
|
||||||
{
|
{
|
||||||
$this->stubLinks();
|
$this->stubLinks();
|
||||||
Functions\when('get_post_meta')->alias(static fn (int $id, string $key) => $key === '_et_pb_use_builder' ? 'on' : '');
|
Functions\when('get_post_meta')->justReturn('');
|
||||||
Functions\when('get_post_thumbnail_id')->justReturn(0);
|
Functions\when('get_post_thumbnail_id')->justReturn(0);
|
||||||
$schema = Mockery::mock(ContentSchema::class);
|
$schema = Mockery::mock(ContentSchema::class);
|
||||||
$schema->allows('taxonomies')->andReturn([]);
|
$schema->allows('taxonomies')->andReturn([]);
|
||||||
$schema->allows('fields')->andReturn([]);
|
$schema->allows('fields')->andReturn([]);
|
||||||
$post = new WP_Post(['ID' => 5, 'post_type' => 'page', 'post_content' => '<!-- wp:divi/section /-->']);
|
$post = new WP_Post(['ID' => 5, 'post_type' => 'page', 'post_content' => '<!-- wp:divi/section /-->']);
|
||||||
|
|
||||||
$full = (new PostPresenter($schema))->full($post);
|
$presenter = new PostPresenter($schema, new FakeBuilderDetector(false, [5]));
|
||||||
|
|
||||||
|
$full = $presenter->full($post);
|
||||||
|
|
||||||
self::assertTrue($full['divi']);
|
self::assertTrue($full['divi']);
|
||||||
self::assertNull($full['content']);
|
self::assertNull($full['content']);
|
||||||
self::assertNull($full['featured_image']);
|
self::assertNull($full['featured_image']);
|
||||||
self::assertSame([PostPresenter::diviHint(false)], $full['hints']);
|
self::assertSame([$presenter->diviHint(false)], $full['hints']);
|
||||||
self::assertInstanceOf(stdClass::class, $full['terms']);
|
self::assertInstanceOf(stdClass::class, $full['terms']);
|
||||||
self::assertInstanceOf(stdClass::class, $full['fields']);
|
self::assertInstanceOf(stdClass::class, $full['fields']);
|
||||||
self::assertSame(Rev::ofPost($post), $full['rev']);
|
self::assertSame(Rev::ofPost($post), $full['rev']);
|
||||||
@@ -95,14 +98,35 @@ final class PostPresenterTest extends TestCase
|
|||||||
|
|
||||||
public function testDiviHintNamesTheDiviToolsOnlyWhenDiviIsActive(): void
|
public function testDiviHintNamesTheDiviToolsOnlyWhenDiviIsActive(): void
|
||||||
{
|
{
|
||||||
self::assertStringContainsString('lit-divi-outline', PostPresenter::diviHint(true));
|
$presenter = new PostPresenter(Mockery::mock(ContentSchema::class));
|
||||||
self::assertStringContainsString('lit-content-update', PostPresenter::diviHint(true));
|
|
||||||
|
self::assertStringContainsString('lit-divi-outline', $presenter->diviHint(true));
|
||||||
|
self::assertStringContainsString('lit-content-update', $presenter->diviHint(true));
|
||||||
foreach (['lit-divi-insert', 'lit-divi-update', 'lit-divi-move', 'lit-divi-delete'] as $tool) {
|
foreach (['lit-divi-insert', 'lit-divi-update', 'lit-divi-move', 'lit-divi-delete'] as $tool) {
|
||||||
self::assertStringContainsString($tool, PostPresenter::diviHint(true));
|
self::assertStringContainsString($tool, $presenter->diviHint(true));
|
||||||
}
|
}
|
||||||
self::assertStringNotContainsString('vorerst im Divi-Builder', PostPresenter::diviHint(true));
|
self::assertStringNotContainsString('vorerst im Divi-Builder', $presenter->diviHint(true));
|
||||||
self::assertStringNotContainsString('lit-divi', PostPresenter::diviHint(false));
|
self::assertStringNotContainsString('lit-divi', $presenter->diviHint(false));
|
||||||
self::assertStringContainsString(PostPresenter::DIVI_INACTIVE_NOTE, PostPresenter::diviHint());
|
self::assertStringContainsString(PostPresenter::DIVI_INACTIVE_NOTE, $presenter->diviHint());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheBuilderDetectorDecidesWhichNoteApplies(): void
|
||||||
|
{
|
||||||
|
$schema = Mockery::mock(ContentSchema::class);
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
PostPresenter::DIVI_NOTE,
|
||||||
|
(new PostPresenter($schema, new FakeBuilderDetector(true)))->diviNote()
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
PostPresenter::DIVI_INACTIVE_NOTE,
|
||||||
|
(new PostPresenter($schema, new FakeBuilderDetector(false)))->diviNote()
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
PostPresenter::DIVI_INACTIVE_NOTE,
|
||||||
|
(new PostPresenter($schema))->diviNote(),
|
||||||
|
'without a detector no builder is assumed'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testPasswordAndProtectedReadonlyMetaFieldsAreOmitted(): void
|
public function testPasswordAndProtectedReadonlyMetaFieldsAreOmitted(): void
|
||||||
@@ -131,9 +155,21 @@ final class PostPresenterTest extends TestCase
|
|||||||
|
|
||||||
public function testDiviIsAlsoDetectedFromBlockMarkup(): void
|
public function testDiviIsAlsoDetectedFromBlockMarkup(): void
|
||||||
{
|
{
|
||||||
Functions\when('get_post_meta')->justReturn('');
|
$presenter = new PostPresenter(Mockery::mock(ContentSchema::class), new FakeBuilderDetector(true, []));
|
||||||
|
|
||||||
self::assertTrue(PostPresenter::usesDivi(new WP_Post(['post_content' => 'x <!-- wp:divi/text {} /--> y'])));
|
self::assertTrue($presenter->usesDivi(new WP_Post(['post_content' => 'x <!-- wp:divi/text {} /--> y'])));
|
||||||
self::assertFalse(PostPresenter::usesDivi(new WP_Post(['post_content' => '<!-- wp:paragraph -->'])));
|
self::assertFalse($presenter->usesDivi(new WP_Post(['post_content' => '<!-- wp:paragraph -->'])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDiviIsDetectedFromTheBuilderFlagOfTheDetector(): void
|
||||||
|
{
|
||||||
|
$presenter = new PostPresenter(Mockery::mock(ContentSchema::class), new FakeBuilderDetector(true, [7]));
|
||||||
|
|
||||||
|
self::assertTrue($presenter->usesDivi(new WP_Post(['ID' => 7, 'post_content' => '[et_pb_section]'])));
|
||||||
|
self::assertFalse($presenter->usesDivi(new WP_Post(['ID' => 8, 'post_content' => '[et_pb_section]'])));
|
||||||
|
self::assertFalse(
|
||||||
|
(new PostPresenter(Mockery::mock(ContentSchema::class)))->usesDivi(new WP_Post(['ID' => 7])),
|
||||||
|
'without a detector only block markup counts'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use Brain\Monkey\Functions;
|
|||||||
use LIT\MCP\Abilities\Cursor;
|
use LIT\MCP\Abilities\Cursor;
|
||||||
use LIT\MCP\Integrations\WordPress\Content\PostPresenter;
|
use LIT\MCP\Integrations\WordPress\Content\PostPresenter;
|
||||||
use LIT\MCP\Integrations\WordPress\Revisions\RevisionsListAbility;
|
use LIT\MCP\Integrations\WordPress\Revisions\RevisionsListAbility;
|
||||||
|
use LIT\MCP\Tests\Support\FakeBuilderDetector;
|
||||||
use LIT\MCP\Tests\Support\SchemaFixture;
|
use LIT\MCP\Tests\Support\SchemaFixture;
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
use WP_Post;
|
use WP_Post;
|
||||||
@@ -24,11 +25,11 @@ final class RevisionsListAbilityTest extends TestCase
|
|||||||
Functions\when('get_post_meta')->justReturn('');
|
Functions\when('get_post_meta')->justReturn('');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function ability(): RevisionsListAbility
|
private function ability(?PostPresenter $presenter = null): RevisionsListAbility
|
||||||
{
|
{
|
||||||
$schema = SchemaFixture::managed();
|
$schema = SchemaFixture::managed();
|
||||||
|
|
||||||
return new RevisionsListAbility($schema);
|
return new RevisionsListAbility($schema, $presenter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testListsRevisionsNewestFirstWithCursor(): void
|
public function testListsRevisionsNewestFirstWithCursor(): void
|
||||||
@@ -60,11 +61,14 @@ final class RevisionsListAbilityTest extends TestCase
|
|||||||
811 => new WP_Post(['ID' => 811, 'post_content' => '<p>Normal</p>']),
|
811 => new WP_Post(['ID' => 811, 'post_content' => '<p>Normal</p>']),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$result = $this->ability()->execute(['post_id' => 5]);
|
$presenter = new PostPresenter(SchemaFixture::managed(), new FakeBuilderDetector(true));
|
||||||
|
|
||||||
|
$result = $this->ability($presenter)->execute(['post_id' => 5]);
|
||||||
|
|
||||||
self::assertTrue($result['items'][0]['divi']);
|
self::assertTrue($result['items'][0]['divi']);
|
||||||
self::assertFalse($result['items'][1]['divi']);
|
self::assertFalse($result['items'][1]['divi']);
|
||||||
self::assertSame([PostPresenter::diviHint()], $result['hints']);
|
self::assertSame([$presenter->diviHint()], $result['hints']);
|
||||||
|
self::assertStringContainsString(PostPresenter::DIVI_NOTE, $result['hints'][0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRevisionsOfATrashedPostCanStillBeListed(): void
|
public function testRevisionsOfATrashedPostCanStillBeListed(): void
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ final class ContentSchemaTest extends TestCase
|
|||||||
{
|
{
|
||||||
$this->stubPostTypes();
|
$this->stubPostTypes();
|
||||||
|
|
||||||
$schema = new ContentSchema([]);
|
$schema = new ContentSchema([], null, ['et_pb_layout']);
|
||||||
|
|
||||||
self::assertSame(['lit_person', 'page', 'post'], $schema->postTypeNames());
|
self::assertSame(['lit_person', 'page', 'post'], $schema->postTypeNames());
|
||||||
self::assertTrue($schema->isManaged('page'));
|
self::assertTrue($schema->isManaged('page'));
|
||||||
@@ -80,7 +80,8 @@ final class ContentSchemaTest extends TestCase
|
|||||||
new FieldDefinition('footnotes', 'footnotes', 'string', 'meta', 'writable'),
|
new FieldDefinition('footnotes', 'footnotes', 'string', 'meta', 'writable'),
|
||||||
new FieldDefinition('zz_hidden', 'Verborgen', 'string', 'pods', FieldDefinition::HIDDEN),
|
new FieldDefinition('zz_hidden', 'Verborgen', 'string', 'pods', FieldDefinition::HIDDEN),
|
||||||
])],
|
])],
|
||||||
static fn (string $type): bool => $type === 'page'
|
static fn (string $type): bool => $type === 'page',
|
||||||
|
['et_pb_layout']
|
||||||
);
|
);
|
||||||
|
|
||||||
$withoutFields = $schema->describe('page', false);
|
$withoutFields = $schema->describe('page', false);
|
||||||
@@ -118,17 +119,34 @@ final class ContentSchemaTest extends TestCase
|
|||||||
self::assertSame(['logo', 'photo'], (new ContentSchema([$source]))->mediaFieldKeys());
|
self::assertSame(['logo', 'photo'], (new ContentSchema([$source]))->mediaFieldKeys());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Spec §3: Divi's post types come from the Katalog adapter – the list itself stays the same. */
|
public function testWordPressExcludesItsOwnPostTypesAndIntegrationsAddTheirs(): void
|
||||||
public function testExcludedPostTypesStayTheSame(): void
|
|
||||||
{
|
{
|
||||||
|
$this->stubPostTypes();
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
['et_pb_layout', 'lit_person', 'page', 'post'],
|
||||||
|
(new ContentSchema([]))->postTypeNames(),
|
||||||
|
'without an integration the Divi layouts are an ordinary post type'
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
['page', 'post'],
|
||||||
|
(new ContentSchema([], null, ['et_pb_layout', 'lit_person']))->postTypeNames()
|
||||||
|
);
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
[
|
[
|
||||||
'attachment', 'nav_menu_item', 'revision', 'wp_block', 'wp_template', 'wp_template_part',
|
'attachment', 'nav_menu_item', 'revision', 'wp_block', 'wp_template', 'wp_template_part',
|
||||||
'wp_navigation', 'wp_global_styles', 'wp_font_family', 'wp_font_face', 'et_pb_layout',
|
'wp_navigation', 'wp_global_styles', 'wp_font_family', 'wp_font_face',
|
||||||
'et_theme_builder', 'et_template', 'et_header_layout', 'et_body_layout', 'et_footer_layout',
|
|
||||||
'et_pb_canvas', '_et_pb_speculation',
|
|
||||||
],
|
],
|
||||||
ContentSchema::EXCLUDED
|
ContentSchema::EXCLUDED
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testTheBuilderIsNeverOnWithoutACheck(): void
|
||||||
|
{
|
||||||
|
self::assertFalse((new ContentSchema([]))->diviEnabled('page'));
|
||||||
|
$schema = new ContentSchema([], static fn (string $type): bool => $type === 'page');
|
||||||
|
|
||||||
|
self::assertTrue($schema->diviEnabled('page'));
|
||||||
|
self::assertFalse($schema->diviEnabled('post'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user