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'), '