From e902d0aefdf823134d25b53d406e592fb1e060d3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:00:08 +0000 Subject: [PATCH 1/4] Initial plan From 4d9c3afd04e023fd7f461d85f569be3dc593e387 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:09:40 +0000 Subject: [PATCH 2/4] Fix infinite recursion in getCategoryTree by adding cycle detection Co-authored-by: thorsten <45284+thorsten@users.noreply.github.com> --- phpmyfaq/src/phpMyFAQ/Category/Order.php | 26 +++++++-- tests/phpMyFAQ/Category/OrderTest.php | 74 ++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 4 deletions(-) diff --git a/phpmyfaq/src/phpMyFAQ/Category/Order.php b/phpmyfaq/src/phpMyFAQ/Category/Order.php index 4a91a8b33f..6e4a4f3222 100644 --- a/phpmyfaq/src/phpMyFAQ/Category/Order.php +++ b/phpmyfaq/src/phpMyFAQ/Category/Order.php @@ -120,15 +120,33 @@ public function setCategoryTree( * Returns the category tree. * * @param stdClass[] $categories + * @param int $parentId + * @param array $visited Track visited categories to prevent infinite recursion */ - public function getCategoryTree(array $categories, int $parentId = 0): array + public function getCategoryTree(array $categories, int $parentId = 0, array &$visited = []): array { $result = []; foreach ($categories as $category) { - if ((int)$category['parent_id'] === $parentId) { - $childCategories = $this->getCategoryTree($categories, (int)$category['category_id']); - $result[$category['category_id']] = $childCategories; + $categoryId = (int)$category['category_id']; + $categoryParentId = (int)$category['parent_id']; + + // Skip if category is its own parent or creates a cycle + if ($categoryId === $categoryParentId) { + continue; + } + + if ($categoryParentId === $parentId) { + // Check if this category has already been visited to prevent cycles + if (in_array($categoryId, $visited, true)) { + continue; + } + + // Add current category to visited list + $visited[] = $categoryId; + + $childCategories = $this->getCategoryTree($categories, $categoryId, $visited); + $result[$categoryId] = $childCategories; } } diff --git a/tests/phpMyFAQ/Category/OrderTest.php b/tests/phpMyFAQ/Category/OrderTest.php index 37f465568d..4425448aff 100644 --- a/tests/phpMyFAQ/Category/OrderTest.php +++ b/tests/phpMyFAQ/Category/OrderTest.php @@ -129,4 +129,78 @@ public function testGetParentId(): void $this->assertEquals($expected, $actual); } + + /** + * Test that getCategoryTree handles self-referencing categories without infinite recursion + */ + public function testGetCategoryTreeWithSelfReference(): void + { + // Simulate a category that references itself as parent + $categories = [ + [ + 'category_id' => 1, + 'parent_id' => 1, // Self-reference + 'position' => 1, + ], + [ + 'category_id' => 2, + 'parent_id' => 0, + 'position' => 2, + ], + ]; + + $result = $this->categoryOrder->getCategoryTree($categories); + + // Category 1 should be skipped due to self-reference + // Only category 2 should be in the result + $expected = [ + 2 => [], + ]; + + $this->assertEquals($expected, $result); + } + + /** + * Test that getCategoryTree handles circular references without infinite recursion + */ + public function testGetCategoryTreeWithCircularReference(): void + { + // Simulate circular reference: 1 -> 2 -> 3 -> 2 (cycle) + $categories = [ + [ + 'category_id' => 1, + 'parent_id' => 0, + 'position' => 1, + ], + [ + 'category_id' => 2, + 'parent_id' => 1, + 'position' => 2, + ], + [ + 'category_id' => 3, + 'parent_id' => 2, + 'position' => 3, + ], + [ + 'category_id' => 2, // Duplicate entry creating circular reference + 'parent_id' => 3, + 'position' => 4, + ], + ]; + + $result = $this->categoryOrder->getCategoryTree($categories); + + // Should handle circular reference gracefully + // Category 2 should only be visited once + $expected = [ + 1 => [ + 2 => [ + 3 => [], + ], + ], + ]; + + $this->assertEquals($expected, $result); + } } From 99a80f3c23199cf232ae2dc7c58aabe92a267b04 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:13:29 +0000 Subject: [PATCH 3/4] Optimize cycle detection with isset() for O(1) lookup performance Co-authored-by: thorsten <45284+thorsten@users.noreply.github.com> --- phpmyfaq/src/phpMyFAQ/Category/Order.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/phpmyfaq/src/phpMyFAQ/Category/Order.php b/phpmyfaq/src/phpMyFAQ/Category/Order.php index 6e4a4f3222..8ef13fd8f2 100644 --- a/phpmyfaq/src/phpMyFAQ/Category/Order.php +++ b/phpmyfaq/src/phpMyFAQ/Category/Order.php @@ -138,12 +138,12 @@ public function getCategoryTree(array $categories, int $parentId = 0, array &$vi if ($categoryParentId === $parentId) { // Check if this category has already been visited to prevent cycles - if (in_array($categoryId, $visited, true)) { + if (isset($visited[$categoryId])) { continue; } // Add current category to visited list - $visited[] = $categoryId; + $visited[$categoryId] = true; $childCategories = $this->getCategoryTree($categories, $categoryId, $visited); $result[$categoryId] = $childCategories; From 16be2fbd11184ac384b17f8c1fbba4257c75aa96 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 17:27:47 +0000 Subject: [PATCH 4/4] Fix linting errors: remove trailing whitespace Co-authored-by: thorsten <45284+thorsten@users.noreply.github.com> --- phpmyfaq/src/phpMyFAQ/Category/Order.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/phpmyfaq/src/phpMyFAQ/Category/Order.php b/phpmyfaq/src/phpMyFAQ/Category/Order.php index 8ef13fd8f2..72c6b99efe 100644 --- a/phpmyfaq/src/phpMyFAQ/Category/Order.php +++ b/phpmyfaq/src/phpMyFAQ/Category/Order.php @@ -130,21 +130,21 @@ public function getCategoryTree(array $categories, int $parentId = 0, array &$vi foreach ($categories as $category) { $categoryId = (int)$category['category_id']; $categoryParentId = (int)$category['parent_id']; - + // Skip if category is its own parent or creates a cycle if ($categoryId === $categoryParentId) { continue; } - + if ($categoryParentId === $parentId) { // Check if this category has already been visited to prevent cycles if (isset($visited[$categoryId])) { continue; } - + // Add current category to visited list $visited[$categoryId] = true; - + $childCategories = $this->getCategoryTree($categories, $categoryId, $visited); $result[$categoryId] = $childCategories; }