diff --git a/Sources/Actions/Admin/Boards.php b/Sources/Actions/Admin/Boards.php index 615c24eb5d..7c9968f59e 100644 --- a/Sources/Actions/Admin/Boards.php +++ b/Sources/Actions/Admin/Boards.php @@ -458,7 +458,7 @@ public function editBoard(): void 'is_new' => true, 'name' => Lang::getTxt('mboards_new_board_name', file: 'ManageBoards'), 'description' => '', - 'count_posts' => true, + 'posts_count' => true, 'posts' => 0, 'topics' => 0, 'theme' => 0, diff --git a/Sources/Actions/Admin/Maintenance.php b/Sources/Actions/Admin/Maintenance.php index 291228cf16..2587a57450 100644 --- a/Sources/Actions/Admin/Maintenance.php +++ b/Sources/Actions/Admin/Maintenance.php @@ -1455,9 +1455,12 @@ public function recountPosts(): void 'SELECT COUNT(DISTINCT m.id_member) FROM {db_prefix}messages AS m JOIN {db_prefix}boards AS b on m.id_board = b.id_board - WHERE m.id_member != 0 - AND b.count_posts = 0', + WHERE m.id_member != {int:zero} + AND b.posts_count = {int:one} + AND m.approved = {int:one}', [ + 'zero' => 0, + 'one' => 1, ], ); @@ -1474,7 +1477,8 @@ public function recountPosts(): void FROM {db_prefix}messages AS m INNER JOIN {db_prefix}boards AS b ON m.id_board = b.id_board WHERE m.id_member != {int:zero} - AND b.count_posts = {int:zero} + AND b.posts_count = {int:one} + AND m.approved = {int:one} ' . (!empty(Config::$modSettings['recycle_enable']) ? ' AND b.id_board != {int:recycle}' : '') . ' GROUP BY m.id_member LIMIT {int:start}, {int:number}', @@ -1483,6 +1487,7 @@ public function recountPosts(): void 'number' => $increment, 'recycle' => Config::$modSettings['recycle_board'], 'zero' => 0, + 'one' => 1, ], ); $total_rows = Db::$db->num_rows($request); @@ -1492,11 +1497,8 @@ public function recountPosts(): void Db::$db->query( 'UPDATE {db_prefix}members SET posts = {int:posts} - WHERE id_member = {int:row}', - [ - 'row' => $row['id_member'], - 'posts' => $row['posts'], - ], + WHERE id_member = {int:id_member}', + $row, ); } Db::$db->free_result($request); @@ -1517,27 +1519,29 @@ public function recountPosts(): void // final steps ... made more difficult since we don't yet support sub-selects on joins // place all members who have posts in the message table in a temp table + Db::$db->query('DROP TABLE IF EXISTS {db_prefix}tmp_maint_recountposts'); + $createTemporary = Db::$db->query( - 'CREATE TEMPORARY TABLE {db_prefix}tmp_maint_recountposts ( - id_member mediumint(8) unsigned NOT NULL default {string:string_zero}, - PRIMARY KEY (id_member) - ) + 'CREATE TEMPORARY TABLE {db_prefix}tmp_maint_recountposts AS SELECT m.id_member FROM {db_prefix}messages AS m INNER JOIN {db_prefix}boards AS b ON m.id_board = b.id_board WHERE m.id_member != {int:zero} - AND b.count_posts = {int:zero} + AND b.posts_count = {int:one} + AND m.approved = {int:one} ' . (!empty(Config::$modSettings['recycle_enable']) ? ' AND b.id_board != {int:recycle}' : '') . ' GROUP BY m.id_member', [ 'zero' => 0, - 'string_zero' => '0', + 'one' => 1, 'db_error_skip' => true, 'recycle' => !empty(Config::$modSettings['recycle_board']) ? Config::$modSettings['recycle_board'] : 0, ], ) !== false; if ($createTemporary) { + Db::$db->add_index('{db_prefix}tmp_maint_recountposts', ['type' => 'primary', 'columns' => ['id_member']]); + // outer join the members table on the temporary table finding the members that have a post count but no posts in the message table $request = Db::$db->query( 'SELECT mem.id_member, mem.posts @@ -1556,9 +1560,9 @@ public function recountPosts(): void Db::$db->query( 'UPDATE {db_prefix}members SET posts = {int:zero} - WHERE id_member = {int:row}', + WHERE id_member = {int:id_member}', [ - 'row' => $row['id_member'], + 'id_member' => $row['id_member'], 'zero' => 0, ], ); @@ -2077,14 +2081,14 @@ public static function reattributePosts(int $memID, ?string $email = null, ?stri $request = Db::$db->query( 'SELECT COUNT(*) FROM {db_prefix}messages AS m - INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board AND b.count_posts = {int:count_posts}) + INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board AND b.posts_count = {int:posts_count}) WHERE m.id_member = {int:guest_id} AND m.approved = {int:is_approved}' . (!empty($recycle_board) ? ' AND m.id_board != {int:recycled_board}' : '') . (empty($email) ? '' : ' AND m.poster_email = {string:email_address}') . (empty($membername) ? '' : ' AND m.poster_name = {string:member_name}'), [ - 'count_posts' => 0, + 'posts_count' => 1, 'guest_id' => 0, 'email_address' => $email, 'member_name' => $membername, diff --git a/Sources/Actions/Admin/Reports.php b/Sources/Actions/Admin/Reports.php index 48033cc974..1bbe23615b 100644 --- a/Sources/Actions/Admin/Reports.php +++ b/Sources/Actions/Admin/Reports.php @@ -261,7 +261,7 @@ public function boards(): void 'redirect' => Lang::getTxt('board_redirect', file: 'Reports'), 'num_topics' => Lang::getTxt('board_num_topics', file: 'Reports'), 'num_posts' => Lang::getTxt('board_num_posts', file: 'Reports'), - 'count_posts' => Lang::getTxt('board_count_posts', file: 'Reports'), + 'posts_count' => Lang::getTxt('board_count_posts', file: 'Reports'), 'theme' => Lang::getTxt('board_theme', file: 'Reports'), 'override_theme' => Lang::getTxt('board_override_theme', file: 'Reports'), 'profile' => Lang::getTxt('board_profile', file: 'Reports'), @@ -302,7 +302,7 @@ public function boards(): void 'redirect' => $board->redirect, 'num_posts' => $board->posts, 'num_topics' => $board->topics, - 'count_posts' => Lang::getTxt(empty($board->count_posts) ? 'yes' : 'no', file: 'General'), + 'posts_count' => Lang::getTxt($board->posts_count ? 'yes' : 'no', file: 'General'), 'theme' => Utils::$context['themes'][$board->theme] ?? Lang::getTxt('none', file: 'General'), 'profile' => Utils::$context['profiles'][$board->profile]['name'], 'override_theme' => Lang::getTxt($board->override_theme ? 'yes' : 'no', file: 'General'), diff --git a/Sources/Actions/Profile/StatPanel.php b/Sources/Actions/Profile/StatPanel.php index 9de24504b9..27585275b5 100644 --- a/Sources/Actions/Profile/StatPanel.php +++ b/Sources/Actions/Profile/StatPanel.php @@ -112,14 +112,14 @@ public function execute(): void FROM {db_prefix}messages AS m INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board) WHERE m.id_member = {int:current_member} - AND b.count_posts = {int:count_enabled} + AND b.posts_count = {int:count_enabled} AND {query_see_board} GROUP BY b.id_board ORDER BY message_count DESC LIMIT 10', [ 'current_member' => Profile::$member->id, - 'count_enabled' => 0, + 'count_enabled' => 1, ], ); diff --git a/Sources/Actions/QuickModeration.php b/Sources/Actions/QuickModeration.php index aeeec4d3ab..2d9761084c 100644 --- a/Sources/Actions/QuickModeration.php +++ b/Sources/Actions/QuickModeration.php @@ -671,7 +671,7 @@ protected function doMove(): void $countPosts = []; $request = Db::$db->query( - 'SELECT t.id_topic, t.id_board, b.count_posts + 'SELECT t.id_topic, t.id_board, b.posts_count FROM {db_prefix}topics AS t LEFT JOIN {db_prefix}boards AS b ON (t.id_board = b.id_board) WHERE t.id_topic IN ({array_int:move_topic_ids})' . (!empty(Board::$info->id) && !User::$me->allowedTo('move_any') ? ' @@ -692,7 +692,7 @@ protected function doMove(): void } // Does this topic's board count the posts or not? - $countPosts[(int) $row['id_topic']] = empty($row['count_posts']); + $countPosts[(int) $row['id_topic']] = !empty($row['posts_count']); if (!isset($moveTos[$to])) { $moveTos[$to] = []; @@ -716,7 +716,7 @@ protected function doMove(): void if (!empty($moveTos)) { $topicRecounts = []; $request = Db::$db->query( - 'SELECT id_board, count_posts + 'SELECT id_board, posts_count FROM {db_prefix}boards WHERE id_board IN ({array_int:move_boards})', [ @@ -725,7 +725,7 @@ protected function doMove(): void ); while ($row = Db::$db->fetch_assoc($request)) { - $cp = empty($row['count_posts']); + $cp = !empty($row['posts_count']); // Go through all the topics that are being moved to this board. foreach ($moveTos[(int) $row['id_board']] as $topic) { @@ -740,7 +740,7 @@ protected function doMove(): void Db::$db->free_result($request); if (!empty($topicRecounts)) { - $members = []; + $adjustments = []; // Get all the members who have posted in the moved topics. $request = Db::$db->query( @@ -754,22 +754,22 @@ protected function doMove(): void ); while ($row = Db::$db->fetch_assoc($request)) { - if (!isset($members[$row['id_member']])) { - $members[(int) $row['id_member']] = 0; + if (!isset($adjustments[$row['id_member']])) { + $adjustments[(int) $row['id_member']] = 0; } if ($topicRecounts[(int) $row['id_topic']] === '+') { - $members[(int) $row['id_member']]++; + $adjustments[(int) $row['id_member']]++; } else { - $members[(int) $row['id_member']]--; + $adjustments[(int) $row['id_member']]--; } - - $members[(int) $row['id_member']] = max(0, $members[(int) $row['id_member']]); } Db::$db->free_result($request); // And now update the member's post counts. - foreach ($members as $id => $post_adj) { + $members = []; + + foreach ($adjustments as $id => $post_adj) { $members[$id] = current(User::load($id, dataset: UserDataset::Minimal)); if ($members[$id] instanceof User) { diff --git a/Sources/Actions/TopicMove2.php b/Sources/Actions/TopicMove2.php index 4e46ff4cc9..e54b324ea1 100644 --- a/Sources/Actions/TopicMove2.php +++ b/Sources/Actions/TopicMove2.php @@ -106,7 +106,7 @@ public function execute(): void // Make sure they can see the board they are trying to move to (and get whether posts count in the target board). $request = Db::$db->query( - 'SELECT b.count_posts, b.name, m.subject + 'SELECT b.posts_count, b.name, m.subject FROM {db_prefix}boards AS b INNER JOIN {db_prefix}topics AS t ON (t.id_topic = {int:current_topic}) INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_first_msg) @@ -221,25 +221,14 @@ public function execute(): void $posterOptions = [ 'id' => User::$me->id, - 'update_post_count' => empty($pcounter), + 'update_post_count' => (bool) Board::$info->posts_count, ]; Msg::create($msgOptions, $topicOptions, $posterOptions); } - $request = Db::$db->query( - 'SELECT count_posts - FROM {db_prefix}boards - WHERE id_board = {int:current_board} - LIMIT 1', - [ - 'current_board' => Board::$info->id, - ], - ); - list($pcounter_from) = Db::$db->fetch_row($request); - Db::$db->free_result($request); - - if ($pcounter_from != $pcounter) { + // If one of the boards counts posts and the other doesn't, we have more work to do. + if (Board::$info->posts_count != $pcounter) { $posters = []; $request = Db::$db->query( @@ -259,7 +248,7 @@ public function execute(): void } // The board we're moving from counted posts, but not to. - if (empty($pcounter_from)) { + if (Board::$info->posts_count) { $posters[$row['id_member']]--; } // The reverse: from didn't, to did. diff --git a/Sources/Actions/TopicRestore.php b/Sources/Actions/TopicRestore.php index 35abc76fa1..eb45288c6e 100644 --- a/Sources/Actions/TopicRestore.php +++ b/Sources/Actions/TopicRestore.php @@ -72,7 +72,7 @@ public function execute(): void // Get the id_previous_board and id_previous_topic. $request = Db::$db->query( 'SELECT m.id_topic, m.id_msg, m.id_board, m.subject, m.id_member, t.id_previous_board, t.id_previous_topic, - t.id_first_msg, b.count_posts, COALESCE(pt.id_board, 0) AS possible_prev_board + t.id_first_msg, b.posts_count, COALESCE(pt.id_board, 0) AS possible_prev_board FROM {db_prefix}messages AS m INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic) INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board) @@ -106,7 +106,7 @@ public function execute(): void if (empty($actioned_messages[$row['id_previous_topic']])) { $actioned_messages[$row['id_previous_topic']] = [ 'msgs' => [], - 'count_posts' => $row['count_posts'], + 'posts_count' => $row['posts_count'], 'subject' => $row['subject'], 'previous_board' => $row['id_previous_board'], 'possible_prev_board' => $row['possible_prev_board'], @@ -212,17 +212,17 @@ public function execute(): void // Lets see if the board that we are returning to has post count enabled. $request2 = Db::$db->query( - 'SELECT count_posts + 'SELECT posts_count FROM {db_prefix}boards WHERE id_board = {int:board}', [ 'board' => $row['id_previous_board'], ], ); - list($count_posts) = Db::$db->fetch_row($request2); + list($posts_count) = Db::$db->fetch_row($request2); Db::$db->free_result($request2); - if (empty($count_posts)) { + if (!empty($posts_count)) { $members = []; // Lets get the members that need their post count restored. @@ -313,7 +313,7 @@ protected static function mergePosts(array|int $msgs, int $from_topic, int $targ // Get some target topic and board stats. $request = Db::$db->query( - 'SELECT t.id_board, t.id_first_msg, t.num_replies, t.unapproved_posts, b.count_posts + 'SELECT t.id_board, t.id_first_msg, t.num_replies, t.unapproved_posts, b.posts_count FROM {db_prefix}topics AS t INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board) WHERE t.id_topic = {int:target_topic}', @@ -321,11 +321,11 @@ protected static function mergePosts(array|int $msgs, int $from_topic, int $targ 'target_topic' => $target_topic, ], ); - list($target_board, $target_first_msg, $target_replies, $target_unapproved_posts, $count_posts) = Db::$db->fetch_row($request); + list($target_board, $target_first_msg, $target_replies, $target_unapproved_posts, $posts_count) = Db::$db->fetch_row($request); Db::$db->free_result($request); // Lets see if the board that we are returning to has post count enabled. - if (empty($count_posts)) { + if (!empty($posts_count)) { // Lets get the members that need their post count restored. $members = User::loadCustom( query_customizations: [ diff --git a/Sources/Board.php b/Sources/Board.php index 17543250c3..91d8f55c11 100644 --- a/Sources/Board.php +++ b/Sources/Board.php @@ -254,7 +254,7 @@ class Board implements \ArrayAccess, Routable * * Whether posts in this board count toward a user's total post count. */ - public bool $count_posts = true; + public bool $posts_count = true; /** * @var bool @@ -423,7 +423,6 @@ class Board implements \ArrayAccess, Routable 'id_theme' => 'theme', 'board_theme' => 'theme', 'id_profile' => 'profile', - 'posts_count' => 'count_posts', 'href' => 'url', 'id_last_msg' => 'last_msg', 'id_msg_updated' => 'msg_updated', @@ -434,6 +433,7 @@ class Board implements \ArrayAccess, Routable // Initial exclamation mark means inverse of the property. 'is_read' => '!new', + 'count_posts' => '!posts_count', ]; /**************************** @@ -470,7 +470,7 @@ class Board implements \ArrayAccess, Routable 'profile', 'redirect', 'recycle', - 'count_posts', + 'posts_count', 'cur_topic_approved', 'cur_topic_starter', ], @@ -1275,7 +1275,7 @@ public static function modify(int $board_id, array &$boardOptions): void $board->profile = (int) ($boardOptions['profile'] ?? $board->profile ?? 1); // Boolean properties. - $board->count_posts = !empty($boardOptions['posts_count'] ?? $board->count_posts ?? true); + $board->posts_count = !empty($boardOptions['posts_count'] ?? $board->posts_count ?? true); $board->override_theme = !empty($boardOptions['override_theme'] ?? $board->override_theme ?? false); // Array properties. @@ -2328,7 +2328,7 @@ protected function loadBoardInfo(): void break; case 'override_theme': - case 'count_posts': + case 'posts_count': $props[$key] = !empty($value); break; @@ -2522,7 +2522,7 @@ protected function saveNew(): void 'description' => 'string', 'num_topics' => 'int', 'num_posts' => 'int', - 'count_posts' => 'int', + 'posts_count' => 'int', 'id_theme' => 'int', 'override_theme' => 'int', 'unapproved_posts' => 'int', @@ -2544,7 +2544,7 @@ protected function saveNew(): void $this->description, $this->num_topics, $this->num_posts, - (int) $this->count_posts, + (int) $this->posts_count, $this->theme, (int) $this->override_theme, $this->unapproved_posts, @@ -2642,7 +2642,7 @@ protected function saveExisting(int $level): void 'id_profile = {int:profile}', 'name = {string:board_name}', 'description = {string:board_description}', - 'count_posts = {int:count_posts}', + 'posts_count = {int:posts_count}', 'id_theme = {int:board_theme}', 'override_theme = {int:override_theme}', 'redirect = {string:redirect}', @@ -2659,15 +2659,35 @@ protected function saveExisting(int $level): void 'profile' => $this->profile, 'board_name' => $this->name, 'board_description' => $this->description, - 'count_posts' => (int) $this->count_posts, + 'posts_count' => (int) $this->posts_count, 'board_theme' => $this->theme, 'override_theme' => (int) $this->override_theme, 'redirect' => $this->redirect, ], ); + // Old mods would have expected $params['count_posts'], which had + // an inverted value (i.e. 0 = true, 1 = false). + if (!empty(Config::$backward_compatibility)) { + $params['count_posts'] = $count_posts = (int) !$this->posts_count; + } + // Do any hooks want to add or adjust anything? IntegrationHook::call('integrate_modify_board', [$this->id, $this->internal_data['boardOptions'] ?? [], &$set, &$params]); + + // Clean up the backward compatibility changes. + if (!empty(Config::$backward_compatibility)) { + // If posts_count did not change, but count_posts did, then + // sync the change to count_posts back to posts_count. + if ( + (int) $params['posts_count'] === (int) $this->posts_count + && (int) $params['count_posts'] !== (int) $count_posts + ) { + $params['posts_count'] = (int) empty($params['count_posts']); + } + + unset($params['count_posts']); + } } // Perform the update. diff --git a/Sources/Category.php b/Sources/Category.php index 1c11dbd096..8b0b026d2e 100644 --- a/Sources/Category.php +++ b/Sources/Category.php @@ -698,7 +698,7 @@ public static function getTree(): void 'COALESCE(b.id_board, 0) AS id_board', 'b.name', 'b.description', 'b.id_parent', 'b.child_level', 'b.board_order', 'b.redirect', 'b.member_groups', 'b.deny_member_groups', 'b.id_profile', - 'b.id_theme', 'b.override_theme', 'b.count_posts', 'b.num_posts', + 'b.id_theme', 'b.override_theme', 'b.posts_count', 'b.num_posts', 'b.num_topics', 'c.id_cat', 'c.cat_order', 'c.can_collapse', 'c.name AS cat_name', 'c.description AS cat_desc', ]; diff --git a/Sources/Db/APIs/MySQL.php b/Sources/Db/APIs/MySQL.php index d7a3e47497..a1fd12b39c 100644 --- a/Sources/Db/APIs/MySQL.php +++ b/Sources/Db/APIs/MySQL.php @@ -267,6 +267,8 @@ public function quote(string $db_string, array $db_values, ?object $connection = ); } + $db_string = $this->backcompatQuoteFixes($db_string); + return $db_string; } @@ -390,6 +392,9 @@ public function insert(string $method, string $table, array $columns, array $dat } } + // Apply any adjustments needed for backward compatibility. + [$columns, $data, $keys] = $this->backcompatInsertFixes($table, $columns, $data, $keys); + // Create the mold for a single row insert. $insertData = '('; @@ -2923,6 +2928,103 @@ protected function replacement__callback(array $matches, array $db_values, objec throw new \Exception(); } + /** + * Helper for $this->quote() that makes any changes to the query string that + * might be required for backward compatibility support. + * + * Assumes $db_string has already been processed by replacement_callback(). + * + * @param string $db_string The database query string. + * @return string Possibly modified version of $db_string. + */ + protected function backcompatQuoteFixes(string $db_string): string + { + if (empty(Config::$backward_compatibility)) { + return $db_string; + } + + // Prior to SMF 3.0, the boards table contained a 'count_posts' column + // that used inverted logic (i.e. 0 = true, 1 = false). That column was + // replaced in 3.0 with a 'posts_count' column that uses normal logic + // (i.e. 0 = false, 1 = true). This code detects references to the old + // column and replaces them with references to the new column. + if ( + str_contains($db_string, 'count_posts') + && preg_match('/\b' . preg_quote(Config::$db_prefix) . 'boards\b(?:\s+AS\s+(\w+))?/i', $db_string, $matches) + ) { + $old_col = (!empty($matches[1]) ? '(?:' . $matches[1] . '\.)?' : '') . 'count_posts'; + $new_col = (!empty($matches[1]) ? $matches[1] . '.' : '') . 'posts_count'; + + $db_string = preg_replace_callback_array( + [ + '/(\bSELECT\b(?:.(?!\bFROM\b))*)((? fn($m) => $m[1] . $new_col . ', (1 - ' . $new_col . ') AS ' . md5('count_posts'), + '/(?)?|=|>=?)\s*([01])\b/' => function ($m) use ($new_col) { + $m[1] = match ($m[1]) { + '>' => '<', + '>=' => '<=', + '<' => '>', + '<=' => '>=', + default => $m[1], + }; + + return $new_col . ' ' . $m[1] . ' ' . ((int) !$m[2]); + }, + '/(? fn($m) => $new_col, + '/' . md5('count_posts') . '/' => fn($m) => 'count_posts', + ], + $db_string, + ); + } + + return $db_string; + } + + /** + * Helper for $this->insert() that makes any changes to the columns, data, + * and/or keys that might be required for backward compatibility support. + * + * @param string $table The table. + * @param array $columns Array of the columns we're inserting the data into. + * Should contain 'column' => 'datatype' pairs. + * @param array $data Rows of data to insert. Each element of $data must + * be an array of values corresponding to $columns. + * @param array $keys The keys for the table. + * @return array Updated versions $columns, $data, and $keys. + */ + protected function backcompatInsertFixes(string $table, array $columns, array $data, array $keys): array + { + if (empty(Config::$backward_compatibility)) { + return [$columns, $data, $keys]; + } + + // Prior to SMF 3.0, the boards table contained a 'count_posts' column + // that used inverted logic (i.e. 0 = true, 1 = false). That column was + // replaced in 3.0 with a 'posts_count' column that uses normal logic + // (i.e. 0 = false, 1 = true). This code detects references to the old + // column and replaces them with references to the new column. + if ($table === $this->prefix . 'boards') { + if (isset($columns['count_posts'])) { + $pos = array_search('count_posts', array_keys($columns)); + + foreach ($data as $row_num => $row) { + $data[$row_num][$pos] = (int) !$row[$pos]; + } + + $columns = array_merge( + \array_slice($columns, 0, $pos), + ['posts_count' => 'int'], + \array_slice($columns, $pos + 1), + ); + } + + if (\in_array('count_posts', $keys)) { + $keys[array_search('count_posts', $keys)] = 'posts_count'; + } + } + + return [$columns, $data, $keys]; + } + /** * This function tries to work out additional error information from a back trace. * diff --git a/Sources/Db/APIs/PostgreSQL.php b/Sources/Db/APIs/PostgreSQL.php index 6c4c0fa6a9..8184ed95f1 100644 --- a/Sources/Db/APIs/PostgreSQL.php +++ b/Sources/Db/APIs/PostgreSQL.php @@ -326,6 +326,8 @@ public function quote(string $db_string, array $db_values, ?object $connection = ); } + $db_string = $this->backcompatQuoteFixes($db_string); + return $db_string; } @@ -463,6 +465,9 @@ public function insert(string $method, string $table, array $columns, array $dat $with_returning = true; } + // Apply any adjustments needed for backward compatibility. + [$columns, $data, $keys] = $this->backcompatInsertFixes($table, $columns, $data, $keys); + if (!empty($data)) { // Create the mold for a single row insert. $insertData = '('; @@ -1377,8 +1382,6 @@ public function add_index(string $table_name, array $index_info, array $paramete } // MySQL you can do a "column_name (length)", postgresql does not allow this. Strip it. - $cols = $this->list_columns($table_name, true); - foreach ($index_info['columns'] as &$c) { if (\is_array($c)) { $c = $c['name'] . (isset($c['opclass']) ? ' ' . $c['opclass'] : ''); @@ -2797,6 +2800,103 @@ protected function replacement__callback(array $matches, array $db_values, objec throw new \Exception(); } + /** + * Helper for $this->quote() that makes any changes to the query string that + * might be required for backward compatibility support. + * + * Assumes $db_string has already been processed by replacement_callback(). + * + * @param string $db_string The database query string. + * @return string Possibly modified version of $db_string. + */ + protected function backcompatQuoteFixes(string $db_string): string + { + if (empty(Config::$backward_compatibility)) { + return $db_string; + } + + // Prior to SMF 3.0, the boards table contained a 'count_posts' column + // that used inverted logic (i.e. 0 = true, 1 = false). That column was + // replaced in 3.0 with a 'posts_count' column that uses normal logic + // (i.e. 0 = false, 1 = true). This code detects references to the old + // column and replaces them with references to the new column. + if ( + str_contains($db_string, 'count_posts') + && preg_match('/\b' . preg_quote(Config::$db_prefix) . 'boards\b(?:\s+AS\s+(\w+))?/i', $db_string, $matches) + ) { + $old_col = (!empty($matches[1]) ? '(?:' . $matches[1] . '\.)?' : '') . 'count_posts'; + $new_col = (!empty($matches[1]) ? $matches[1] . '.' : '') . 'posts_count'; + + $db_string = preg_replace_callback_array( + [ + '/(\bSELECT\b(?:.(?!\bFROM\b))*)((? fn($m) => $m[1] . $new_col . ', (1 - ' . $new_col . ') AS ' . md5('count_posts'), + '/(?)?|=|>=?)\s*([01])\b/' => function ($m) use ($new_col) { + $m[1] = match ($m[1]) { + '>' => '<', + '>=' => '<=', + '<' => '>', + '<=' => '>=', + default => $m[1], + }; + + return $new_col . ' ' . $m[1] . ' ' . ((int) !$m[2]); + }, + '/(? fn($m) => $new_col, + '/' . md5('count_posts') . '/' => fn($m) => 'count_posts', + ], + $db_string, + ); + } + + return $db_string; + } + + /** + * Helper for $this->insert() that makes any changes to the columns, data, + * and/or keys that might be required for backward compatibility support. + * + * @param string $table The table. + * @param array $columns Array of the columns we're inserting the data into. + * Should contain 'column' => 'datatype' pairs. + * @param array $data Rows of data to insert. Each element of $data must + * be an array of values corresponding to $columns. + * @param array $keys The keys for the table. + * @return array Updated versions $columns, $data, and $keys. + */ + protected function backcompatInsertFixes(string $table, array $columns, array $data, array $keys): array + { + if (empty(Config::$backward_compatibility)) { + return [$columns, $data, $keys]; + } + + // Prior to SMF 3.0, the boards table contained a 'count_posts' column + // that used inverted logic (i.e. 0 = true, 1 = false). That column was + // replaced in 3.0 with a 'posts_count' column that uses normal logic + // (i.e. 0 = false, 1 = true). This code detects references to the old + // column and replaces them with references to the new column. + if ($table === $this->prefix . 'boards') { + if (isset($columns['count_posts'])) { + $pos = array_search('count_posts', array_keys($columns)); + + foreach ($data as $row_num => $row) { + $data[$row_num][$pos] = (int) !$row[$pos]; + } + + $columns = array_merge( + \array_slice($columns, 0, $pos), + ['posts_count' => 'int'], + \array_slice($columns, $pos + 1), + ); + } + + if (\in_array('count_posts', $keys)) { + $keys[array_search('count_posts', $keys)] = 'posts_count'; + } + } + + return [$columns, $data, $keys]; + } + /** * This function tries to work out additional error information from a back trace. * diff --git a/Sources/Db/DatabaseApiInterface.php b/Sources/Db/DatabaseApiInterface.php index 254c9508c8..5c7b1c78e5 100644 --- a/Sources/Db/DatabaseApiInterface.php +++ b/Sources/Db/DatabaseApiInterface.php @@ -89,7 +89,7 @@ public function free_result(object $result): bool; * returns the resulting IDs. * * @param string $method INSERT or REPLACE. - * @param string $table The table (only used for Postgres). + * @param string $table The table. * @param array $columns Array of the columns we're inserting the data into. * Should contain 'column' => 'datatype' pairs. * @param array $data Rows of data to insert. Each element of $data must diff --git a/Sources/Db/Schema/v3_0/Boards.php b/Sources/Db/Schema/v3_0/Boards.php index 136fbab854..a41be8cae6 100644 --- a/Sources/Db/Schema/v3_0/Boards.php +++ b/Sources/Db/Schema/v3_0/Boards.php @@ -148,11 +148,11 @@ public function __construct() not_null: true, default: 0, ), - 'count_posts' => new Column( - name: 'count_posts', + 'posts_count' => new Column( + name: 'posts_count', type: 'tinyint', not_null: true, - default: 0, + default: 1, ), 'id_theme' => new Column( name: 'id_theme', diff --git a/Sources/Maintenance/Migration/v3_0/BoardPostsCount.php b/Sources/Maintenance/Migration/v3_0/BoardPostsCount.php new file mode 100644 index 0000000000..082ec0ec81 --- /dev/null +++ b/Sources/Maintenance/Migration/v3_0/BoardPostsCount.php @@ -0,0 +1,64 @@ +getCurrentStructure(); + + return isset($existing_structure['columns']['count_posts']); + } + + /** + * + */ + public function execute(): bool + { + $table = new Schema\v3_0\Boards(); + $table->addColumn($table->columns['posts_count']); + + $this->query( + 'UPDATE {db_prefix}boards + SET posts_count = CASE WHEN count_posts = 0 THEN 1 ELSE 0 END', + ); + + $table->dropColumn('count_posts'); + + return true; + } +} diff --git a/Sources/Maintenance/Tools/Upgrade.php b/Sources/Maintenance/Tools/Upgrade.php index 903517bd1f..a18d138e8e 100644 --- a/Sources/Maintenance/Tools/Upgrade.php +++ b/Sources/Maintenance/Tools/Upgrade.php @@ -180,6 +180,7 @@ class Upgrade extends ToolsBase implements ToolsInterface Migration\v3_0\MailType::class, Migration\v3_0\RemoveCookieTime::class, Migration\v3_0\PermissionChanges::class, + Migration\v3_0\BoardPostsCount::class, ], ]; diff --git a/Sources/Msg.php b/Sources/Msg.php index e4c663d824..a8d2e6fb42 100644 --- a/Sources/Msg.php +++ b/Sources/Msg.php @@ -1370,7 +1370,7 @@ public static function approve(array|int $msgs, bool $approve = true, bool $noti $request = Db::$db->query( 'SELECT m.id_msg, m.approved, m.id_topic, m.id_board, t.id_first_msg, t.id_last_msg, m.body, m.subject, COALESCE(mem.real_name, m.poster_name) AS poster_name, m.id_member, - t.approved AS topic_approved, b.count_posts + t.approved AS topic_approved, b.posts_count FROM {db_prefix}messages AS m INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic) INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board) @@ -1459,7 +1459,7 @@ public static function approve(array|int $msgs, bool $approve = true, bool $noti $board_changes[$row['id_board']]['posts'] += $approve ? 1 : -1; // Post count for the user? - if ($row['id_member'] && empty($row['count_posts'])) { + if ($row['id_member'] && !empty($row['posts_count'])) { $member_post_changes[$row['id_member']] = isset($member_post_changes[$row['id_member']]) ? $member_post_changes[$row['id_member']] + 1 : 1; } } @@ -1848,7 +1848,7 @@ public static function remove(int $message, bool $decreasePostCount = true): boo m.id_member, m.icon, m.poster_time, m.subject, m.body, m.approved, t.id_topic, t.id_first_msg, t.id_last_msg, t.num_replies, t.id_board, t.id_member_started AS id_member_poster, - b.count_posts + b.posts_count FROM {db_prefix}messages AS m INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic) INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board) @@ -1867,6 +1867,12 @@ public static function remove(int $message, bool $decreasePostCount = true): boo $row = Db::$db->fetch_assoc($request); Db::$db->free_result($request); + // Old mods would have expected $row['count_posts'], which had + // an inverted value (i.e. 0 = true, 1 = false). + if (!empty(Config::$backward_compatibility)) { + $row['count_posts'] = (int) empty($row['posts_count']); + } + // Give mods a heads-up before we do anything. IntegrationHook::call('integrate_pre_remove_message', [$message, $decreasePostCount, $row]); @@ -2236,7 +2242,7 @@ public static function remove(int $message, bool $decreasePostCount = true): boo // If the poster was registered and the board this message was on incremented // the member's posts when it was posted, decrease his or her post count. - if (!empty($row['id_member']) && $decreasePostCount && empty($row['count_posts']) && $row['approved']) { + if (!empty($row['id_member']) && $decreasePostCount && !empty($row['posts_count']) && $row['approved']) { $member = current(User::load((int) $row['id_member'], dataset: UserDataset::Minimal)); $member->posts--; $member->save(); @@ -2354,7 +2360,7 @@ public static function parseRoute(array $route, array $params = []): array * @param array $params Parameters to substitute into query text. * @param array $joins Zero or more *complete* JOIN clauses. * E.g.: 'LEFT JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat)' - * Note that 'FROM {db_prefix}boards AS b' is always part of the query. + * Note that 'FROM {db_prefix}messages AS m' is always part of the query. * @param array $where Zero or more conditions for the WHERE clause. * Conditions will be placed in parentheses and concatenated with AND. * If this is left empty, no WHERE clause will be used. diff --git a/Sources/Topic.php b/Sources/Topic.php index 7cc629e4e5..67803a8689 100644 --- a/Sources/Topic.php +++ b/Sources/Topic.php @@ -1516,11 +1516,11 @@ public static function remove(array|int $topics, bool $decreasePostCount = true, INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board) WHERE m.id_topic IN ({array_int:topics})' . (!empty($recycle_board) ? ' AND m.id_board != {int:recycled_board}' : '') . ' - AND b.count_posts = {int:do_count_posts} + AND b.posts_count = {int:do_count_posts} AND m.approved = {int:is_approved} GROUP BY m.id_member', [ - 'do_count_posts' => 0, + 'do_count_posts' => 1, 'recycled_board' => $recycle_board, 'topics' => $topics, 'is_approved' => 1, diff --git a/Themes/default/ManageBoards.template.php b/Themes/default/ManageBoards.template.php index 98fb35c526..5257507576 100644 --- a/Themes/default/ManageBoards.template.php +++ b/Themes/default/ManageBoards.template.php @@ -522,7 +522,7 @@ function template_modify_board() ', Lang::getTxt('mboards_count_posts_desc', file: 'ManageBoards'), '
- +
';