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
24 changes: 21 additions & 3 deletions src/ConfluencePageContentDownloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,33 @@ public function downloadPageContent(ConfluencePage $page, bool $withAttachments
}
}

/**
* Normalises the page content by letting DOMDocument repair malformed markup and then
* stripping the document wrapper that loadHTML() adds, keeping only the body inner HTML.
*
* We deliberately do not call DOMDocument::validate(): it would validate the content
* against the HTML 4.0 Transitional DTD referenced in the doctype, which forces an
* outbound HTTP request to w3.org on every page. In locked-down environments without
* outbound internet access that request fails and PHP emits a warning. Validation adds
* no value here anyway (Confluence storage format is never valid HTML 4.0), so removing
* it also drops an unnecessary network dependency.
*/
private function repairPageContent(ConfluencePage $page): ConfluencePage
{
$content = $page->getContent();
if ($content === '') {
return $page;
}

$previousLibxmlState = libxml_use_internal_errors(true);

$domDocument = new DOMDocument();
$domDocument->loadHTML($page->getContent());
if (!$domDocument->validate()) {
$domDocument->loadHTML($content, LIBXML_NONET);

$body = $domDocument->getElementsByTagName('body')->item(0);
if ($body !== null) {
$pageContent = '';
foreach ($domDocument->getElementsByTagName('body')->item(0)->childNodes ?? [] as $child) {
foreach ($body->childNodes as $child) {
$pageContent .= $domDocument->saveHTML($child);
}

Expand Down
33 changes: 33 additions & 0 deletions tests/ConfluencePageContentDownloaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,39 @@ public function testAppliesReplacersAndDownloadsContentAndAttachments(): void
$downloader->downloadPageContent($this->page('123'));
}

/**
* Regression test for #33547: page content arrives wrapped in a full HTML document whose
* doctype references the HTML 4.0 loose DTD. repairPageContent() must strip the wrapper down
* to the body inner HTML without ever validating against (and thus fetching) that external
* DTD, which previously triggered a "Failed to open stream: HTTP request failed!" warning in
* environments without outbound internet access.
*/
public function testStripsHtmlDocumentWrapperWithoutFetchingExternalDtd(): void
{
$wrapped = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" '
. '"http://www.w3.org/TR/REC-html40/loose.dtd">'
. '<html><head><title>x</title></head><body><p>original</p></body></html>';

$savedContent = null;
$download = $this->createMock(Download::class);
$download->expects($this->once())->method('downloadPageContent')
->with($this->callback(static function (ConfluencePage $page) use (&$savedContent): bool {
$savedContent = $page->getContent();

return true;
}), 'content.html');

$logger = $this->createMock(LoggerInterface::class);
$logger->expects($this->never())->method('error');

$page = new ConfluencePage(['id' => '123', 'body' => ['storage' => ['value' => $wrapped]]]);

$downloader = new ConfluencePageContentDownloader(self::createStub(Content::class), $download, [], $logger);
$downloader->downloadPageContent($page, false);

self::assertSame('<p>original</p>', $savedContent);
}

public function testDoesNotDownloadAttachmentsWhenDisabled(): void
{
$content = $this->createMock(Content::class);
Expand Down
Loading