fix(audit M1, L2): Meta-Schlüssel-Optionen gegen alle Schleifen der Vorfahrenkette prüfen; queryType wie sanitize_key normalisieren, terms/users als Term-/Benutzer-Schleife
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
52208764f0
commit
522fc8ff4d
@@ -45,8 +45,10 @@ final class LoopContext
|
|||||||
|
|
||||||
$queryType = self::settings($attrs)['queryType'] ?? 'post_types';
|
$queryType = self::settings($attrs)['queryType'] ?? 'post_types';
|
||||||
|
|
||||||
// Divi's sanitize_key() makes a non-string "" – a loop without a kind the tools support.
|
// Divi's sanitize_key() (LoopUtils.php:729-731) makes a non-string "" and lowercases and strips the rest.
|
||||||
return LoopScope::ofQueryType(is_string($queryType) ? $queryType : '');
|
return LoopScope::ofQueryType(
|
||||||
|
is_string($queryType) ? preg_replace('/[^a-z0-9_\-]/', '', strtolower($queryType)) ?? '' : ''
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,14 +56,20 @@ final class LoopContext
|
|||||||
*/
|
*/
|
||||||
public static function fromChain(array $attrChain): LoopScope
|
public static function fromChain(array $attrChain): LoopScope
|
||||||
{
|
{
|
||||||
|
$scopes = [];
|
||||||
foreach (array_reverse($attrChain) as $attrs) {
|
foreach (array_reverse($attrChain) as $attrs) {
|
||||||
$scope = self::ownScope($attrs);
|
$scope = self::ownScope($attrs);
|
||||||
if ($scope !== null) {
|
if ($scope !== null) {
|
||||||
return $scope;
|
$scopes[] = $scope;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ($scopes === []) {
|
||||||
|
return LoopScope::none();
|
||||||
|
}
|
||||||
|
|
||||||
return LoopScope::none();
|
$outerKinds = array_map(static fn (LoopScope $outer): ?string => $outer->kind(), array_slice($scopes, 1));
|
||||||
|
|
||||||
|
return $scopes[0]->inside($outerKinds);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -103,14 +111,12 @@ final class LoopContext
|
|||||||
/** The loop of the node $nodeId (path ID) or its nearest ancestor; none() for an unknown ID. */
|
/** The loop of the node $nodeId (path ID) or its nearest ancestor; none() for an unknown ID. */
|
||||||
public static function of(NodeIndex $index, string $nodeId): LoopScope
|
public static function of(NodeIndex $index, string $nodeId): LoopScope
|
||||||
{
|
{
|
||||||
|
$chain = [];
|
||||||
for ($node = $index->find($nodeId); $node !== null; $node = $index->parentBlock($node)) {
|
for ($node = $index->find($nodeId); $node !== null; $node = $index->parentBlock($node)) {
|
||||||
$scope = $node->isOpaque() ? null : self::ownScope($node->attrs());
|
array_unshift($chain, $node->isOpaque() ? [] : $node->attrs());
|
||||||
if ($scope !== null) {
|
|
||||||
return $scope;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return LoopScope::none();
|
return self::fromChain($chain);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,10 +11,15 @@ defined('ABSPATH') || exit;
|
|||||||
* queryType: post_types and current_page → post, post_taxonomies → term, user_roles → user, menus → menu
|
* queryType: post_types and current_page → post, post_taxonomies → term, user_roles → user, menus → menu
|
||||||
* (the five values the Visual Builder offers; LoopUtils.php:730-739, QueryTypeController.php:89-96).
|
* (the five values the Visual Builder offers; LoopUtils.php:730-739, QueryTypeController.php:89-96).
|
||||||
*
|
*
|
||||||
* Any other queryType – repeater_<ACF field>, the aliases "terms"/"users", "" – is a loop the tools do
|
* "terms" and "users" are Divi's aliases for term and user loops (LoopUtils.php:881-898; LoopContext
|
||||||
* not support: queryType() names it, but it has no kind and is not active(), so every loop_* option is
|
* normalises the queryType like sanitize_key first). Any other queryType – repeater_<ACF field>, "" – is a
|
||||||
* refused inside it (fail closed). ACF repeater rows would need the setting acf_type, which the content
|
* loop the tools do not support: queryType() names it, but it has no kind and is not active(), so every
|
||||||
* rules refuse (ruling X7e); Divi's content renderer knows no aliases (LoopUtils.php:3130-3148).
|
* loop_* option is refused inside it (fail closed). ACF repeater rows would need the setting acf_type,
|
||||||
|
* which the content rules refuse (ruling X7e).
|
||||||
|
*
|
||||||
|
* Nested loops (audit M1): an empty inner loop hands its block unchanged to the outer loop, which then
|
||||||
|
* resolves the references with ITS object. So kinds() lists the kinds of all enabled loops of the chain,
|
||||||
|
* and a meta key option has to fit every one of them (fits()).
|
||||||
*
|
*
|
||||||
* kindOfOption() is the kind an option NAME belongs to: Divi takes the meta type of
|
* kindOfOption() is the kind an option NAME belongs to: Divi takes the meta type of
|
||||||
* loop_<post|user|term>_meta_key_* from the name and reads the meta of the loop object's ID, whatever the
|
* loop_<post|user|term>_meta_key_* from the name and reads the meta of the loop object's ID, whatever the
|
||||||
@@ -35,6 +40,8 @@ final class LoopScope
|
|||||||
'post_taxonomies' => self::TERM,
|
'post_taxonomies' => self::TERM,
|
||||||
'user_roles' => self::USER,
|
'user_roles' => self::USER,
|
||||||
'menus' => self::MENU,
|
'menus' => self::MENU,
|
||||||
|
'terms' => self::TERM,
|
||||||
|
'users' => self::USER,
|
||||||
];
|
];
|
||||||
|
|
||||||
private const LABELS = [
|
private const LABELS = [
|
||||||
@@ -52,7 +59,8 @@ final class LoopScope
|
|||||||
'loop_acf_' => null,
|
'loop_acf_' => null,
|
||||||
];
|
];
|
||||||
|
|
||||||
private function __construct(private ?string $queryType)
|
/** @param list<?string> $outerKinds Kinds of the enclosing enabled loops, innermost first */
|
||||||
|
private function __construct(private ?string $queryType, private array $outerKinds = [])
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +74,26 @@ final class LoopScope
|
|||||||
return new self($queryType);
|
return new self($queryType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The same loop inside the loops of $outerKinds (innermost first; null = an unsupported loop).
|
||||||
|
*
|
||||||
|
* @param list<?string> $outerKinds
|
||||||
|
*/
|
||||||
|
public function inside(array $outerKinds): self
|
||||||
|
{
|
||||||
|
return new self($this->queryType, $outerKinds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kinds of all enabled loops of the chain, innermost (own) first; null for an unsupported loop; [] without.
|
||||||
|
*
|
||||||
|
* @return list<?string>
|
||||||
|
*/
|
||||||
|
public function kinds(): array
|
||||||
|
{
|
||||||
|
return $this->queryType === null ? [] : [$this->kind(), ...$this->outerKinds];
|
||||||
|
}
|
||||||
|
|
||||||
/** Whether there is a loop whose loop_* options the tools allow. */
|
/** Whether there is a loop whose loop_* options the tools allow. */
|
||||||
public function active(): bool
|
public function active(): bool
|
||||||
{
|
{
|
||||||
@@ -92,10 +120,19 @@ final class LoopScope
|
|||||||
return $kind === null ? (string) $this->queryType : self::LABELS[$kind];
|
return $kind === null ? (string) $this->queryType : self::LABELS[$kind];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether the loop_* option $option belongs to this loop's kind. */
|
/**
|
||||||
|
* Whether the loop_* option $option belongs to this loop's kind – a meta key option (loop_<kind>_meta_key_*)
|
||||||
|
* to the kind of every loop of the chain (audit M1), since Divi reads meta by the ID of whatever loop
|
||||||
|
* object resolves it.
|
||||||
|
*/
|
||||||
public function fits(string $option): bool
|
public function fits(string $option): bool
|
||||||
{
|
{
|
||||||
return $this->active() && self::kindOfOption($option) === $this->kind();
|
$kind = self::kindOfOption($option);
|
||||||
|
if (! $this->active() || $kind !== $this->kind()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ! str_contains($option, '_meta_key_') || array_unique($this->kinds()) === [$kind];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Kind of a loop_* option by its name; null for other names and ACF repeater options. */
|
/** Kind of a loop_* option by its name; null for other names and ACF repeater options. */
|
||||||
|
|||||||
@@ -338,6 +338,9 @@ final class DiviDynamicSourcesAbility implements Ability, RequiresDiviFeatures
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ($this->map->loopFields($kind, (int) $page->post->ID) as $name => $settings) {
|
foreach ($this->map->loopFields($kind, (int) $page->post->ID) as $name => $settings) {
|
||||||
|
if (! $scope->fits((string) $name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$result['loop_sources'][] = [
|
$result['loop_sources'][] = [
|
||||||
'name' => (string) $name,
|
'name' => (string) $name,
|
||||||
'placeholder' => self::placeholder((string) $name),
|
'placeholder' => self::placeholder((string) $name),
|
||||||
|
|||||||
@@ -79,8 +79,17 @@ final class DiviMoveAbility extends DiviWriteAbility
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Both scopes from the tree before the move: the new parent never lies inside $node (TreeEditor::move()).
|
// Both scopes from the tree before the move: the new parent never lies inside $node (TreeEditor::move()).
|
||||||
$from = self::loopAt($page->index, $page->index->parentBlock($node));
|
// Audit M1: for $node and its loop-enabled descendants – the new ancestors join their chains.
|
||||||
$to = self::loopAt($page->index, $position['parent']);
|
$target = self::ancestorAttrs($page->index, $position['parent']);
|
||||||
|
$warnings = [];
|
||||||
|
foreach (self::loopRoots($node) as [$root, $path]) {
|
||||||
|
$names = LoopContext::optionsIn($root, true);
|
||||||
|
$from = self::loopAt($page->index, $root);
|
||||||
|
$to = LoopContext::fromChain([...$target, ...$path]);
|
||||||
|
if (self::loopMisfits($names, $from, $to)) {
|
||||||
|
$warnings[] = self::loopWarning($names, $from, $to);
|
||||||
|
}
|
||||||
|
}
|
||||||
$problem = $editor->move($page->index, $node, $position);
|
$problem = $editor->move($page->index, $node, $position);
|
||||||
if ($problem !== null) {
|
if ($problem !== null) {
|
||||||
return Errors::invalidInput(
|
return Errors::invalidInput(
|
||||||
@@ -90,9 +99,8 @@ final class DiviMoveAbility extends DiviWriteAbility
|
|||||||
}
|
}
|
||||||
|
|
||||||
$change = (new DiviChange(sprintf('Knoten %s verschoben', $id)))->record($node, 'moved');
|
$change = (new DiviChange(sprintf('Knoten %s verschoben', $id)))->record($node, 'moved');
|
||||||
$names = LoopContext::enabled($node->attrs()) ? [] : LoopContext::optionsIn($node, true);
|
foreach ($warnings as $warning) {
|
||||||
if ($names !== [] && $from->kind() !== $to->kind()) {
|
$change->warning($warning);
|
||||||
$change->warning(self::loopWarning($names, $from, $to));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $change;
|
return $change;
|
||||||
|
|||||||
@@ -192,21 +192,28 @@ final class DiviUpdateAbility extends DiviWriteAbility
|
|||||||
static fn (array $update): bool => $update[1] !== $update[0]->attrs()
|
static fn (array $update): bool => $update[1] !== $update[0]->attrs()
|
||||||
));
|
));
|
||||||
$change = new DiviChange(sprintf('%d Divi-Knoten geändert', count($updates)));
|
$change = new DiviChange(sprintf('%d Divi-Knoten geändert', count($updates)));
|
||||||
$before = [];
|
// Audit M1: besides the changed blocks, their loop-enabled descendants (an outer loop changes their chain).
|
||||||
|
$roots = [];
|
||||||
foreach ($updates as [$block]) {
|
foreach ($updates as [$block]) {
|
||||||
$before[spl_object_id($block)] = self::loopAt($page->index, $block);
|
foreach (self::loopRoots($block) as [$root]) {
|
||||||
|
$roots[spl_object_id($root)] = $root;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
$before = array_map(static fn (Block $root): LoopScope => self::loopAt($page->index, $root), $roots);
|
||||||
foreach ($updates as [$block, $attrs]) {
|
foreach ($updates as [$block, $attrs]) {
|
||||||
// Composer::update() output: every client value already passed the choke point.
|
// Composer::update() output: every client value already passed the choke point.
|
||||||
$block->setAttrs($attrs);
|
$block->setAttrs($attrs);
|
||||||
$change->record($block, 'updated');
|
$change->record($block, 'updated');
|
||||||
}
|
}
|
||||||
foreach ($updates as [$block]) {
|
foreach ($roots as $key => $root) {
|
||||||
$from = $before[spl_object_id($block)];
|
$to = self::loopAt($page->index, $root);
|
||||||
$to = self::loopAt($page->index, $block);
|
$names = LoopContext::optionsIn($root, true);
|
||||||
$names = $from->kind() === $to->kind() ? [] : LoopContext::optionsIn($block, true);
|
if (self::loopMisfits($names, $before[$key], $to)) {
|
||||||
if ($names !== []) {
|
$change->warning(sprintf(
|
||||||
$change->warning(sprintf('%s: %s', $page->index->idOf($block), self::loopWarning($names, $from, $to)));
|
'%s: %s',
|
||||||
|
$page->index->idOf($root),
|
||||||
|
self::loopWarning($names, $before[$key], $to)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -128,26 +128,77 @@ abstract class DiviWriteAbility implements Ability, RequiresDiviFeatures
|
|||||||
* @param array<string, array<string, mixed>> $newAttrs
|
* @param array<string, array<string, mixed>> $newAttrs
|
||||||
*/
|
*/
|
||||||
protected static function loopAt(NodeIndex $index, ?Block $block, array $newAttrs = []): LoopScope
|
protected static function loopAt(NodeIndex $index, ?Block $block, array $newAttrs = []): LoopScope
|
||||||
|
{
|
||||||
|
return LoopContext::fromChain(self::ancestorAttrs($index, $block, $newAttrs));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attributes from the root down to $block (inclusive); $newAttrs replaces those of nodes this call changes.
|
||||||
|
*
|
||||||
|
* @param array<string, array<string, mixed>> $newAttrs
|
||||||
|
* @return list<array<string, mixed>>
|
||||||
|
*/
|
||||||
|
protected static function ancestorAttrs(NodeIndex $index, ?Block $block, array $newAttrs = []): array
|
||||||
{
|
{
|
||||||
$chain = [];
|
$chain = [];
|
||||||
for ($node = $block; $node !== null; $node = $index->parentBlock($node)) {
|
for ($node = $block; $node !== null; $node = $index->parentBlock($node)) {
|
||||||
array_unshift($chain, $newAttrs[(string) $index->idOf($node)] ?? $node->attrs());
|
array_unshift($chain, $newAttrs[(string) $index->idOf($node)] ?? $node->attrs());
|
||||||
}
|
}
|
||||||
|
|
||||||
return LoopContext::fromChain($chain);
|
return $chain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* $block and every loop-enabled descendant (audit M1: their chains change with an outer loop, and meta key
|
||||||
|
* options care), each with the attrs from $block down to it. Opaque blocks are not entered.
|
||||||
|
*
|
||||||
|
* @param list<array<string, mixed>> $path
|
||||||
|
* @return list<array{0: Block, 1: list<array<string, mixed>>}>
|
||||||
|
*/
|
||||||
|
protected static function loopRoots(Block $block, array $path = [], bool $top = true): array
|
||||||
|
{
|
||||||
|
$path[] = $block->attrs();
|
||||||
|
$roots = $top || LoopContext::enabled($block->attrs()) ? [[$block, $path]] : [];
|
||||||
|
if (! $block->isOpaque()) {
|
||||||
|
foreach ($block->children() as $child) {
|
||||||
|
array_push($roots, ...self::loopRoots($child, $path, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $roots;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether moving or changing loop content from $from to $to needs a warning: the kind changes, or (audit M1)
|
||||||
|
* a loop option no longer fits the whole chain – meta key options are checked against every enclosing loop.
|
||||||
|
*
|
||||||
|
* @param list<string> $names
|
||||||
|
*/
|
||||||
|
protected static function loopMisfits(array $names, LoopScope $from, LoopScope $to): bool
|
||||||
|
{
|
||||||
|
if ($names === []) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $from->kind() !== $to->kind()
|
||||||
|
|| array_filter($names, static fn (string $name): bool => ! $to->fits($name)) !== [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param list<string> $names */
|
/** @param list<string> $names */
|
||||||
protected static function loopWarning(array $names, LoopScope $from, LoopScope $to): string
|
protected static function loopWarning(array $names, LoopScope $from, LoopScope $to): string
|
||||||
{
|
{
|
||||||
$where = $to->active()
|
$where = $to->queryType() === null
|
||||||
? sprintf(
|
? 'jetzt außerhalb jeder Divi-Schleife; Divi zeigt sie dort leer'
|
||||||
|
: ($to->active() ? sprintf(
|
||||||
'jetzt in einer Divi-Schleife vom Typ: %s (vorher: %s); passen sie nicht zu dieser Art, zeigt Divi '
|
'jetzt in einer Divi-Schleife vom Typ: %s (vorher: %s); passen sie nicht zu dieser Art, zeigt Divi '
|
||||||
. 'sie leer oder mit Daten eines anderen Objekts mit derselben ID',
|
. 'sie leer oder mit Daten eines anderen Objekts mit derselben ID',
|
||||||
$to->label(),
|
$to->label(),
|
||||||
$from->active() ? $from->label() : 'keine Schleife'
|
$from->active() ? $from->label() : 'keine Schleife'
|
||||||
)
|
) : sprintf(
|
||||||
: 'jetzt außerhalb jeder Divi-Schleife; Divi zeigt sie dort leer';
|
'jetzt in einer Divi-Schleife, die dieses Werkzeug nicht unterstützt (Typ: %s); Divi kann Daten '
|
||||||
|
. 'eines anderen Objekts zeigen',
|
||||||
|
$to->label()
|
||||||
|
));
|
||||||
|
|
||||||
return sprintf(
|
return sprintf(
|
||||||
'Schleifen-Inhalte (%s) liegen %s. Zurückverschieben oder die Inhalte anpassen.',
|
'Schleifen-Inhalte (%s) liegen %s. Zurückverschieben oder die Inhalte anpassen.',
|
||||||
|
|||||||
@@ -119,6 +119,37 @@ final class LoopContextTest extends TestCase
|
|||||||
self::assertSame('repeater_team', $inner->queryType());
|
self::assertSame('repeater_team', $inner->queryType());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Audit M1: a meta key option must fit EVERY enabled loop of the chain (an empty inner loop hands over to the outer). */
|
||||||
|
public function testMetaKeyOptionsMustFitEveryLoopOfTheChain(): void
|
||||||
|
{
|
||||||
|
$posts = self::loop(['enable' => 'on', 'queryType' => 'post_types']);
|
||||||
|
$terms = self::loop(['enable' => 'on', 'queryType' => 'post_taxonomies']);
|
||||||
|
$repeater = self::loop(['enable' => 'on', 'queryType' => 'repeater_team']);
|
||||||
|
$meta = 'loop_post_meta_key_manual_custom_field';
|
||||||
|
|
||||||
|
$postInTerm = LoopContext::fromChain([$terms, $posts]);
|
||||||
|
self::assertSame([LoopScope::POST, LoopScope::TERM], $postInTerm->kinds());
|
||||||
|
self::assertFalse($postInTerm->fits($meta));
|
||||||
|
self::assertTrue($postInTerm->fits('loop_post_title'), 'other options keep the innermost rule');
|
||||||
|
self::assertTrue(LoopContext::fromChain([$posts, $posts])->fits($meta));
|
||||||
|
self::assertFalse(LoopContext::fromChain([$repeater, $posts])->fits($meta), 'unsupported outer loop');
|
||||||
|
self::assertTrue(LoopContext::fromChain([self::loop([]), $posts])->fits($meta), 'a disabled loop is no loop');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Audit L2: Divi sanitize_key()s queryType and knows the aliases terms/users. */
|
||||||
|
public function testQueryTypeIsNormalisedLikeSanitizeKey(): void
|
||||||
|
{
|
||||||
|
$kind = static fn (string $q): ?string => LoopContext::ownScope(self::loop(['enable' => 'on', 'queryType' => $q]))
|
||||||
|
?->kind();
|
||||||
|
|
||||||
|
self::assertSame(LoopScope::TERM, $kind('Post_Taxonomies'));
|
||||||
|
self::assertSame(LoopScope::TERM, $kind(' post_taxonomies '));
|
||||||
|
self::assertSame(LoopScope::TERM, $kind('terms'));
|
||||||
|
self::assertSame(LoopScope::USER, $kind('users'));
|
||||||
|
self::assertSame(LoopScope::POST, $kind("POST_TYPES\n"));
|
||||||
|
self::assertNull($kind('repeater_team'));
|
||||||
|
}
|
||||||
|
|
||||||
private static function page(): NodeIndex
|
private static function page(): NodeIndex
|
||||||
{
|
{
|
||||||
$loop = static fn (string $queryType): string => '{"module":{"advanced":{"loop":{"desktop":{"value":'
|
$loop = static fn (string $queryType): string => '{"module":{"advanced":{"loop":{"desktop":{"value":'
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ final class LoopScopeTest extends TestCase
|
|||||||
'taxonomies' => ['post_taxonomies', LoopScope::TERM, 'Terme'],
|
'taxonomies' => ['post_taxonomies', LoopScope::TERM, 'Terme'],
|
||||||
'user roles' => ['user_roles', LoopScope::USER, 'Benutzer'],
|
'user roles' => ['user_roles', LoopScope::USER, 'Benutzer'],
|
||||||
'menus' => ['menus', LoopScope::MENU, 'Menü'],
|
'menus' => ['menus', LoopScope::MENU, 'Menü'],
|
||||||
|
'terms alias (audit L2)' => ['terms', LoopScope::TERM, 'Terme'],
|
||||||
|
'users alias (audit L2)' => ['users', LoopScope::USER, 'Benutzer'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +52,7 @@ final class LoopScopeTest extends TestCase
|
|||||||
*/
|
*/
|
||||||
public function testOtherQueryTypesAreALoopWithoutKind(): void
|
public function testOtherQueryTypesAreALoopWithoutKind(): void
|
||||||
{
|
{
|
||||||
foreach (['repeater_team', 'terms', 'users', '', 'Post_Types', 'post_types '] as $queryType) {
|
foreach (['repeater_team', '', 'Post_Types', 'post_types '] as $queryType) {
|
||||||
$scope = LoopScope::ofQueryType($queryType);
|
$scope = LoopScope::ofQueryType($queryType);
|
||||||
|
|
||||||
self::assertFalse($scope->active(), $queryType);
|
self::assertFalse($scope->active(), $queryType);
|
||||||
|
|||||||
@@ -373,4 +373,70 @@ final class DiviLoopWriteTest extends DiviWriteAbilityTestCase
|
|||||||
]);
|
]);
|
||||||
self::assertStringContainsString('außerhalb jeder Divi-Schleife', implode(' | ', $off['warnings']));
|
self::assertStringContainsString('außerhalb jeder Divi-Schleife', implode(' | ', $off['warnings']));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function useChainPage(string $outerType): void
|
||||||
|
{
|
||||||
|
$meta = VariableRef::serialize('content', 'loop_post_meta_key_manual_custom_field', ['loop_meta_key' => 'telefon']);
|
||||||
|
$loop = static fn (string $type): array => self::loop(['enable' => 'on', 'queryType' => $type]);
|
||||||
|
$this->original = '<!-- wp:divi/placeholder -->' . self::block('section', $loop($outerType), self::block(
|
||||||
|
'row',
|
||||||
|
$loop('post_types'),
|
||||||
|
self::block('column', [], self::text('<p>Fest</p>') . self::text($meta))
|
||||||
|
)) . '<!-- /wp:divi/placeholder -->';
|
||||||
|
$this->post->post_content = $this->original;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Audit M1: a post loop inside a term loop may be empty; the term loop would then read post meta by term ID. */
|
||||||
|
public function testPostMetaInAPostLoopInsideATermLoopIsRefusedOnInsertAndUpdate(): void
|
||||||
|
{
|
||||||
|
$this->useChainPage('post_taxonomies');
|
||||||
|
$meta = VariableRef::serialize('content', 'loop_post_meta_key_manual_custom_field', ['loop_meta_key' => 'telefon']);
|
||||||
|
|
||||||
|
$insert = $this->insertInto('s0.r0.c0', ['nodes' => [['type' => 'text', 'content' => $meta]]]);
|
||||||
|
self::assertStringContainsString('passt nicht zur Schleife', self::problems($insert));
|
||||||
|
$update = $this->changeAll([['id' => 's0.r0.c0.m0', 'set' => ['content' => $meta]]]);
|
||||||
|
self::assertStringContainsString('passt nicht zur Schleife', self::problems($update));
|
||||||
|
self::assertSame([], $this->saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPostMetaInAPostLoopInsideAPostLoopIsAccepted(): void
|
||||||
|
{
|
||||||
|
$this->useChainPage('post_types');
|
||||||
|
$meta = VariableRef::serialize('content', 'loop_post_meta_key_manual_custom_field', ['loop_meta_key' => 'telefon']);
|
||||||
|
|
||||||
|
$insert = $this->insertInto('s0.r0.c0', ['nodes' => [['type' => 'text', 'content' => $meta]]]);
|
||||||
|
self::assertTrue($insert['ok'], (string) json_encode($insert));
|
||||||
|
$update = $this->changeAll([['id' => 's0.r0.c0.m0', 'set' => ['content' => $meta]]]);
|
||||||
|
self::assertTrue($update['ok'], (string) json_encode($update));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Audit M1: the update warning follows the chain, not only the innermost loop. */
|
||||||
|
public function testUpdateWarnsWhenAnOuterLoopTurnsIntoAnotherKindUnderMetaContent(): void
|
||||||
|
{
|
||||||
|
$this->useChainPage('post_types');
|
||||||
|
|
||||||
|
$result = $this->changeAll([
|
||||||
|
['id' => 's0', 'attrs' => ['module.advanced.loop' => ['desktop' => self::TERM_LOOP]]],
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::assertTrue($result['ok'], (string) json_encode($result));
|
||||||
|
self::assertStringContainsString('loop_post_meta_key_manual_custom_field', implode(' | ', $result['warnings']));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Audit L2: "terms" is a term loop for Divi, an unsupported target "can show data of another object". */
|
||||||
|
public function testUpdateWarningNamesTheRealKindOfAnAliasAndNeverCallsAnUnsupportedLoopEmpty(): void
|
||||||
|
{
|
||||||
|
$alias = $this->changeAll([
|
||||||
|
['id' => 's0.r0.c0', 'attrs' => ['module.advanced.loop' => ['desktop' => ['enable' => 'on', 'queryType' => 'Terms']]]],
|
||||||
|
]);
|
||||||
|
self::assertStringContainsString('Typ: Terme (vorher: Beiträge)', implode(' | ', $alias['warnings']));
|
||||||
|
|
||||||
|
$this->post->post_content = $this->original;
|
||||||
|
$other = $this->changeAll([
|
||||||
|
['id' => 's0.r0.c0', 'attrs' => ['module.advanced.loop' => ['desktop' => ['enable' => 'on', 'queryType' => 'repeater_x']]]],
|
||||||
|
]);
|
||||||
|
$text = implode(' | ', $other['warnings']);
|
||||||
|
self::assertStringContainsString('kann Daten eines anderen Objekts zeigen', $text);
|
||||||
|
self::assertStringNotContainsString('zeigt sie dort leer', $text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user