Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Languages/en_US/Help.php
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@
</ul>';

$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 <em>maximum</em> number of posts a topic can have to show the <em>all</em> link. Setting this lower than &quot;Maximum messages to display in a topic page&quot; will simply mean it never gets shown, and setting it too high could slow down your forum.';
$helptxt['enableAllMessages'] = 'Set this to the <em>maximum</em> number of posts a topic can have to show the <em>all</em> link. Setting this lower than &quot;Maximum messages to display in a topic page&quot; 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 <em>all</em> 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.';
Expand Down
90 changes: 86 additions & 4 deletions Sources/Actions/TopicPrint.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
****************/
Expand Down Expand Up @@ -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]);
}
Expand All @@ -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'] = [];
Expand Down Expand Up @@ -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;
}
}
8 changes: 7 additions & 1 deletion Themes/default/Printpage.template.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 '
<div class="print_options">';

// Long topics are printed a page at a time.
if (isset(Utils::$context['page_index'])) {
echo '
<div>', Utils::$context['page_index'], '</div>';
}

// Which option is set, text or text&images
if (isset($_REQUEST['images'])) {
echo '
Expand Down
187 changes: 187 additions & 0 deletions tests/Integration/Http/TopicPrintTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<?php

declare(strict_types=1);

namespace SMF\Tests\Integration\Http;

use PHPUnit\Framework\Attributes\CoversNothing;
use SMF\Config;
use SMF\Msg;
use SMF\Topic;

/**
* The print view of a topic, over HTTP.
*
* What is worth proving here is what a single request is willing to do. The
* print view renders whole posts with nothing else on the page, so a request for
* a long topic parses every post in it at once, and a visitor needs no account
* to ask for one. Only a real request shows how many posts come back, since the
* limit is applied in the action and the page is built by the template.
*/
#[CoversNothing]
class TopicPrintTest extends HttpTestCase
{
/*****************
* Class constants
*****************/

/**
* How many posts to put in the test topic.
*/
private const POSTS = 5;

/**
* How many of them a print page is allowed to show.
*/
private const PER_PAGE = 2;

/*********************
* Internal properties
*********************/

/**
* @var array Topics this test created, to be removed afterwards.
*/
private array $created_topics = [];

/**
* @var ?string The forum's own maximum topic size for the "All" view.
*/
private ?string $previous_max = null;

/****************
* Public methods
****************/

public function testAPrintPageStopsAtOnePageOfPosts(): void
{
$topic_id = $this->seedTopic();

$first = $this->fetch('?action=printpage;topic=' . $topic_id . '.0');

$this->assertSame(
self::PER_PAGE,
$first->xpath('//div[@class="postheader"]')->length,
'the print page rendered the whole topic instead of one page of it',
);

$this->assertStringNotContainsString(
$this->bodyOfPost(self::POSTS),
$first->text(),
'the last post of the topic is on the first print page',
);

$this->assertNoErrorsLogged('printing a topic logged something.' . "\n");
}

public function testTheRestOfTheTopicIsOnTheFollowingPages(): void
{
$topic_id = $this->seedTopic();

$last = $this->fetch('?action=printpage;topic=' . $topic_id . '.' . (self::POSTS - 1));

$this->assertStringContainsString(
$this->bodyOfPost(self::POSTS),
$last->text(),
'the last post of the topic cannot be printed at all',
);

$this->assertNoErrorsLogged('printing the last page logged something.' . "\n");
}

/**
* Without these the rest of a long topic is unreachable, since the print
* page is the only thing that links to itself.
*/
public function testAPrintPageLinksToTheOtherPages(): void
{
$topic_id = $this->seedTopic();

$first = $this->fetch('?action=printpage;topic=' . $topic_id . '.0');

$this->assertGreaterThan(
0,
$first->xpath(
'//a[contains(@href, "action=printpage")][contains(@href, "topic=' . $topic_id . '.' . self::PER_PAGE . '")]',
)->length,
'the print page does not link to its second page',
);
}

/******************
* Internal methods
******************/

protected function setUp(): void
{
parent::setUp();

// A print page shows no more posts than the "All" view is allowed to,
// so this is what decides where it stops.
$this->previous_max = $this->rawSetting('enableAllMessages');

Config::updateModSettings(['enableAllMessages' => self::PER_PAGE]);
}

protected function tearDown(): void
{
// Before the parent runs, while the connection is still ours.
Config::updateModSettings(['enableAllMessages' => (int) $this->previous_max]);

if ($this->created_topics !== []) {
Topic::remove($this->created_topics);

$this->created_topics = [];
}

parent::tearDown();
}

/**
* Starts a topic holding more posts than one print page may show.
*
* @return int The new topic's id.
*/
private function seedTopic(): int
{
$this->actingAs($this->adminId());

$topic_id = 0;

for ($i = 1; $i <= self::POSTS; $i++) {
$msgOptions = [
'subject' => ($i === 1 ? '' : 'Re: ') . 'Print pagination',
'body' => $this->bodyOfPost($i),
'send_notifications' => false,
];
$topicOptions = [
'id' => $topic_id,
'board' => 1,
];
$posterOptions = [
'id' => $this->adminId(),
];

Msg::create($msgOptions, $topicOptions, $posterOptions);

$topic_id = (int) $topicOptions['id'];

if ($this->created_topics === []) {
$this->created_topics[] = $topic_id;
}
}

return $topic_id;
}

/**
* The body of one of the seeded posts.
*
* @param int $number Which post.
* @return string Its body.
*/
private function bodyOfPost(int $number): string
{
return 'Print pagination post number ' . $number . '.';
}
}
Loading