feat: lit/tb-list and the tool group "Divi Theme Builder"
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,7 @@ use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
|||||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
|
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\Integration;
|
use LIT\MCP\Integrations\Integration;
|
||||||
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;
|
||||||
@@ -170,6 +171,17 @@ final class DiviIntegration implements Integration
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The group "Divi Theme Builder" (lit/tb-*), sharing this integration's Theme Builder access (Etappe 3b). */
|
||||||
|
public function themeBuilder(): ThemeBuilderIntegration
|
||||||
|
{
|
||||||
|
return new ThemeBuilderIntegration(
|
||||||
|
$this->themeBuilderAccess,
|
||||||
|
new DiviComposition($this->modules, $this->design),
|
||||||
|
new DiviSaver(),
|
||||||
|
$this->log
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private function writer(DiviPageLoader $loader): DiviWriter
|
private function writer(DiviPageLoader $loader): DiviWriter
|
||||||
{
|
{
|
||||||
return new DiviWriter(
|
return new DiviWriter(
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Integrations\Divi\ThemeBuilder;
|
||||||
|
|
||||||
|
use LIT\MCP\Abilities\Ability;
|
||||||
|
use LIT\MCP\Abilities\Annotations;
|
||||||
|
use LIT\MCP\Abilities\Errors;
|
||||||
|
use LIT\MCP\Integrations\WordPress\PostGuard;
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* lit/tb-list (spec §5.7): the templates of the live Theme Builder with conditions (Divi's labels), layouts
|
||||||
|
* (title, shared with other templates – the "global" header/footer is the default template's layout shared by
|
||||||
|
* ID), template_rev for the writing TB tools; templates a later Theme Builder save removed ("detached", with
|
||||||
|
* Divi's unused mark – Divi trashes marked posts 7 days later, F19, stage3b finding 10) so they can be
|
||||||
|
* reattached; optionally Divi's condition catalog. Needs ThemeBuilderAccess::canUse(). Ruling Q8/B11: a
|
||||||
|
* template the user may not edit does not appear at all (not listed, not in shared_with, asked for by ID it
|
||||||
|
* reads like a missing one – P12); layout titles only for layouts the user may edit; condition labels name
|
||||||
|
* objects by the stored ID only, never by title.
|
||||||
|
*/
|
||||||
|
final class TbListAbility implements Ability
|
||||||
|
{
|
||||||
|
public const DETACHED_LIMIT = 50;
|
||||||
|
|
||||||
|
public function __construct(private ThemeBuilderAccess $access, private TemplateConditions $conditions)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'lit/tb-list';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return 'Theme-Builder-Vorlagen';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Listet die Vorlagen des Divi Theme Builders: aktiv oder deaktiviert, Bedingungen mit Beschriftung, '
|
||||||
|
. 'Layouts für Kopfzeile, Inhalt und Fußzeile (Layout-IDs lassen sich mit den Divi-Werkzeugen bearbeiten, '
|
||||||
|
. 'z. B. lit-divi-outline mit post_id) und template_rev für lit/tb-assign und lit/tb-activate. '
|
||||||
|
. 'template_id: nur diese Vorlage. conditions: true liefert alle Bedingungen, die Divi anbietet; IDs, die '
|
||||||
|
. 'auf „:“ enden, brauchen ein Objekt (z. B. "singular:post_type:page:id:42"). detached: Vorlagen, die ein '
|
||||||
|
. 'späteres Speichern im Theme Builder entfernt hat; nur auf ausdrücklichen Wunsch wieder zuordnen.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'template_id' => ['type' => 'integer', 'minimum' => 1],
|
||||||
|
'conditions' => ['type' => 'boolean', 'description' => 'Divis Bedingungen mitliefern.'],
|
||||||
|
],
|
||||||
|
'additionalProperties' => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function outputSchema(): ?array
|
||||||
|
{
|
||||||
|
$objects = ['type' => 'array', 'items' => ['type' => 'object']];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'theme_builder_id' => ['type' => 'integer'],
|
||||||
|
'draft_pending' => ['type' => 'boolean'],
|
||||||
|
'templates' => $objects,
|
||||||
|
'detached' => $objects,
|
||||||
|
'conditions' => $objects,
|
||||||
|
'notes' => ['type' => 'array', 'items' => ['type' => 'string']],
|
||||||
|
],
|
||||||
|
'required' => ['theme_builder_id', 'draft_pending', 'templates', 'detached', 'notes'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function annotations(): array
|
||||||
|
{
|
||||||
|
return Annotations::read();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isDirect(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function permission(array $input): bool|WP_Error
|
||||||
|
{
|
||||||
|
return $this->access->canUse() ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(array $input): mixed
|
||||||
|
{
|
||||||
|
$refusal = $this->access->canUse();
|
||||||
|
if ($refusal !== null) {
|
||||||
|
return $refusal;
|
||||||
|
}
|
||||||
|
|
||||||
|
$store = $this->access->store();
|
||||||
|
$editable = fn (Template $template): bool => $this->access->canEditTemplate($template->id);
|
||||||
|
$live = array_values(array_filter($store->liveTemplates(), $editable));
|
||||||
|
$detached = [];
|
||||||
|
foreach ($store->detachedIds(self::DETACHED_LIMIT) as $id) {
|
||||||
|
$template = $store->template($id, false);
|
||||||
|
if ($template !== null && $editable($template)) {
|
||||||
|
$detached[] = $template;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Only what the user may see counts for shared_with (B11).
|
||||||
|
$shared = array_merge($live, $detached);
|
||||||
|
|
||||||
|
if (isset($input['template_id'])) {
|
||||||
|
$id = (int) PostGuard::id($input['template_id']);
|
||||||
|
$only = static fn (array $list): array => array_values(
|
||||||
|
array_filter($list, static fn (Template $template): bool => $template->id === $id)
|
||||||
|
);
|
||||||
|
$live = $only($live);
|
||||||
|
$detached = $only($detached);
|
||||||
|
if ($live === [] && $detached === []) {
|
||||||
|
return Errors::notFound(
|
||||||
|
sprintf('Theme-Builder-Vorlage %d gibt es nicht.', $id),
|
||||||
|
'Vorlagen mit lit/tb-list ohne template_id auflisten.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$draft = $store->draftId() > 0;
|
||||||
|
$notes = [];
|
||||||
|
if ($draft) {
|
||||||
|
$notes[] = 'Der Theme Builder hat ungespeicherte Änderungen (Entwurf); bis sie dort gespeichert oder '
|
||||||
|
. 'verworfen sind, ändert LIT MCP keine Vorlagen.';
|
||||||
|
}
|
||||||
|
if ($detached !== []) {
|
||||||
|
$notes[] = 'detached: Diese Vorlagen hat ein späteres Speichern im Theme Builder entfernt – oder jemand '
|
||||||
|
. 'hat sie dort bewusst gelöscht; Divi löscht als ungenutzt markierte Vorlagen und Layouts 7 Tage '
|
||||||
|
. 'nach der Markierung. Nur auf ausdrücklichen Wunsch wieder zuordnen (bleibt deaktiviert): '
|
||||||
|
. 'lit/tb-assign mit reattach: true.';
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [
|
||||||
|
'theme_builder_id' => $store->liveId(),
|
||||||
|
'draft_pending' => $draft,
|
||||||
|
'templates' => array_map(fn (Template $t): array => $this->describe($t, $shared), $live),
|
||||||
|
'detached' => array_map(fn (Template $t): array => $this->describe($t, $shared), $detached),
|
||||||
|
'notes' => $notes,
|
||||||
|
];
|
||||||
|
if (($input['conditions'] ?? false) === true) {
|
||||||
|
$result['conditions'] = $this->conditions->available();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<Template> $visible Templates the user may see (the shared_with candidates).
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function describe(Template $template, array $visible): array
|
||||||
|
{
|
||||||
|
$row = $template->toArray();
|
||||||
|
foreach (Template::AREAS as $area) {
|
||||||
|
$id = $template->layouts[$area]['id'];
|
||||||
|
$others = array_filter(
|
||||||
|
$visible,
|
||||||
|
static fn (Template $other): bool => $id > 0 && $other->id !== $template->id
|
||||||
|
&& $other->layouts[$area]['id'] === $id
|
||||||
|
);
|
||||||
|
$row['layouts'][$area]['title'] = $id > 0 && current_user_can('edit_post', $id)
|
||||||
|
? (string) get_the_title($id)
|
||||||
|
: '';
|
||||||
|
$row['layouts'][$area]['shared_with'] = array_values(array_map(
|
||||||
|
static fn (Template $other): int => $other->id,
|
||||||
|
$others
|
||||||
|
));
|
||||||
|
}
|
||||||
|
$row['use_on_labels'] = array_map($this->conditions->label(...), $template->useOn);
|
||||||
|
$row['exclude_from_labels'] = array_map($this->conditions->label(...), $template->excludeFrom);
|
||||||
|
|
||||||
|
return $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Integrations\Divi\ThemeBuilder;
|
||||||
|
|
||||||
|
use LIT\MCP\ChangeLog\ChangeLog;
|
||||||
|
use LIT\MCP\Integrations\Divi\Pages\DiviComposition;
|
||||||
|
use LIT\MCP\Integrations\Divi\Pages\DiviSaver;
|
||||||
|
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||||||
|
use LIT\MCP\Integrations\Integration;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The group "Divi Theme Builder" (Etappe 3b): lit/tb-list, lit/tb-create-template, lit/tb-assign and
|
||||||
|
* lit/tb-activate, all indirect. Built by DiviIntegration::themeBuilder() so it shares the Theme Builder access
|
||||||
|
* (rights, Tools-tab gates – its id ThemeBuilderAccess::GROUP is what the gates of the Divi tools ask; once this
|
||||||
|
* group is registered, its mode and "nur lesen" switch in the Tools tab drive them). Available with the Divi 5
|
||||||
|
* builder, like the Divi group; missing Divi internals show in lit-site-status. Composition, saver and log are
|
||||||
|
* for the writing tools of the later tasks.
|
||||||
|
*/
|
||||||
|
final class ThemeBuilderIntegration implements Integration
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ThemeBuilderAccess $access,
|
||||||
|
private DiviComposition $composition,
|
||||||
|
private DiviSaver $saver,
|
||||||
|
private ChangeLog $log
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function id(): string
|
||||||
|
{
|
||||||
|
return ThemeBuilderAccess::GROUP;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return __('Divi Theme Builder', 'wp-lit-mcp');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isAvailable(): bool
|
||||||
|
{
|
||||||
|
return DiviModules::isAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function abilities(): array
|
||||||
|
{
|
||||||
|
$conditions = new TemplateConditions($this->access->store());
|
||||||
|
|
||||||
|
return [
|
||||||
|
new TbListAbility($this->access, $conditions),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -123,7 +123,8 @@ final class Plugin
|
|||||||
),
|
),
|
||||||
new MediaIntegration($schema, $changeLog, $limits),
|
new MediaIntegration($schema, $changeLog, $limits),
|
||||||
$divi,
|
$divi,
|
||||||
$divi->library()
|
$divi->library(),
|
||||||
|
$divi->themeBuilder()
|
||||||
);
|
);
|
||||||
|
|
||||||
$policy = ExposurePolicy::fromOption()->withGroups($registry->groupMap(...));
|
$policy = ExposurePolicy::fromOption()->withGroups($registry->groupMap(...));
|
||||||
|
|||||||
@@ -69,6 +69,25 @@ final class DiviIntegrationTest extends TestCase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testProvidesTheThemeBuilderGroup(): void
|
||||||
|
{
|
||||||
|
Functions\when('__')->returnArg();
|
||||||
|
$group = (new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden()))->themeBuilder();
|
||||||
|
|
||||||
|
self::assertSame('divi-theme-builder', $group->id());
|
||||||
|
self::assertSame('Divi Theme Builder', $group->label());
|
||||||
|
self::assertFalse($group->isAvailable(), 'no Divi classes in unit tests');
|
||||||
|
self::assertSame(
|
||||||
|
['lit/tb-list'],
|
||||||
|
array_map(static fn (Ability $ability): string => $ability->name(), $group->abilities())
|
||||||
|
);
|
||||||
|
foreach ($group->abilities() as $ability) {
|
||||||
|
self::assertFalse($ability->inputSchema()['additionalProperties'], $ability->name());
|
||||||
|
self::assertNotNull($ability->outputSchema(), $ability->name());
|
||||||
|
self::assertFalse($ability->isDirect(), $ability->name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Ruling P9 (E9): lit/divi-node-copy follows lit/divi-insert's Tools-tab state. */
|
/** Ruling P9 (E9): lit/divi-node-copy follows lit/divi-insert's Tools-tab state. */
|
||||||
public function testNodeCopyFollowsLitDiviInsert(): void
|
public function testNodeCopyFollowsLitDiviInsert(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi\ThemeBuilder;
|
||||||
|
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
|
||||||
|
use LIT\MCP\Integrations\Divi\ThemeBuilder\TbListAbility;
|
||||||
|
use LIT\MCP\Integrations\Divi\ThemeBuilder\TemplateConditions;
|
||||||
|
use LIT\MCP\Tests\Support\LibraryFixtures;
|
||||||
|
use LIT\MCP\Tests\Support\ThemeBuilderFixtures;
|
||||||
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
|
||||||
|
final class TbListAbilityTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
LibraryFixtures::given([]);
|
||||||
|
Functions\when('get_the_title')->alias(static fn (int $id): string => '[Fixture] Layout ' . $id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param list<string> $deniedRoles */
|
||||||
|
private static function ability(DiviThemeBuilder $store, array $deniedRoles = [], bool $visible = true): TbListAbility
|
||||||
|
{
|
||||||
|
return new TbListAbility(
|
||||||
|
ThemeBuilderFixtures::access($deniedRoles, true, $store, $visible),
|
||||||
|
new TemplateConditions($store)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param list<int> $notEditable Post IDs without edit_post. */
|
||||||
|
private static function denyEditing(array $notEditable): void
|
||||||
|
{
|
||||||
|
Functions\when('current_user_can')->alias(
|
||||||
|
static fn (string $cap, mixed ...$args): bool => ! ($cap === 'edit_post'
|
||||||
|
&& in_array((int) ($args[0] ?? 0), $notEditable, true))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testListsTemplatesWithLayoutsAndLabels(): void
|
||||||
|
{
|
||||||
|
$store = ThemeBuilderFixtures::store([ThemeBuilderFixtures::template(1065), ThemeBuilderFixtures::template(1066)]);
|
||||||
|
|
||||||
|
$result = self::ability($store)->execute([]);
|
||||||
|
|
||||||
|
self::assertSame(1064, $result['theme_builder_id']);
|
||||||
|
self::assertFalse($result['draft_pending']);
|
||||||
|
self::assertSame([1065, 1066], array_column($result['templates'], 'id'));
|
||||||
|
$template = $result['templates'][1];
|
||||||
|
self::assertFalse($template['enabled']);
|
||||||
|
self::assertFalse($template['live']);
|
||||||
|
self::assertSame(ThemeBuilderFixtures::template(1066)->rev(), $template['template_rev']);
|
||||||
|
self::assertSame(['Bestimmte Seiten: 1053'], $template['use_on_labels']);
|
||||||
|
self::assertSame([], $template['exclude_from_labels']);
|
||||||
|
self::assertSame('[Fixture] Layout 1062', $template['layouts']['body']['title']);
|
||||||
|
self::assertSame([1065], $template['layouts']['header']['shared_with'], 'the global header');
|
||||||
|
self::assertSame([], $template['layouts']['body']['shared_with']);
|
||||||
|
self::assertSame('', $result['templates'][0]['layouts']['body']['title'], 'no layout');
|
||||||
|
self::assertSame([], $result['detached']);
|
||||||
|
self::assertSame([], $result['notes']);
|
||||||
|
self::assertArrayNotHasKey('conditions', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReportsDetachedAndMarkedTemplates(): void
|
||||||
|
{
|
||||||
|
$store = ThemeBuilderFixtures::store(
|
||||||
|
[ThemeBuilderFixtures::template(1065)],
|
||||||
|
0,
|
||||||
|
[ThemeBuilderFixtures::template(1066, ['attached' => false, 'marked_unused' => '2026-09-27 10:00:00'])]
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = self::ability($store)->execute([]);
|
||||||
|
|
||||||
|
self::assertSame([1066], array_column($result['detached'], 'id'));
|
||||||
|
self::assertSame('2026-09-27 10:00:00', $result['detached'][0]['marked_unused']);
|
||||||
|
self::assertFalse($result['detached'][0]['attached']);
|
||||||
|
self::assertSame([1065], $result['detached'][0]['layouts']['header']['shared_with']);
|
||||||
|
self::assertStringContainsString('reattach', implode(' ', $result['notes']));
|
||||||
|
self::assertSame([1066], array_column(self::ability($store)->execute(['template_id' => 1066])['detached'], 'id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOneTemplateTheDraftAndTheConditionCatalog(): void
|
||||||
|
{
|
||||||
|
$store = ThemeBuilderFixtures::store([ThemeBuilderFixtures::template(1065), ThemeBuilderFixtures::template(1066)], 999);
|
||||||
|
|
||||||
|
$result = self::ability($store)->execute(['template_id' => 1066, 'conditions' => true]);
|
||||||
|
|
||||||
|
self::assertSame([1066], array_column($result['templates'], 'id'));
|
||||||
|
self::assertTrue($result['draft_pending']);
|
||||||
|
self::assertStringContainsString('Entwurf', implode(' ', $result['notes']));
|
||||||
|
self::assertContains('singular:post_type:lit_person:all', array_column($result['conditions'], 'id'));
|
||||||
|
self::assertToolError(self::ability($store)->execute(['template_id' => 4711]), 'not_found');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNeedsTheThemeBuilderRole(): void
|
||||||
|
{
|
||||||
|
$ability = self::ability(ThemeBuilderFixtures::store(), ['theme_builder']);
|
||||||
|
|
||||||
|
self::assertToolError($ability->permission([]), 'forbidden');
|
||||||
|
self::assertToolError($ability->execute([]), 'forbidden');
|
||||||
|
self::assertTrue($ability->annotations()['readonly']);
|
||||||
|
self::assertFalse($ability->isDirect());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ruling Q15: without edit_theme_options the Theme Builder is closed. */
|
||||||
|
public function testNeedsEditThemeOptions(): void
|
||||||
|
{
|
||||||
|
LibraryFixtures::given([], ['edit_theme_options']);
|
||||||
|
|
||||||
|
self::assertToolError(self::ability(ThemeBuilderFixtures::store())->execute([]), 'forbidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ruling Q8/B11: templates the user may not edit do not appear anywhere – not in the lists, not in
|
||||||
|
* shared_with; asking for one reads exactly like a missing ID (P12).
|
||||||
|
*/
|
||||||
|
public function testHidesTemplatesTheUserCannotEdit(): void
|
||||||
|
{
|
||||||
|
self::denyEditing([1065, 1067]);
|
||||||
|
$store = ThemeBuilderFixtures::store(
|
||||||
|
[ThemeBuilderFixtures::template(1065), ThemeBuilderFixtures::template(1066)],
|
||||||
|
0,
|
||||||
|
[ThemeBuilderFixtures::template(1067, ['attached' => false])]
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = self::ability($store)->execute([]);
|
||||||
|
|
||||||
|
self::assertSame([1066], array_column($result['templates'], 'id'));
|
||||||
|
self::assertSame([], $result['detached']);
|
||||||
|
self::assertSame([], $result['templates'][0]['layouts']['header']['shared_with']);
|
||||||
|
self::assertSame([], $result['notes'], 'no hint about hidden detached templates');
|
||||||
|
$encoded = (string) json_encode($result);
|
||||||
|
self::assertStringNotContainsString('1065', $encoded);
|
||||||
|
self::assertStringNotContainsString('1067', $encoded);
|
||||||
|
|
||||||
|
$hidden = self::ability($store)->execute(['template_id' => 1065]);
|
||||||
|
$missing = self::ability($store)->execute(['template_id' => 4711]);
|
||||||
|
self::assertToolError($hidden, 'not_found');
|
||||||
|
self::assertSame(
|
||||||
|
$missing->get_error_message(),
|
||||||
|
str_replace('1065', '4711', $hidden->get_error_message())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ruling Q8/B11: layout titles only for layouts the user may edit. */
|
||||||
|
public function testLayoutTitlesOnlyForEditableLayouts(): void
|
||||||
|
{
|
||||||
|
self::denyEditing([1062]);
|
||||||
|
$store = ThemeBuilderFixtures::store([ThemeBuilderFixtures::template(1066)]);
|
||||||
|
|
||||||
|
$layouts = self::ability($store)->execute([])['templates'][0]['layouts'];
|
||||||
|
|
||||||
|
self::assertSame('', $layouts['body']['title']);
|
||||||
|
self::assertSame('[Fixture] Layout 1061', $layouts['header']['title']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConditionLabelsNeverNameObjectTitles(): void
|
||||||
|
{
|
||||||
|
Functions\when('get_the_title')->justReturn('Geheime Seite');
|
||||||
|
$store = ThemeBuilderFixtures::store([ThemeBuilderFixtures::template(1066, [
|
||||||
|
'layouts' => [
|
||||||
|
'header' => ['id' => 0, 'enabled' => true],
|
||||||
|
'body' => ['id' => 0, 'enabled' => true],
|
||||||
|
'footer' => ['id' => 0, 'enabled' => true],
|
||||||
|
],
|
||||||
|
'exclude_from' => ['singular:post_type:page:children:id:7', 'unbekannt:x'],
|
||||||
|
])]);
|
||||||
|
|
||||||
|
$result = self::ability($store)->execute([]);
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
['Unterseiten bestimmter Seiten: 7', 'unbekannt:x'],
|
||||||
|
$result['templates'][0]['exclude_from_labels']
|
||||||
|
);
|
||||||
|
self::assertStringNotContainsString('Geheime Seite', (string) json_encode($result));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user