diff --git a/phpmyfaq/src/phpMyFAQ/Category/Order.php b/phpmyfaq/src/phpMyFAQ/Category/Order.php index 4a91a8b33f..72c6b99efe 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 (isset($visited[$categoryId])) { + continue; + } + + // Add current category to visited list + $visited[$categoryId] = true; + + $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); + } }