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);