From 0e583d312a7af9f0e69cff21cbe5ac5a4d825d3b Mon Sep 17 00:00:00 2001 From: albertlast Date: Wed, 9 Sep 2026 06:58:38 +0200 Subject: [PATCH] Limits a print page to one page of posts Every other view that shows a lot of a topic at once is capped. The "All" view refuses a topic longer than enableAllMessages, and a feed takes at most 255 items. The print view took the lot: one query for every message in the topic, each one parsed and held in memory, for anyone who could read the board. It now shows the same number of posts the admin is willing to show in the "All" view, and pages through the rest with a page index of its own. Forums that never show "All" get a limit of 250 rather than none, since 0 there means the link is off and not that a request may render anything it likes. The rest follows from paging: the page index and the text/images links carry the current start, the poll prints once with the first page rather than on top of every page, and an out of range start redirects the way a topic page does. The page links are plain text because the print page loads neither the theme's icon CSS nor its JavaScript, so the usual arrows and the expanding page list would both be dead there. Co-Authored-By: Claude Opus 5 Signed-off-by: albertlast --- Languages/en_US/Help.php | 2 +- Sources/Actions/TopicPrint.php | 90 ++++++++++- Themes/default/Printpage.template.php | 8 +- tests/Integration/Http/TopicPrintTest.php | 187 ++++++++++++++++++++++ tests/Unit/TopicPrintTest.php | 74 +++++++++ 5 files changed, 355 insertions(+), 6 deletions(-) create mode 100644 tests/Integration/Http/TopicPrintTest.php create mode 100644 tests/Unit/TopicPrintTest.php diff --git a/Languages/en_US/Help.php b/Languages/en_US/Help.php index 13354bf72bc..7e08895ac40 100644 --- a/Languages/en_US/Help.php +++ b/Languages/en_US/Help.php @@ -220,7 +220,7 @@ '; $helptxt['topicSummaryPosts'] = 'This allows you to set the number of previous posts shown in the topic summary on the reply page.'; -$helptxt['enableAllMessages'] = 'Set this to the maximum number of posts a topic can have to show the all link. Setting this lower than "Maximum messages to display in a topic page" will simply mean it never gets shown, and setting it too high could slow down your forum.'; +$helptxt['enableAllMessages'] = 'Set this to the maximum number of posts a topic can have to show the all link. Setting this lower than "Maximum messages to display in a topic page" will simply mean it never gets shown, and setting it too high could slow down your forum. The print view of a topic shows this many posts at a time as well, and falls back to a limit of its own when the all link is turned off here.'; $helptxt['allow_guestAccess'] = 'Unchecking this box will stop guests from doing anything but very basic actions on your forum - login, register, password reminder, etc. - on your forum. This is not the same as disallowing guest access to boards.'; $helptxt['userLanguage'] = 'Turning this setting on will allow users to select which language file they use. It will not affect the default selection.'; diff --git a/Sources/Actions/TopicPrint.php b/Sources/Actions/TopicPrint.php index 0d8b30cd2f5..9241161851d 100644 --- a/Sources/Actions/TopicPrint.php +++ b/Sources/Actions/TopicPrint.php @@ -24,6 +24,7 @@ use SMF\Db\DatabaseApi as Db; use SMF\ErrorHandler; use SMF\Lang; +use SMF\PageIndex; use SMF\Parser; use SMF\Poll; use SMF\Routable; @@ -43,6 +44,16 @@ class TopicPrint implements ActionInterface, Routable use ActionSuffixRouter; use ActionTrait; + /***************** + * Class constants + *****************/ + + /** + * The number of posts to show on one print page in forums that never show + * the "All" view. + */ + public const DEFAULT_MAX_POSTS = 250; + /**************** * Public methods ****************/ @@ -97,7 +108,56 @@ public function execute(): void $row = Db::$db->fetch_assoc($request); Db::$db->free_result($request); - if (!empty($row['id_poll'])) { + // Only the posts this user is allowed to see are printed or counted. + $approval_filter = Config::$modSettings['postmod_active'] && !User::$me->allowedTo('approve_posts') ? ' + AND (m.approved = {int:is_approved}' . (User::$me->is_guest ? '' : ' OR m.id_member = {int:current_member}') . ')' : ''; + + $request = Db::$db->query( + 'SELECT COUNT(*) + FROM {db_prefix}messages AS m + WHERE m.id_topic = {int:current_topic}' . $approval_filter, + [ + 'current_topic' => Topic::$topic_id, + 'is_approved' => 1, + 'current_member' => User::$me->id, + ], + ); + list($total_posts) = Db::$db->fetch_row($request); + Db::$db->free_result($request); + + $per_page = $this->getPostsPerPage(); + + Utils::$context['start'] = (int) $_REQUEST['start']; + + $page_index = new PageIndex( + Config::$scripturl . '?action=printpage;topic=' . Topic::$topic_id . '.%1$d' . (isset($_REQUEST['images']) ? ';images' : ''), + Utils::$context['start'], + (int) $total_posts, + $per_page, + true, + true, + // The print page carries its own styles and no scripts, so the + // icons and the expanding page list have to be plain text here. + [ + 'previous_page' => Lang::getTxt('prev', file: 'General'), + 'next_page' => Lang::getTxt('next', file: 'General'), + 'expand_pages' => ' ... ', + ], + ); + + // If the supplied start value was invalid, redirect to the correct one. + if ($_REQUEST['start'] != Utils::$context['start']) { + Utils::redirectexit(\sprintf($page_index->base_url, Utils::$context['start'])); + } + + // There is nothing to navigate when the whole topic fits on one page. + if ($total_posts > $per_page) { + Utils::$context['page_index'] = $page_index; + } + + // The poll belongs to the topic rather than to any of its posts, so it + // is printed once, with the first page. + if (!empty($row['id_poll']) && Utils::$context['start'] === 0) { $poll = Poll::load(Topic::$topic_id, Poll::LOAD_BY_TOPIC); Utils::$context['poll'] = $poll->format(['no_buttons' => true]); } @@ -120,13 +180,15 @@ public function execute(): void 'SELECT subject, poster_time, body, COALESCE(mem.real_name, poster_name) AS poster_name, id_msg FROM {db_prefix}messages AS m LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member) - WHERE m.id_topic = {int:current_topic}' . (Config::$modSettings['postmod_active'] && !User::$me->allowedTo('approve_posts') ? ' - AND (m.approved = {int:is_approved}' . (User::$me->is_guest ? '' : ' OR m.id_member = {int:current_member}') . ')' : '') . ' - ORDER BY m.id_msg', + WHERE m.id_topic = {int:current_topic}' . $approval_filter . ' + ORDER BY m.id_msg + LIMIT {int:per_page} OFFSET {int:start}', [ 'current_topic' => Topic::$topic_id, 'is_approved' => 1, 'current_member' => User::$me->id, + 'per_page' => $per_page, + 'start' => Utils::$context['start'], ], ); Utils::$context['posts'] = []; @@ -215,4 +277,24 @@ public function execute(): void // Set a canonical URL for this page. Utils::$context['canonical_url'] = Config::$scripturl . '?topic=' . Topic::$topic_id . '.0'; } + + /****************** + * Internal methods + ******************/ + + /** + * Works out how many posts belong on a single print page. + * + * A print page holds whole posts, parsed and in memory all at once, so it + * shows no more of them at a time than the admin is willing to show in the + * "All" view of a topic. + * + * @return int The maximum number of posts on one print page. + */ + protected function getPostsPerPage(): int + { + $per_page = (int) (Config::$modSettings['enableAllMessages'] ?? 0); + + return $per_page > 0 ? $per_page : self::DEFAULT_MAX_POSTS; + } } diff --git a/Themes/default/Printpage.template.php b/Themes/default/Printpage.template.php index 056916e9281..244e2667b3d 100644 --- a/Themes/default/Printpage.template.php +++ b/Themes/default/Printpage.template.php @@ -224,12 +224,18 @@ function template_print_below() */ function template_print_options() { - $url_text = Config::$scripturl . '?action=printpage;topic=' . Topic::$topic_id . '.0'; + $url_text = Config::$scripturl . '?action=printpage;topic=' . Topic::$topic_id . '.' . Utils::$context['start']; $url_images = $url_text . ';images'; echo '