fix: tb-assign reattaches only templates no Theme Builder post lists (audit L4)
"Detached" now means for lit/tb-assign what lit/tb-list shows as detached: DiviThemeBuilder::isListedAnywhere() looks for any _et_template row (live, draft or Theme Builder library post). A template another Theme Builder post lists is refused with or without reattach; nothing is written. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -76,7 +76,8 @@ bleiben die Felder ungeprüft (die Antwort sagt das). Kopf- und Fußzeile werden
|
|||||||
- Speichert jemand in einem Theme-Builder-Fenster, das vor dem Anlegen einer Vorlage geöffnet wurde, entfernt
|
- Speichert jemand in einem Theme-Builder-Fenster, das vor dem Anlegen einer Vorlage geöffnet wurde, entfernt
|
||||||
Divi die neue Vorlage wieder aus der Liste und löscht sie 7 Tage später. `lit/tb-list` zeigt sie dann unter
|
Divi die neue Vorlage wieder aus der Liste und löscht sie 7 Tage später. `lit/tb-list` zeigt sie dann unter
|
||||||
`detached` (mit `marked_unused`). Nur auf ausdrücklichen Wunsch: `lit/tb-assign` mit `reattach: true` ordnet
|
`detached` (mit `marked_unused`). Nur auf ausdrücklichen Wunsch: `lit/tb-assign` mit `reattach: true` ordnet
|
||||||
sie wieder zu – immer deaktiviert, und nur, wenn ihre Layouts noch aktiv sind.
|
sie wieder zu – immer deaktiviert, nur Vorlagen unter `detached` (keine, die eine Theme-Builder-Vorlage der
|
||||||
|
Divi-Bibliothek nutzt) und nur, wenn ihre Layouts noch aktiv sind.
|
||||||
|
|
||||||
## Rechte
|
## Rechte
|
||||||
|
|
||||||
|
|||||||
@@ -177,6 +177,23 @@ class DiviThemeBuilder
|
|||||||
return array_values(array_map('intval', is_array($ids) ? $ids : []));
|
return array_values(array_map('intval', is_array($ids) ? $ids : []));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether any Theme Builder post (live, draft or a Theme Builder library item) lists template $templateId –
|
||||||
|
* the criterion detachedIds() uses; lit/tb-assign reattaches only templates none lists (audit L4).
|
||||||
|
*/
|
||||||
|
public function isListedAnywhere(int $templateId): bool
|
||||||
|
{
|
||||||
|
global $wpdb;
|
||||||
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery -- the _et_template rows of every Theme Builder post.
|
||||||
|
$found = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT 1 FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value = %s LIMIT 1",
|
||||||
|
self::TEMPLATE_LIST,
|
||||||
|
(string) $templateId
|
||||||
|
));
|
||||||
|
|
||||||
|
return $found !== null;
|
||||||
|
}
|
||||||
|
|
||||||
/** @throws RuntimeException */
|
/** @throws RuntimeException */
|
||||||
public function insertLayout(string $area, string $title): int
|
public function insertLayout(string $area, string $title): int
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ defined('ABSPATH') || exit;
|
|||||||
* (ruling Q3/B3). A live template changes the website at once, so it is a dry run first and needs confirm: true
|
* (ruling Q3/B3). A live template changes the website at once, so it is a dry run first and needs confirm: true
|
||||||
* (ruling Q9/B22), listing both lists before/after and overlaps (identical and possible, B9) with other live
|
* (ruling Q9/B22), listing both lists before/after and overlaps (identical and possible, B9) with other live
|
||||||
* templates for the resulting lists. reattach: true puts a template a stale Theme Builder save removed back on the
|
* templates for the resulting lists. reattach: true puts a template a stale Theme Builder save removed back on the
|
||||||
* live list (only on the user's request, B19) – always disabled, Divi's "unused" marks removed from it and its
|
* live list (only on the user's request, B19; only one no Theme Builder post lists – tb-list's detached, audit L4)
|
||||||
|
* – always disabled, Divi's "unused" marks removed from it and its
|
||||||
* layouts, which must still be published (B10). Templates have no revisions: the previous conditions are in the
|
* layouts, which must still be published (B10). Templates have no revisions: the previous conditions are in the
|
||||||
* answer and the change log; a failed write restores them (and undoes the reattach).
|
* answer and the change log; a failed write restores them (and undoes the reattach).
|
||||||
*/
|
*/
|
||||||
@@ -144,13 +145,30 @@ final class TbAssignAbility implements Ability
|
|||||||
return $this->write($template, $useOn['ids'], $excludeFrom['ids'], $reattach);
|
return $this->write($template, $useOn['ids'], $excludeFrom['ids'], $reattach);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A detached template needs reattach: true, a live Theme Builder post and published layouts (B10). */
|
/**
|
||||||
|
* A detached template needs reattach: true, a live Theme Builder post and published layouts (B10). Detached
|
||||||
|
* means what tb-list shows as detached: no Theme Builder post lists it – a template of a Theme Builder library
|
||||||
|
* item or draft is none (audit L4).
|
||||||
|
*/
|
||||||
private function reattachRefusal(Template $template, bool $reattach): ?WP_Error
|
private function reattachRefusal(Template $template, bool $reattach): ?WP_Error
|
||||||
{
|
{
|
||||||
if ($template->attached) {
|
if ($template->attached) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->access->store()->isListedAnywhere($template->id)) {
|
||||||
|
return Errors::invalidInput(
|
||||||
|
sprintf(
|
||||||
|
'Vorlage %d ist keine entfernte Vorlage (siehe detached in lit/tb-list); nichts wurde gespeichert.',
|
||||||
|
$template->id
|
||||||
|
),
|
||||||
|
[
|
||||||
|
'template_id' => 'nur Vorlagen der Website (templates) oder entfernte Vorlagen (detached) aus '
|
||||||
|
. 'lit/tb-list',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (! $reattach) {
|
if (! $reattach) {
|
||||||
$marked = $template->markedUnused === null
|
$marked = $template->markedUnused === null
|
||||||
? ''
|
? ''
|
||||||
|
|||||||
@@ -128,6 +128,9 @@ final class ThemeBuilderFixtures
|
|||||||
$store->allows('templateIds')->andReturn(array_map(static fn (Template $t): int => $t->id, $templates));
|
$store->allows('templateIds')->andReturn(array_map(static fn (Template $t): int => $t->id, $templates));
|
||||||
$store->allows('template')->andReturnUsing(static fn (int $id): ?Template => $byId[$id] ?? null)->byDefault();
|
$store->allows('template')->andReturnUsing(static fn (int $id): ?Template => $byId[$id] ?? null)->byDefault();
|
||||||
$store->allows('detachedIds')->andReturn(array_map(static fn (Template $t): int => $t->id, $detached));
|
$store->allows('detachedIds')->andReturn(array_map(static fn (Template $t): int => $t->id, $detached));
|
||||||
|
$store->allows('isListedAnywhere')->andReturnUsing(
|
||||||
|
static fn (int $id): bool => in_array($id, array_map(static fn (Template $t): int => $t->id, $templates), true)
|
||||||
|
)->byDefault();
|
||||||
$store->allows('flatConditions')->andReturn(self::conditions());
|
$store->allows('flatConditions')->andReturn(self::conditions());
|
||||||
|
|
||||||
return $store;
|
return $store;
|
||||||
|
|||||||
@@ -26,6 +26,38 @@ final class DiviThemeBuilderTest extends TestCase
|
|||||||
self::assertNull(DiviThemeBuilder::areaOf('page'));
|
self::assertNull(DiviThemeBuilder::areaOf('page'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Review M-3: one list of every Theme Builder post type. */
|
||||||
|
public function testPostTypesNameEveryThemeBuilderType(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
['et_theme_builder', 'et_template', 'et_header_layout', 'et_body_layout', 'et_footer_layout'],
|
||||||
|
DiviThemeBuilder::postTypes()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Audit L4: a template is listed when any Theme Builder post's _et_template row names it. */
|
||||||
|
public function testIsListedAnywhereAsksEveryThemeBuilderPost(): void
|
||||||
|
{
|
||||||
|
$db = Mockery::mock(\wpdb::class)->makePartial();
|
||||||
|
$db->postmeta = 'wp_postmeta';
|
||||||
|
$sql = [];
|
||||||
|
$db->allows('get_var')->andReturnUsing(static function (string $query) use (&$sql): ?string {
|
||||||
|
$sql[] = $query;
|
||||||
|
|
||||||
|
return str_contains($query, "'1066'") ? '1' : null;
|
||||||
|
});
|
||||||
|
$GLOBALS['wpdb'] = $db;
|
||||||
|
|
||||||
|
try {
|
||||||
|
self::assertTrue((new DiviThemeBuilder())->isListedAnywhere(1066));
|
||||||
|
self::assertFalse((new DiviThemeBuilder())->isListedAnywhere(2394));
|
||||||
|
} finally {
|
||||||
|
unset($GLOBALS['wpdb']);
|
||||||
|
}
|
||||||
|
self::assertStringContainsString("meta_key = '_et_template'", $sql[0]);
|
||||||
|
self::assertStringContainsString('LIMIT 1', $sql[0]);
|
||||||
|
}
|
||||||
|
|
||||||
/** F17/F18: Divi wp_die()s without edit_others_posts – the adapter never gets there. */
|
/** F17/F18: Divi wp_die()s without edit_others_posts – the adapter never gets there. */
|
||||||
public function testWithoutEditOthersPostsDiviIsNeverCalled(): void
|
public function testWithoutEditOthersPostsDiviIsNeverCalled(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -180,6 +180,27 @@ final class TbAssignAbilityTest extends TestCase
|
|||||||
self::assertTrue($result['previous']['enabled']);
|
self::assertTrue($result['previous']['enabled']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audit L4: reattach takes only templates no Theme Builder post lists (tb-list's detached) – not one that a
|
||||||
|
* Theme Builder library item or draft lists; nothing is written.
|
||||||
|
*/
|
||||||
|
public function testReattachRefusesATemplateAnotherThemeBuilderPostLists(): void
|
||||||
|
{
|
||||||
|
$listedElsewhere = ThemeBuilderFixtures::template(1066, ['attached' => false]);
|
||||||
|
$store = ThemeBuilderFixtures::store([], 0, [], [$listedElsewhere]);
|
||||||
|
$store->allows('isListedAnywhere')->with(1066)->andReturn(true);
|
||||||
|
foreach (['setEnabled', 'attach', 'unmarkUnused', 'setConditions'] as $method) {
|
||||||
|
$store->shouldReceive($method)->never();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ([['reattach' => true], []] as $extra) {
|
||||||
|
$refused = self::ability($store, FakeChangeLog::forbidden())->execute(self::input($listedElsewhere, $extra));
|
||||||
|
self::assertToolError($refused, 'validation_failed');
|
||||||
|
self::assertStringContainsString('keine entfernte Vorlage', self::clientError($refused)['message']);
|
||||||
|
self::assertArrayHasKey('template_id', self::errorFields($refused));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Ruling Q8/B10: a layout Divi trashed meanwhile blocks the reattach; nothing is written. */
|
/** Ruling Q8/B10: a layout Divi trashed meanwhile blocks the reattach; nothing is written. */
|
||||||
public function testReattachNeedsPublishedLayouts(): void
|
public function testReattachNeedsPublishedLayouts(): void
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user