Files
roundcubemail/tests/MessageRendering/MessageRenderingTestCase.php
Pablo Zmdl 1019b462d3 Render each mime part in an individual, sandboxed iframe
This includes a new "message loading" notice without meta refresh (which
requires unsafe-inline in a CSP, which we want to avoid)
2025-04-24 15:12:49 +02:00

160 lines
6.1 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace Tests\MessageRendering;
use Masterminds\HTML5;
use Roundcube\Tests\ActionTestCase;
use Roundcube\Tests\ExitException;
/**
* Class to base actual test classes on, which test specific message rendering.
*/
class MessageRenderingTestCase extends ActionTestCase
{
/**
* Get the body from the document, trimmed from surrounding whitespace.
*/
protected function getBody(\DOMXPath $domxpath): string
{
$bodyElem = $domxpath->query('//div[@id="messagebody"]');
$this->assertCount(1, $bodyElem, 'Message body');
return trim($bodyElem[0]->textContent);
}
/**
* Get the subject from the document, stripped by the prefix "Subject: ",
* the suffix "Open in new window", and trimmed from surrounding whitespace.
*/
protected function getScrubbedSubject(\DOMXPath $domxpath): string
{
$subjectElem = $domxpath->query('//h2[@class="subject"][1]');
$subject = preg_replace('/^\s*Subject:\s*(.*)\s*Open in new window$/', '$1', trim($subjectElem[0]->textContent));
return trim($subject);
}
/**
* Execute run() to render the message with the given $msgId.
*
* This is useful to check how rcmail_action_mail_show() renders messages.
* It requires a running dovecot to fetch the messages from. Messages need
* to be placed as individual files in
* `tests/src/emails/test@example/Mail/cur/`.
*/
protected function renderIframedBodyContent(array $params): \DOMXPath
{
return $this->runActionAndGetHtmlOutputDomxpath('get', function ($storage) use ($params) {
// rcmail_action_mail_get calls rcmail_attachment_handler, which
// closes one layer of output buffering, probably because index.php
// always starts one and rcmail_attachment_handler wants to close
// that. In order to avoid our tests being "risky" and thus marked
// as problematic, we need to simulate the opened output buffer
// layer.
ob_start();
$action = new \rcmail_action_mail_get();
$this->assertTrue($action->checks());
$_GET = $params;
$action->run();
});
}
protected function renderMessage(string $msgId): \DOMXPath
{
return $this->runActionAndGetHtmlOutputDomxpath('preview', function ($storage) use ($msgId) {
$action = new \rcmail_action_mail_show();
$this->assertTrue($action->checks());
$messagesList = $storage->list_messages();
// Find the UID of the wanted message.
$messageUid = null;
// TODO: find a method that scales better.
foreach ($messagesList as $messageHeaders) {
if ($messageHeaders->get('message-id') === "<{$msgId}>") {
$messageUid = $messageHeaders->uid;
break;
}
}
if ($messageUid === null) {
throw new \Exception("No message found in messages list with Message-Id '{$msgId}'");
}
$_GET = ['_uid' => $messageUid];
$action->run();
});
}
protected function runActionAndGetHtmlOutputDomxpath(string $urlActionParam, \Closure $callback): \DOMXPath
{
$imap_host = getenv('RC_CONFIG_IMAP_HOST') ?: 'tls://localhost:143';
$rcmail = \rcmail::get_instance();
// We need to overwrite the storage object, else storage_init() just
// returns the cached one (which might be a StorageMock instance).
$mockStorage = $rcmail->storage;
$rcmail->storage = null;
$rcmail->storage_init();
// Login our test user so we can fetch messages from the imap server.
$rcmail->login('test-message-rendering@localhost', 'pass', $imap_host);
$storage = $rcmail->get_storage();
$storage->set_options(['all_headers' => true]);
// We need to set the folder, else no message can be fetched.
$storage->set_folder('INBOX');
$this->initOutput(\rcmail_action::MODE_HTTP, 'mail', $urlActionParam);
// TODO: Why do we need to set the skin manually?
$rcmail->output->set_skin('elastic');
// Prepare and trigger the rendering.
$html = '';
try {
$callback($storage);
} catch (ExitException $e) {
// phpstan complains that the output classes of the production code
// don't provide that method which is true, but not relevant
// here.
// @phpstan-ignore-next-line
$html = $rcmail->output->getOutput();
}
// Reset the storage to the mocked one most other tests expect.
$rcmail->storage = $mockStorage;
// disabled_html_ns=true is a workaround for the performance issue
// https://github.com/Masterminds/html5-php/issues/181
$html5 = new HTML5(['disable_html_ns' => true]);
return new \DOMXPath($html5->loadHTML($html));
}
protected function getSrcParams(\DOMNode $elem): array
{
$src = $elem->attributes->getNamedItem('data-src')->textContent;
$url = parse_url($src, \PHP_URL_QUERY);
parse_str($url, $params);
return $params;
}
protected function getIframedContent(array $params): string
{
$domxpath_body = $this->renderIframedBodyContent($params);
$bodyElem = $domxpath_body->query('//body');
$this->assertCount(1, $bodyElem, 'Message body');
return $bodyElem[0]->textContent;
}
protected function assertIframedContent(\DOMNode $elem, string $partId, string $content): void
{
$params = $this->getSrcParams($elem);
$this->assertSrcUrlParams($params, $partId);
$body = $this->getIframedContent($params);
$this->assertSame($content, trim($body));
}
protected function assertSrcUrlParams(array $params, string $partId): void
{
$this->assertSame('mail', $params['_task']);
$this->assertSame('get', $params['_action']);
$this->assertSame('INBOX', $params['_mbox']);
$this->assertMatchesRegularExpression('/^\d+$/', $params['_uid']);
$this->assertSame($partId, $params['_part']);
}
}