From 0129744506b86ab6db921cb2f9d4bc750f018678 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Mon, 4 May 2026 00:45:55 -0600 Subject: [PATCH 1/5] Revert "[3.0] Move search board loading logic into Board" This reverts commit ccabfa2da6de1312f45104101b529ce81b4ab9c1. Signed-off-by: Jon Stovell --- Sources/Actions/Search.php | 94 +++++++++++++++++++++++++++++++++----- Sources/Board.php | 53 --------------------- 2 files changed, 83 insertions(+), 64 deletions(-) diff --git a/Sources/Actions/Search.php b/Sources/Actions/Search.php index 7d5a9f39f9..41e46ab51a 100644 --- a/Sources/Actions/Search.php +++ b/Sources/Actions/Search.php @@ -18,7 +18,7 @@ use SMF\ActionInterface; use SMF\ActionRouter; use SMF\ActionTrait; -use SMF\Board; +use SMF\Category; use SMF\Config; use SMF\Db\DatabaseApi as Db; use SMF\ErrorHandler; @@ -70,7 +70,6 @@ public function execute(): void // Don't load this in XML mode. if (!isset($_REQUEST['xml'])) { Theme::loadTemplate('Search'); - Theme::loadTemplate('GenericControls'); Theme::loadJavaScriptFile('suggest.js', ['defer' => false, 'minimize' => true], 'smf_suggest'); } @@ -175,18 +174,91 @@ public function execute(): void } } - // If user selected some particular boards, is this one of them? - if (!empty(Utils::$context['search_params']['brd'])) { - $boards = Utils::$context['search_params']['brd']; + // Find all the boards this user is allowed to see. + $request = Db::$db->query( + 'SELECT b.id_cat, c.name AS cat_name, b.id_board, b.name, b.child_level + FROM {db_prefix}boards AS b + LEFT JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat) + WHERE {query_see_board} + AND redirect = {string:empty_string}', + [ + 'empty_string' => '', + ], + identifier: 'order_by_board_order', + ); + Utils::$context['num_boards'] = Db::$db->num_rows($request); + Utils::$context['boards_check_all'] = true; + Utils::$context['categories'] = []; + + while ($row = Db::$db->fetch_assoc($request)) { + // This category hasn't been set up yet.. + if (!isset(Utils::$context['categories'][$row['id_cat']])) { + Utils::$context['categories'][$row['id_cat']] = [ + 'id' => $row['id_cat'], + 'name' => $row['cat_name'], + 'boards' => [], + ]; + } + + $is_recycle_board = !empty(Config::$modSettings['recycle_enable']) && $row['id_board'] == Config::$modSettings['recycle_board']; + + // Set this board up, and let the template know when it's a child. (indent them..) + Utils::$context['categories'][$row['id_cat']]['boards'][$row['id_board']] = [ + 'id' => $row['id_board'], + 'name' => $row['name'], + 'child_level' => $row['child_level'], + ]; + + // If user selected some particular boards, is this one of them? + if (!empty(Utils::$context['search_params']['brd'])) { + Utils::$context['categories'][$row['id_cat']]['boards'][$row['id_board']]['selected'] = \in_array($row['id_board'], Utils::$context['search_params']['brd']); + } + // User didn't select any boards, so select all except ignored and recycle boards. + else { + Utils::$context['categories'][$row['id_cat']]['boards'][$row['id_board']]['selected'] = !$is_recycle_board && !\in_array($row['id_board'], User::$me->ignoreboards); + } + + // If a board wasn't checked that probably should have been ensure the board selection is selected, yo! + if (!Utils::$context['categories'][$row['id_cat']]['boards'][$row['id_board']]['selected'] && !$is_recycle_board) { + Utils::$context['boards_check_all'] = false; + } + } + Db::$db->free_result($request); + + Category::sort(Utils::$context['categories']); + + // Now, let's sort the list of categories into the boards for templates that like that. + $temp_boards = []; + + foreach (Utils::$context['categories'] as $category) { + $temp_boards[] = [ + 'name' => $category['name'], + 'child_ids' => array_keys($category['boards']), + ]; + $temp_boards = array_merge($temp_boards, array_values($category['boards'])); + + // Include a list of boards per category for easy toggling. + Utils::$context['categories'][$category['id']]['child_ids'] = array_keys($category['boards']); } - // User didn't select any boards, so select all except ignored and recycle boards. - elseif (!empty(Config::$modSettings['recycle_enable']) && !empty(Config::$modSettings['recycle_board'])) { - $boards = array_merge(User::$me->ignoreboards, [(int) Config::$modSettings['recycle_board']]); - } else { - $boards = User::$me->ignoreboards; + + $max_boards = ceil(\count($temp_boards) / 2); + + if ($max_boards == 1) { + $max_boards = 2; } - Utils::$context['categories'] = Board::getUserVisibleBoards($boards); + // Now, alternate them so they can be shown left and right ;). + Utils::$context['board_columns'] = []; + + for ($i = 0; $i < $max_boards; $i++) { + Utils::$context['board_columns'][] = $temp_boards[$i]; + + if (isset($temp_boards[$i + $max_boards])) { + Utils::$context['board_columns'][] = $temp_boards[$i + $max_boards]; + } else { + Utils::$context['board_columns'][] = []; + } + } if (!empty($_REQUEST['topic'])) { Utils::$context['search_params']['topic'] = (int) $_REQUEST['topic']; diff --git a/Sources/Board.php b/Sources/Board.php index d20ef689be..9d11fee656 100644 --- a/Sources/Board.php +++ b/Sources/Board.php @@ -1108,59 +1108,6 @@ public static function getMsgMemberID(int $messageID): int return (int) $memberID; } - /** - * Fetches the list of boards the user is allowed to see and organizes them by categories. - * - * This function queries the database for boards visible to the current user, grouped by categories. - * It returns a structured array of categories and their associated boards. - * - * @param array $boards An array of board IDs to mark as selected. - * @return array The structured array of categories and boards. - */ - public static function getUserVisibleBoards(array $boards): array - { - // Query to fetch boards visible to the user. - $request = Db::$db->query( - 'SELECT id_board, b.name, child_level, c.name AS cat_name, id_cat - FROM {db_prefix}boards AS b - JOIN {db_prefix}categories AS c USING (id_cat) - WHERE {query_see_board} - AND redirect = {string:empty_string} - ORDER BY board_order', - [ - 'empty_string' => '', - ], - identifier: 'order_by_board_order', - ); - - $categories = []; - $categoryTracker = []; - $currentCategoryIndex = -1; - - // Process the results and group boards by categories. - while ($row = Db::$db->fetch_assoc($request)) { - if (!isset($categoryTracker[$row['id_cat']])) { - $categories[++$currentCategoryIndex] = [ - 'id' => (int) $row['id_cat'], - 'name' => $row['cat_name'], - 'boards' => [], - ]; - $categoryTracker[$row['id_cat']] = true; - } - - $categories[$currentCategoryIndex]['boards'][] = [ - 'id' => (int) $row['id_board'], - 'name' => $row['name'], - 'child_level' => (int) $row['child_level'], - 'selected' => \in_array($row['id_board'], $boards), - ]; - } - - Db::$db->free_result($request); - - return $categories; - } - /** * Modify the settings and position of a board. * Used by ManageBoards.php to change the settings of a board. From 84826948ed9908091744cd5440a6d9405b396513 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Mon, 4 May 2026 00:47:18 -0600 Subject: [PATCH 2/5] Adds SMF\Category::$child_ids property Signed-off-by: Jon Stovell --- Sources/Category.php | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/Sources/Category.php b/Sources/Category.php index b34a9db6fd..09905c751f 100644 --- a/Sources/Category.php +++ b/Sources/Category.php @@ -136,6 +136,24 @@ class Category implements \ArrayAccess */ public bool $show_unread; + /** + * @var array + * + * IDs of all boards in this category. + * + * For the sake of compatibility with \ArrayAccess it is possible to write + * to this property, but doing so is pointless because the value will be + * overwritten the next time the property is read. + */ + public array $child_ids { + &get { + $this->child_ids = []; + self::recursiveBoards($this->child_ids, $this); + + return $this->child_ids; + } + } + /************************** * Public static properties **************************/ @@ -785,9 +803,9 @@ public static function getTree(): void * Used by self::getTree(). * * @param array &$list The board list - * @param \SMF\Category|\SMF\Board &$tree The board tree + * @param self|Board &$tree The board tree */ - public static function recursiveBoards(array &$list, \SMF\Category|\SMF\Board &$tree): void + public static function recursiveBoards(array &$list, self|Board &$tree): void { if (empty($tree->children)) { return; From cd39e1e29ac3ebd0d134eed177de7c45f4085fac Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Mon, 4 May 2026 00:48:23 -0600 Subject: [PATCH 3/5] Uses property hooks for SMF\Board::$recycle Signed-off-by: Jon Stovell --- Sources/Board.php | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/Sources/Board.php b/Sources/Board.php index 9d11fee656..d08927cddd 100644 --- a/Sources/Board.php +++ b/Sources/Board.php @@ -231,8 +231,23 @@ class Board implements \ArrayAccess, Routable * @var bool * * Whether this board is the recycle bin board. - */ - public bool $recycle = false; + * + * For the sake of compatibility with \ArrayAccess it is possible to write + * to this property, but doing so is pointless because the value will be + * overwritten the next time the property is read. + */ + public bool $recycle { + // @todo Once \ArrayAccess compatibility is no longer required, change this hook to + // `get => !empty(Config::$modSettings['recycle_enable']) && $this->id == (Config::$modSettings['recycle_board'] ?? NAN);` + &get { + $this->recycle = ( + !empty(Config::$modSettings['recycle_enable']) + && $this->id == (Config::$modSettings['recycle_board'] ?? NAN) + ); + + return $this->recycle; + } + } /** * @var bool @@ -2260,7 +2275,6 @@ protected function loadBoardInfo(): void case 'id_board': $props['id'] = (int) $value; - $props['recycle'] = !empty(Config::$modSettings['recycle_enable']) && !empty(Config::$modSettings['recycle_board']) && Config::$modSettings['recycle_board'] == $value; break; case 'id_cat': From 943588b26f2f7186a8dee1275bfc84c87b157a3d Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Mon, 4 May 2026 00:48:49 -0600 Subject: [PATCH 4/5] Adds SMF\Board::$selected property Signed-off-by: Jon Stovell --- Sources/Board.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Sources/Board.php b/Sources/Board.php index d08927cddd..9bece3d294 100644 --- a/Sources/Board.php +++ b/Sources/Board.php @@ -341,6 +341,15 @@ class Board implements \ArrayAccess, Routable */ public string $error; + /** + * @var bool + * + * Whether this board is selected in the search form. + * + * Only used during search. + */ + public bool $selected = false; + /************************** * Public static properties **************************/ From d7998208e49bfb0236b726a867b210359b39dd24 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Mon, 4 May 2026 00:49:31 -0600 Subject: [PATCH 5/5] Improves logic for building board tree for the search form Signed-off-by: Jon Stovell --- Sources/Actions/Search.php | 103 +++++++++-------------------- Themes/default/Search.template.php | 2 +- 2 files changed, 32 insertions(+), 73 deletions(-) diff --git a/Sources/Actions/Search.php b/Sources/Actions/Search.php index 41e46ab51a..30cf73a686 100644 --- a/Sources/Actions/Search.php +++ b/Sources/Actions/Search.php @@ -175,91 +175,50 @@ public function execute(): void } // Find all the boards this user is allowed to see. - $request = Db::$db->query( - 'SELECT b.id_cat, c.name AS cat_name, b.id_board, b.name, b.child_level - FROM {db_prefix}boards AS b - LEFT JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat) - WHERE {query_see_board} - AND redirect = {string:empty_string}', - [ - 'empty_string' => '', - ], - identifier: 'order_by_board_order', - ); - Utils::$context['num_boards'] = Db::$db->num_rows($request); + Category::getTree(); + + Utils::$context['num_boards'] = 0; Utils::$context['boards_check_all'] = true; Utils::$context['categories'] = []; - while ($row = Db::$db->fetch_assoc($request)) { - // This category hasn't been set up yet.. - if (!isset(Utils::$context['categories'][$row['id_cat']])) { - Utils::$context['categories'][$row['id_cat']] = [ - 'id' => $row['id_cat'], - 'name' => $row['cat_name'], - 'boards' => [], - ]; - } - - $is_recycle_board = !empty(Config::$modSettings['recycle_enable']) && $row['id_board'] == Config::$modSettings['recycle_board']; - - // Set this board up, and let the template know when it's a child. (indent them..) - Utils::$context['categories'][$row['id_cat']]['boards'][$row['id_board']] = [ - 'id' => $row['id_board'], - 'name' => $row['name'], - 'child_level' => $row['child_level'], - ]; + foreach (Category::$loaded as $category) { + // Clone it so that we can edit it without touching the real data. + $cat = clone $category; - // If user selected some particular boards, is this one of them? - if (!empty(Utils::$context['search_params']['brd'])) { - Utils::$context['categories'][$row['id_cat']]['boards'][$row['id_board']]['selected'] = \in_array($row['id_board'], Utils::$context['search_params']['brd']); - } - // User didn't select any boards, so select all except ignored and recycle boards. - else { - Utils::$context['categories'][$row['id_cat']]['boards'][$row['id_board']]['selected'] = !$is_recycle_board && !\in_array($row['id_board'], User::$me->ignoreboards); - } + // Remove all redirect boards from the its children. + $cat->children = array_filter( + $cat->children, + fn($board) => empty($board->redirect), + ); - // If a board wasn't checked that probably should have been ensure the board selection is selected, yo! - if (!Utils::$context['categories'][$row['id_cat']]['boards'][$row['id_board']]['selected'] && !$is_recycle_board) { - Utils::$context['boards_check_all'] = false; + // Skip empty categories. + if (empty($cat->children)) { + continue; } - } - Db::$db->free_result($request); - Category::sort(Utils::$context['categories']); + // Add the category to the list. + Utils::$context['categories'][$cat->id] = $cat; - // Now, let's sort the list of categories into the boards for templates that like that. - $temp_boards = []; + // Figure out which boards to mark as selected. + foreach ($cat->children as $key => $board) { + Utils::$context['num_boards']++; - foreach (Utils::$context['categories'] as $category) { - $temp_boards[] = [ - 'name' => $category['name'], - 'child_ids' => array_keys($category['boards']), - ]; - $temp_boards = array_merge($temp_boards, array_values($category['boards'])); - - // Include a list of boards per category for easy toggling. - Utils::$context['categories'][$category['id']]['child_ids'] = array_keys($category['boards']); - } - - $max_boards = ceil(\count($temp_boards) / 2); - - if ($max_boards == 1) { - $max_boards = 2; - } - - // Now, alternate them so they can be shown left and right ;). - Utils::$context['board_columns'] = []; - - for ($i = 0; $i < $max_boards; $i++) { - Utils::$context['board_columns'][] = $temp_boards[$i]; + // If user selected some particular boards, is this one of them? + if (!empty(Utils::$context['search_params']['brd'])) { + $board->selected = \in_array($board->id, Utils::$context['search_params']['brd']); + } + // User didn't select any boards, so select all except ignored and recycle boards. + else { + $board->selected = !$board->recycle && !\in_array($board->id, User::$me->ignoreboards); + } - if (isset($temp_boards[$i + $max_boards])) { - Utils::$context['board_columns'][] = $temp_boards[$i + $max_boards]; - } else { - Utils::$context['board_columns'][] = []; + if (!$board->selected && !$board->recycle) { + Utils::$context['boards_check_all'] = false; + } } } + // Searching in a topic? if (!empty($_REQUEST['topic'])) { Utils::$context['search_params']['topic'] = (int) $_REQUEST['topic']; Utils::$context['search_params']['show_complete'] = true; diff --git a/Themes/default/Search.template.php b/Themes/default/Search.template.php index d8fd66ab09..5ec5644827 100644 --- a/Themes/default/Search.template.php +++ b/Themes/default/Search.template.php @@ -172,7 +172,7 @@ function template_main() ', $category['name'], '
    '; - $cat_boards = array_values($category['boards']); + $cat_boards = array_values($category['children']); foreach ($cat_boards as $key => $board) { echo '