From 02d871b57d9f564c214d9dd76fd5fd18c8a60894 Mon Sep 17 00:00:00 2001 From: Micha Roesler Date: Thu, 9 Jul 2026 08:14:45 +0200 Subject: [PATCH] #33547 fix(confluence): stop fetching external DTD in repairPageContent DOMDocument::validate() validated the page content against the HTML 4.0 loose DTD referenced in its doctype, forcing an outbound HTTP request to w3.org on every page download. In environments without outbound internet access that request failed and PHP emitted: DOMDocument::validate(http://www.w3.org/TR/REC-html40/loose.dtd): Failed to open stream: HTTP request failed! The validation had no functional value (Confluence storage format is never valid HTML 4.0, so it always returned false and the body-extraction branch ran regardless). Remove the validate() call and always strip the document wrapper to the body inner HTML, eliminating the spurious network dependency and the warning. Also guard empty content, which otherwise threw an uncaught ValueError from loadHTML(), and pass LIBXML_NONET as defence in depth. Adds an offline regression test. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ConfluencePageContentDownloader.php | 24 ++++++++++++-- tests/ConfluencePageContentDownloaderTest.php | 33 +++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/ConfluencePageContentDownloader.php b/src/ConfluencePageContentDownloader.php index 6abe720..5911771 100755 --- a/src/ConfluencePageContentDownloader.php +++ b/src/ConfluencePageContentDownloader.php @@ -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); } diff --git a/tests/ConfluencePageContentDownloaderTest.php b/tests/ConfluencePageContentDownloaderTest.php index 95f669b..2e4d045 100644 --- a/tests/ConfluencePageContentDownloaderTest.php +++ b/tests/ConfluencePageContentDownloaderTest.php @@ -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 = '' + . 'x

original

'; + + $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('

original

', $savedContent); + } + public function testDoesNotDownloadAttachmentsWhenDisabled(): void { $content = $this->createMock(Content::class);