feat: adapter-level Divi release lock, content-create and site status
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ 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;
|
||||
|
||||
@@ -19,7 +20,10 @@ class EnvironmentProbe
|
||||
{
|
||||
}
|
||||
|
||||
/** @return list<array{id: string, active: bool, version: ?string, problems: list<string>}> */
|
||||
/**
|
||||
* @return list<array{id: string, active: bool, version: ?string, problems: list<string>,
|
||||
* release: ?array<string, mixed>}>
|
||||
*/
|
||||
public function components(): array
|
||||
{
|
||||
$diviVersion = DiviEnvironment::version();
|
||||
@@ -33,18 +37,21 @@ class EnvironmentProbe
|
||||
'active' => $isDivi,
|
||||
'version' => $diviVersion,
|
||||
'problems' => $isDivi ? ($this->divi ?? new DiviCompatibility())->problems() : [],
|
||||
'release' => $isDivi ? ReleaseGate::status()->toArray() : null,
|
||||
],
|
||||
[
|
||||
'id' => 'pods',
|
||||
'active' => defined('PODS_VERSION'),
|
||||
'version' => defined('PODS_VERSION') ? (string) PODS_VERSION : null,
|
||||
'problems' => [],
|
||||
'release' => null,
|
||||
],
|
||||
[
|
||||
'id' => 'the-events-calendar',
|
||||
'active' => $tecActive,
|
||||
'version' => $tecActive ? (string) constant('Tribe__Events__Main::VERSION') : null,
|
||||
'problems' => [],
|
||||
'release' => null,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ final class SiteStatusAbility implements Ability
|
||||
{
|
||||
return 'Überblick: Website, WordPress-Version, eigener Benutzer und Rollen, '
|
||||
. 'erkannte Integrationen (Divi, Pods, The Events Calendar). '
|
||||
. 'Bei Divi nennt problems Divi-Funktionen, die nach einem Update fehlen.';
|
||||
. 'Bei Divi nennt problems Divi-Stellen, die nach einem Update fehlen oder sich geändert haben, und '
|
||||
. 'release, ob LIT MCP mit dieser Divi-Version schreiben darf (released, untested, incompatible).';
|
||||
}
|
||||
|
||||
public function inputSchema(): array
|
||||
|
||||
@@ -7,8 +7,10 @@ namespace LIT\MCP\Integrations\Divi\Library;
|
||||
use LIT\MCP\Abilities\WriteFailed;
|
||||
use LIT\MCP\Integrations\Divi\Release\Call;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
|
||||
use LIT\MCP\Integrations\Divi\Release\Manifest;
|
||||
use LIT\MCP\Integrations\Divi\Release\Mirror;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use LIT\MCP\Integrations\WordPress\Content\NewDraft;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
@@ -211,9 +213,11 @@ class DiviLibraryStore implements DiviAdapter
|
||||
* when that delete fails; a throwing insert hook is a partial write without ID.
|
||||
*
|
||||
* @throws WriteFailed
|
||||
* @throws DiviLocked When Divi is not released (spec §5.5) – before anything is written.
|
||||
*/
|
||||
public function create(LibraryItemSpec $spec, NewDraft $draft): int
|
||||
{
|
||||
ReleaseGate::assertWritable();
|
||||
$missing = $this->missingTaxonomies();
|
||||
if ($missing !== []) {
|
||||
throw WriteFailed::rolledBack(
|
||||
|
||||
@@ -10,6 +10,7 @@ use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Outline;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Serializer;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||||
use LIT\MCP\Integrations\WordPress\Content\ContentComposer;
|
||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||
@@ -59,6 +60,13 @@ final class DiviContentComposer implements ContentComposer
|
||||
return ($this->writable)();
|
||||
}
|
||||
|
||||
public function refusal(): ?WP_Error
|
||||
{
|
||||
$status = ReleaseGate::status();
|
||||
|
||||
return $status->writable() ? null : $status->refusal();
|
||||
}
|
||||
|
||||
public function check(string $postType, array $sections): ?WP_Error
|
||||
{
|
||||
if (! $this->schema->diviEnabled($postType)) {
|
||||
|
||||
@@ -8,6 +8,8 @@ use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Abilities\FollowUp;
|
||||
use LIT\MCP\Abilities\Rollback;
|
||||
use LIT\MCP\Abilities\WriteFailed;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
|
||||
use LIT\MCP\Integrations\WordPress\PostGuard;
|
||||
use LIT\MCP\Integrations\WordPress\RevisionPoint;
|
||||
@@ -69,10 +71,12 @@ class DiviSaver
|
||||
/**
|
||||
* @return array{revision_id: ?int, warnings: list<string>}
|
||||
* @throws WriteFailed
|
||||
* @throws DiviLocked When Divi is not released (spec §5.5) – before anything is written.
|
||||
*/
|
||||
public function save(WP_Post $post, string $content, SaveScope $scope = SaveScope::Page): array
|
||||
{
|
||||
$postId = (int) $post->ID;
|
||||
ReleaseGate::assertWritable($postId);
|
||||
$revisionId = null;
|
||||
|
||||
try {
|
||||
@@ -138,9 +142,11 @@ class DiviSaver
|
||||
*
|
||||
* @return list<string> Warnings (CHANGED_ON_SAVE).
|
||||
* @throws RuntimeException
|
||||
* @throws DiviLocked When Divi is not released (spec §5.5) – before anything is written.
|
||||
*/
|
||||
public function saveNew(int $postId, string $content, SaveScope $scope = SaveScope::Page): array
|
||||
{
|
||||
ReleaseGate::assertWritable($postId);
|
||||
$refusal = self::refusal($this->writeContent($postId, $content));
|
||||
if ($refusal !== null) {
|
||||
throw new RuntimeException(sprintf('wp_update_post(%d) refused the Divi layout: %s', $postId, $refusal));
|
||||
|
||||
@@ -6,8 +6,10 @@ namespace LIT\MCP\Integrations\Divi\ThemeBuilder;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Release\Call;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
|
||||
use LIT\MCP\Integrations\Divi\Release\Manifest;
|
||||
use LIT\MCP\Integrations\Divi\Release\Mirror;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
use WP_Error;
|
||||
@@ -229,9 +231,13 @@ class DiviThemeBuilder implements DiviAdapter
|
||||
return $found !== null;
|
||||
}
|
||||
|
||||
/** @throws RuntimeException */
|
||||
/**
|
||||
* @throws RuntimeException
|
||||
* @throws DiviLocked When Divi is not released (spec §5.5) – before anything is written.
|
||||
*/
|
||||
public function insertLayout(string $area, string $title): int
|
||||
{
|
||||
ReleaseGate::assertWritable();
|
||||
$type = self::LAYOUT_TYPES[$area] ?? throw new RuntimeException('unknown Theme Builder area ' . $area);
|
||||
$this->assertCanWrite('et_theme_builder_insert_layout');
|
||||
$id = $this->guarded(
|
||||
@@ -253,9 +259,11 @@ class DiviThemeBuilder implements DiviAdapter
|
||||
* @param array{title: string, layouts: array<string, array{id: int, enabled: bool}>, use_on: list<string>,
|
||||
* exclude_from: list<string>} $template
|
||||
* @throws RuntimeException
|
||||
* @throws DiviLocked When Divi is not released (spec §5.5) – before anything is written.
|
||||
*/
|
||||
public function storeTemplate(int $themeBuilderId, array $template): int
|
||||
{
|
||||
ReleaseGate::assertWritable();
|
||||
$this->assertCanWrite('et_theme_builder_store_template', 'et_');
|
||||
$payload = [
|
||||
'title' => $template['title'],
|
||||
@@ -285,9 +293,11 @@ class DiviThemeBuilder implements DiviAdapter
|
||||
* template unused (stage3b README finding 10) – detachedIds() finds it, callers unmarkUnused() on reattach.
|
||||
*
|
||||
* @throws RuntimeException
|
||||
* @throws DiviLocked When Divi is not released (spec §5.5) – before anything is written.
|
||||
*/
|
||||
public function attach(int $themeBuilderId, int $templateId): void
|
||||
{
|
||||
ReleaseGate::assertWritable();
|
||||
$existing = get_post_meta($themeBuilderId, self::TEMPLATE_LIST, false);
|
||||
if (is_array($existing) && in_array((string) $templateId, array_map('strval', $existing), true)) {
|
||||
return;
|
||||
@@ -304,8 +314,10 @@ class DiviThemeBuilder implements DiviAdapter
|
||||
}
|
||||
}
|
||||
|
||||
/** @throws DiviLocked When Divi is not released (spec §5.5) – before anything is written. */
|
||||
public function detach(int $themeBuilderId, int $templateId): void
|
||||
{
|
||||
ReleaseGate::assertWritable();
|
||||
delete_post_meta($themeBuilderId, self::TEMPLATE_LIST, (string) $templateId);
|
||||
}
|
||||
|
||||
@@ -316,9 +328,11 @@ class DiviThemeBuilder implements DiviAdapter
|
||||
* @param list<string> $useOn
|
||||
* @param list<string> $excludeFrom
|
||||
* @throws RuntimeException
|
||||
* @throws DiviLocked When Divi is not released (spec §5.5) – before anything is written.
|
||||
*/
|
||||
public function setConditions(int $templateId, array $useOn, array $excludeFrom): void
|
||||
{
|
||||
ReleaseGate::assertWritable();
|
||||
foreach ([self::USE_ON => $useOn, self::EXCLUDE_FROM => $excludeFrom] as $key => $conditions) {
|
||||
delete_post_meta($templateId, $key);
|
||||
foreach (array_values(array_unique($conditions)) as $condition) {
|
||||
@@ -329,9 +343,13 @@ class DiviThemeBuilder implements DiviAdapter
|
||||
}
|
||||
}
|
||||
|
||||
/** @throws RuntimeException */
|
||||
/**
|
||||
* @throws RuntimeException
|
||||
* @throws DiviLocked When Divi is not released (spec §5.5) – before anything is written.
|
||||
*/
|
||||
public function setEnabled(int $templateId, bool $enabled): void
|
||||
{
|
||||
ReleaseGate::assertWritable();
|
||||
$value = $enabled ? '1' : '0';
|
||||
update_post_meta($templateId, self::ENABLED, $value);
|
||||
if ((string) get_post_meta($templateId, self::ENABLED, true) !== $value) {
|
||||
@@ -339,9 +357,14 @@ class DiviThemeBuilder implements DiviAdapter
|
||||
}
|
||||
}
|
||||
|
||||
/** Removes Divi's "unused" mark (F19), as Divi's own save does for every listed post. */
|
||||
/**
|
||||
* Removes Divi's "unused" mark (F19), as Divi's own save does for every listed post.
|
||||
*
|
||||
* @throws DiviLocked When Divi is not released (spec §5.5) – before anything is written.
|
||||
*/
|
||||
public function unmarkUnused(int $postId): void
|
||||
{
|
||||
ReleaseGate::assertWritable();
|
||||
delete_post_meta($postId, self::UNUSED);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,12 @@ interface ContentComposer
|
||||
*/
|
||||
public function writable(): bool;
|
||||
|
||||
/**
|
||||
* Why writing such content is refused right now although it is enabled (e.g. the installed Divi version is not
|
||||
* released for LIT MCP, spec §5.5); null = allowed.
|
||||
*/
|
||||
public function refusal(): ?WP_Error;
|
||||
|
||||
/**
|
||||
* Validates the description for a new entry of $postType. Nothing is written.
|
||||
*
|
||||
|
||||
@@ -133,6 +133,11 @@ final class ContentCreateAbility implements Ability
|
||||
);
|
||||
}
|
||||
|
||||
$locked = $withDivi ? $this->divi->refusal() : null;
|
||||
if ($locked !== null) {
|
||||
return $locked;
|
||||
}
|
||||
|
||||
$prepared = $this->writer->prepare($postType, $input, true);
|
||||
$problem = self::combined($prepared, $withDivi ? $this->checkDivi($postType, $input) : null);
|
||||
if ($problem !== null) {
|
||||
|
||||
@@ -25,6 +25,7 @@ use LIT\MCP\Integrations\Core\CoreIntegration;
|
||||
use LIT\MCP\Integrations\Divi\DiviIntegration;
|
||||
use LIT\MCP\Integrations\Divi\Library\LibraryAccess;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviToolGate;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\ThemeBuilderAccess;
|
||||
use LIT\MCP\Integrations\IntegrationRegistry;
|
||||
use LIT\MCP\Integrations\Media\MediaIntegration;
|
||||
@@ -73,6 +74,11 @@ final class Plugin
|
||||
(new ChangeLogInstaller($changeLog))->maybeInstall();
|
||||
(new ChangeLogCleanup($changeLog, $limits))->register();
|
||||
|
||||
// Spec §5.4: a theme update checks the Divi fingerprints again at once (the state key would, too).
|
||||
add_action('upgrader_process_complete', static function (): void {
|
||||
delete_transient(ReleaseResolver::CACHE);
|
||||
});
|
||||
|
||||
$schema = self::contentSchema();
|
||||
$registry = null;
|
||||
$policy = null;
|
||||
|
||||
@@ -26,4 +26,22 @@ final class EnvironmentProbeTest extends TestCase
|
||||
self::assertSame(['GlobalLayout'], $components['divi']['problems']);
|
||||
self::assertSame([], $components['pods']['problems']);
|
||||
}
|
||||
|
||||
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,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
|
||||
use LIT\MCP\Integrations\Divi\Library\LibraryItemSpec;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviSaver;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
|
||||
use LIT\MCP\Integrations\WordPress\Content\NewDraft;
|
||||
use LIT\MCP\Tests\Support\FakeChangeLog;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use WP_Post;
|
||||
|
||||
/** Spec §5.5: every writing adapter method refuses before its first write while Divi is not released. */
|
||||
final class ReleaseGuardsTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
ReleaseGate::useResolver(static fn (): ReleaseStatus => new ReleaseStatus(ReleaseState::Untested, '5.3.0'));
|
||||
// Nothing may be written: every write function WordPress offers fails the test.
|
||||
foreach (['wp_update_post', 'wp_insert_post', 'update_post_meta', 'add_post_meta', 'delete_post_meta',
|
||||
'update_option', 'delete_option', 'wp_set_object_terms'] as $write) {
|
||||
Functions\when($write)->alias(static function () use ($write): never {
|
||||
self::fail($write . ' ran although Divi is locked');
|
||||
});
|
||||
}
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
}
|
||||
|
||||
/** @return iterable<string, array{callable(): mixed}> */
|
||||
public static function writes(): iterable
|
||||
{
|
||||
$post = new WP_Post(['ID' => 7, 'post_content' => '', 'post_type' => 'page']);
|
||||
$spec = new LibraryItemSpec('Hero', 'section', false, 'regular', null, 'page');
|
||||
|
||||
yield 'save' => [static fn () => (new DiviSaver())->save($post, '<!-- wp:divi/section /-->')];
|
||||
yield 'saveNew' => [static fn () => (new DiviSaver())->saveNew(7, '<!-- wp:divi/section /-->')];
|
||||
yield 'library create' => [static fn () => (new DiviLibraryStore())->create(
|
||||
$spec,
|
||||
new NewDraft(null, FakeChangeLog::forbidden(), 'lit/library-save', 'et_pb_layout', 'Bibliothekseintrag')
|
||||
)];
|
||||
yield 'tb insertLayout' => [static fn () => (new DiviThemeBuilder())->insertLayout('header', 'Kopf')];
|
||||
yield 'tb storeTemplate' => [static fn () => (new DiviThemeBuilder())->storeTemplate(3, [
|
||||
'title' => 'V', 'layouts' => [], 'use_on' => [], 'exclude_from' => [],
|
||||
])];
|
||||
yield 'tb attach' => [static fn () => (new DiviThemeBuilder())->attach(3, 9)];
|
||||
yield 'tb detach' => [static fn () => (new DiviThemeBuilder())->detach(3, 9)];
|
||||
yield 'tb setConditions' => [static fn () => (new DiviThemeBuilder())->setConditions(9, ['singular'], [])];
|
||||
yield 'tb setEnabled' => [static fn () => (new DiviThemeBuilder())->setEnabled(9, true)];
|
||||
yield 'tb unmarkUnused' => [static fn () => (new DiviThemeBuilder())->unmarkUnused(9)];
|
||||
}
|
||||
|
||||
/** @dataProvider writes */
|
||||
public function testRefusesBeforeTheFirstWrite(callable $write): void
|
||||
{
|
||||
$this->expectException(DiviLocked::class);
|
||||
|
||||
$write();
|
||||
}
|
||||
|
||||
public function testTheCheckPageMayBeWrittenWhileProbing(): void
|
||||
{
|
||||
Functions\when('wp_update_post')->justReturn(41);
|
||||
Functions\when('update_post_meta')->justReturn(true);
|
||||
Functions\when('get_post_meta')->alias(
|
||||
static fn (int $id, string $key): string => $key === '_et_pb_show_page_creation' ? 'off' : 'on'
|
||||
);
|
||||
Functions\when('get_post')->justReturn(new WP_Post(['ID' => 41, 'post_content' => 'x']));
|
||||
Functions\when('has_filter')->justReturn(false);
|
||||
|
||||
$warnings = ReleaseGate::whileProbing(41, static fn () => (new DiviSaver())->saveNew(41, 'x'));
|
||||
|
||||
self::assertSame([], $warnings);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ use LIT\MCP\Abilities\WriteFailed;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviContentComposer;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviSaver;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
|
||||
use LIT\MCP\Integrations\WordPress\Content\ContentComposer;
|
||||
use LIT\MCP\Integrations\WordPress\Content\ContentCreateAbility;
|
||||
use LIT\MCP\Integrations\WordPress\Content\NewDraft;
|
||||
@@ -56,11 +58,12 @@ final class ContentCreateDiviTest extends TestCase
|
||||
}
|
||||
|
||||
/** @return ContentComposer&MockInterface */
|
||||
private function composer(bool $available = true, bool $writable = true): ContentComposer
|
||||
private function composer(bool $available = true, bool $writable = true, ?WP_Error $refusal = null): ContentComposer
|
||||
{
|
||||
$composer = Mockery::mock(ContentComposer::class);
|
||||
$composer->allows('available')->andReturn($available);
|
||||
$composer->allows('writable')->andReturn($writable);
|
||||
$composer->allows('refusal')->andReturn($refusal);
|
||||
$composer->allows('check')->with('page', self::SECTIONS)->andReturn(null);
|
||||
|
||||
return $composer;
|
||||
@@ -194,6 +197,16 @@ final class ContentCreateDiviTest extends TestCase
|
||||
* Security M2: with the Divi group disabled or "nur lesen", or lit/divi-insert disabled, the
|
||||
* divi input is refused before anything is created; content-create without divi keeps working.
|
||||
*/
|
||||
public function testLockedDiviRefusesBeforeTheDraftExists(): void
|
||||
{
|
||||
$refusal = (new ReleaseStatus(ReleaseState::Untested, '5.3.0'))->refusal();
|
||||
Functions\expect('wp_insert_post')->never();
|
||||
|
||||
$result = $this->ability($this->composer(true, true, $refusal))->execute(self::INPUT);
|
||||
|
||||
self::assertToolError($result, 'divi_untested');
|
||||
}
|
||||
|
||||
public function testDiviIsRefusedWhenDiviWritesAreSwitchedOff(): void
|
||||
{
|
||||
Functions\expect('wp_insert_post')->never();
|
||||
@@ -224,6 +237,7 @@ final class ContentCreateDiviTest extends TestCase
|
||||
$composer = Mockery::mock(ContentComposer::class);
|
||||
$composer->allows('available')->andReturn(true);
|
||||
$composer->allows('writable')->andReturn(true);
|
||||
$composer->allows('refusal')->andReturn(null);
|
||||
$composer->allows('check')->andReturn(Errors::invalidAttrPath(['divi[0].attrs.x' => 'unbekannt']));
|
||||
$writer = Mockery::mock(PostWriter::class);
|
||||
$writer->allows('prepare')->andReturn(Errors::invalidInput('ungültig', ['fields.role' => 'Pflichtfeld fehlt.']));
|
||||
@@ -323,6 +337,11 @@ final class ContentCreateDiviTest extends TestCase
|
||||
return true;
|
||||
}
|
||||
|
||||
public function refusal(): ?WP_Error
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function check(string $postType, array $sections): ?WP_Error
|
||||
{
|
||||
return $this->real->check($postType, $sections);
|
||||
|
||||
Reference in New Issue
Block a user