diff --git a/DokuImageProcessorDecorator.php b/DokuImageProcessorDecorator.php
deleted file mode 100644
index d5aea140..00000000
--- a/DokuImageProcessorDecorator.php
+++ /dev/null
@@ -1,113 +0,0 @@
-
- */
-class DokuPDF extends Mpdf
-{
- /**
- * DokuPDF constructor.
- *
- * @param string $pagesize
- * @param string $orientation
- * @param int $fontsize
- *
- * @throws MpdfException
- * @throws Exception
- */
- public function __construct($pagesize = 'A4', $orientation = 'portrait', $fontsize = 11, $docLang = 'en')
- {
- global $conf;
- global $lang;
-
- if (!defined('_MPDF_TEMP_PATH')) {
- define('_MPDF_TEMP_PATH', $conf['tmpdir'] . '/dwpdf/' . random_int(1, 1000) . '/');
- }
- io_mkdir_p(_MPDF_TEMP_PATH);
-
- $format = $pagesize;
- if ($orientation == 'landscape') {
- $format .= '-L';
- }
-
- switch ($docLang) {
- case 'zh':
- case 'zh-tw':
- case 'ja':
- case 'ko':
- $mode = '+aCJK';
- break;
- default:
- $mode = 'UTF-8-s';
- }
-
- parent::__construct([
- 'mode' => $mode,
- 'format' => $format,
- 'default_font_size' => $fontsize,
- 'ImageProcessorClass' => DokuImageProcessorDecorator::class,
- 'tempDir' => _MPDF_TEMP_PATH, //$conf['tmpdir'] . '/tmp/dwpdf'
- 'SHYlang' => $docLang,
- ]);
-
- $this->autoScriptToLang = true;
- $this->baseScript = 1;
- $this->autoVietnamese = true;
- $this->autoArabic = true;
- $this->autoLangToFont = true;
-
- $this->ignore_invalid_utf8 = true;
- $this->tabSpaces = 4;
-
- // assumed that global language can be used, maybe Bookcreator needs more nuances?
- $this->SetDirectionality($lang['direction']);
- }
-
- /**
- * Cleanup temp dir
- */
- public function __destruct()
- {
- io_rmdir(_MPDF_TEMP_PATH, true);
- }
-
- /**
- * Decode all paths, since DokuWiki uses XHTML compliant URLs
- *
- * @param string $path
- * @param string $basepath
- */
- public function GetFullPath(&$path, $basepath = '')
- {
- $path = htmlspecialchars_decode($path);
- parent::GetFullPath($path, $basepath);
- }
-}
diff --git a/_test/ActionPagenameSortTest.php b/_test/ActionPagenameSortTest.php
deleted file mode 100644
index f7188d6c..00000000
--- a/_test/ActionPagenameSortTest.php
+++ /dev/null
@@ -1,104 +0,0 @@
-assertLessThan(0, $action->cbPagenameSort(['id' => 'bar'], ['id' => 'bar:start']));
- $this->assertGreaterThan(0, $action->cbPagenameSort(['id' => 'bar:bar'], ['id' => 'bar:start']));
- }
-
- /**
- * @return array
- * @see testPageNameSort
- */
- public function providerPageNameSort()
- {
- return [
- [
- 'start pages sorted',
- [
- 'bar',
- 'bar:start',
- 'bar:alpha',
- 'bar:bar',
- ],
- ],
- [
- 'pages and subspaces mixed',
- [
- 'alpha',
- 'beta:foo',
- 'gamma',
- ],
- ],
- [
- 'full test',
- [
- 'start',
- '01_page',
- '10_page',
- 'bar',
- 'bar:start',
- 'bar:1_page',
- 'bar:2_page',
- 'bar:10_page',
- 'bar:22_page',
- 'bar:aa_page',
- 'bar:aa_page:detail1',
- 'bar:zz_page',
- 'foo',
- 'foo:start',
- 'foo:01_page',
- 'foo:10_page',
- 'foo:foo',
- 'foo:zz_page',
- 'ns',
- 'ns:01_page',
- 'ns:10_page',
- 'ns:ns',
- 'ns:zz_page',
- 'zz_page',
- ],
- ],
- ];
- }
-
- /**
- * @dataProvider providerPageNameSort
- * @param string $comment
- * @param array $expected
- */
- public function testPagenameSort($comment, $expected)
- {
- // prepare the array as expected in the sort function
- $prepared = [];
- foreach ($expected as $line) {
- $prepared[] = ['id' => $line];
- }
-
- // the input is random
- $input = $prepared;
- shuffle($input);
-
- // run sort
- $action = new \action_plugin_dw2pdf();
- usort($input, [$action, 'cbPagenameSort']);
-
- $this->assertSame($prepared, $input);
- }
-}
-
diff --git a/_test/BookCreatorLiveSelectionCollectorTest.php b/_test/BookCreatorLiveSelectionCollectorTest.php
new file mode 100644
index 00000000..6502ed35
--- /dev/null
+++ b/_test/BookCreatorLiveSelectionCollectorTest.php
@@ -0,0 +1,54 @@
+set('selection', json_encode([$pageA, $pageB, $pageC])); // page B has mixed case to test cleanID()
+
+ $collector = new BookCreatorLiveSelectionCollector(new Config());
+ $this->assertSame(
+ [$pageA, 'playground:bookcreator:child'], // page C should be skipped as it does not exist
+ $collector->getPages()
+ );
+ }
+
+ /**
+ * Invalid JSON payloads must bubble up as JsonException so the caller can handle them.
+ */
+ public function testThrowsOnInvalidJson(): void
+ {
+ global $INPUT;
+ $INPUT->set('selection', '{invalid');
+
+ $this->expectException(\JsonException::class);
+ new BookCreatorLiveSelectionCollector(new Config());
+ }
+}
diff --git a/_test/CollectorFactoryTest.php b/_test/CollectorFactoryTest.php
new file mode 100644
index 00000000..56ea1589
--- /dev/null
+++ b/_test/CollectorFactoryTest.php
@@ -0,0 +1,90 @@
+assertInstanceOf(PageCollector::class, $collector);
+ }
+
+ /**
+ * Factory should switch to namespace collector when export_pdfns is requested.
+ */
+ public function testCreatesNamespaceCollector(): void
+ {
+ global $INPUT;
+ $INPUT->set('book_ns', 'wiki');
+ $config = new Config();
+ $collector = CollectorFactory::create('export_pdfns', $config, null, null);
+ $this->assertInstanceOf(NamespaceCollector::class, $collector);
+ }
+
+ /**
+ * When a live selection payload is present, the live collector is used.
+ */
+ public function testCreatesLiveSelectionCollector(): void
+ {
+ global $INPUT;
+ $INPUT->set('selection', '["wiki:start"]');
+ $config = new Config();
+ $collector = CollectorFactory::create('export_pdfbook', $config, null, null);
+ $this->assertInstanceOf(BookCreatorLiveSelectionCollector::class, $collector);
+ }
+
+ /**
+ * When only a saved selection identifier is present, use the saved collector.
+ */
+ public function testCreatesSavedSelectionCollector(): void
+ {
+ global $INPUT;
+ $INPUT->set('savedselection', 'my-saved-selection');
+ $config = new Config();
+ $collector = CollectorFactory::create('export_pdfbook', $config, null, null);
+ $this->assertInstanceOf(BookCreatorSavedSelectionCollector::class, $collector);
+ }
+
+ /**
+ * A pdfbook export without any selection should be rejected.
+ */
+ public function testBadSelectionCollector(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $config = new Config();
+ CollectorFactory::create('export_pdfbook', $config, null, null);
+ }
+
+ /**
+ * Unknown export actions should be rejected early.
+ */
+ public function testRejectsUnknownEvent(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $config = new Config();
+ CollectorFactory::create('random', $config, null, null);
+ }
+}
diff --git a/_test/ConfigTest.php b/_test/ConfigTest.php
new file mode 100644
index 00000000..a0342713
--- /dev/null
+++ b/_test/ConfigTest.php
@@ -0,0 +1,194 @@
+getMPdfConfig();
+
+ $this->assertSame('A4', $config->getFormat(), 'default pagesize/orientation');
+ $this->assertFalse($config->hasToc(), 'default toc');
+ $this->assertSame(5, $config->getMaxBookmarks(), 'default maxbookmarks');
+ $this->assertFalse($config->useNumberedHeaders(), 'default headernumber');
+ $this->assertSame('', $config->getWatermarkText(), 'default watermark');
+ $this->assertSame('default', $config->getTemplateName(), 'default template');
+ $this->assertSame('file', $config->getOutputTarget(), 'default output');
+ $this->assertSame([], $config->getStyledExtensions(), 'default usestyles');
+ $this->assertSame(0.0, $config->getQRScale(), 'default qrcodescale');
+ $this->assertFalse($config->isDebugEnabled(), 'default debug');
+ $this->assertNull($config->getBookTitle(), 'default book_title');
+ $this->assertSame('', $config->getBookNamespace(), 'default book_ns');
+ $this->assertSame('natural', $config->getBookSortOrder(), 'default book_order');
+ $this->assertSame(0, $config->getBookNamespaceDepth(), 'default book_nsdepth');
+ $this->assertSame([], $config->getBookExcludedPages(), 'default excludes');
+ $this->assertSame([], $config->getBookExcludedNamespaces(), 'default excludesns');
+ $this->assertFalse($config->hasLiveSelection(), 'default selection flag');
+ $this->assertNull($config->getLiveSelection(), 'default selection');
+ $this->assertFalse($config->hasSavedSelection(), 'default savedselection flag');
+ $this->assertNull($config->getSavedSelection(), 'default savedselection');
+ $this->assertSame('', $config->getExportId(), 'default exportid');
+
+ $this->assertSame('A4', $mpdfConfig['format'], 'default pagesize/orientation');
+ $this->assertSame(11, $mpdfConfig['default_font_size'], 'default font-size');
+ $this->assertSame($conf['tmpdir'] . '/mpdf', $mpdfConfig['tempDir'], 'default tmpdir');
+ $this->assertFalse($mpdfConfig['mirrorMargins'], 'default doublesided');
+ $this->assertSame([], $mpdfConfig['h2toc'], 'default toc levels');
+ $this->assertFalse($mpdfConfig['showWatermarkText'], 'default watermark');
+ $this->assertSame('stretch', $mpdfConfig['setAutoTopMargin'], 'default mpdf auto top margin');
+ $this->assertSame('stretch', $mpdfConfig['setAutoBottomMargin'], 'default mpdf auto bottom margin');
+ $this->assertTrue($mpdfConfig['autoScriptToLang'], 'default mpdf autoScriptToLang');
+ $this->assertSame(1, $mpdfConfig['baseScript'], 'default mpdf baseScript');
+ $this->assertTrue($mpdfConfig['autoVietnamese'], 'default mpdf autoVietnamese');
+ $this->assertTrue($mpdfConfig['autoArabic'], 'default mpdf autoArabic');
+ $this->assertTrue($mpdfConfig['autoLangToFont'], 'default mpdf autoLangToFont');
+ $this->assertTrue($mpdfConfig['ignore_invalid_utf8'], 'default mpdf ignore_invalid_utf8');
+ $this->assertSame(4, $mpdfConfig['tabSpaces'], 'default mpdf tabSpaces');
+ }
+
+ /**
+ * Ensure overrides from config work as expected
+ */
+ public function testloadPluginConfig(): void
+ {
+ $config = new Config([
+ 'exportid' => 'playground:start',
+ 'pagesize' => 'Legal',
+ 'orientation' => 'landscape',
+ 'font-size' => 14,
+ 'doublesided' => 0,
+ 'toc' => 1,
+ 'toclevels' => '2-4',
+ 'maxbookmarks' => 3,
+ 'headernumber' => 1,
+ 'template' => 'modern',
+ 'output' => 'inline',
+ 'usestyles' => 'wrap,foo ',
+ 'watermark' => 'CONFIDENTIAL',
+ 'qrcodescale' => '2.5',
+ 'debug' => 1,
+ 'booktitle' => 'My Book',
+ 'booknamespace' => 'playground:sub',
+ 'booksortorder' => 'date',
+ 'booknamespacedepth' => 2,
+ 'bookexcludepages' => ['playground:sub:skip'],
+ 'bookexcludenamespaces' => ['playground:private'],
+ 'liveselection' => '["playground:start","playground:Sub:Child"]',
+ 'savedselection' => 'fav:123',
+ ]);
+ $mpdfConfig = $config->getMPdfConfig();
+
+ $this->assertSame('playground:start', $config->getExportId(), 'from exportid');
+ $this->assertSame('Legal-L', $config->getFormat(), 'from pagesize + orientation');
+ $this->assertSame(14, $mpdfConfig['default_font_size'], 'from font-size');
+ $this->assertFalse($mpdfConfig['mirrorMargins'], 'from doublesided');
+ $this->assertTrue($config->hasToc(), 'from toc');
+ $this->assertSame(['H2' => 1, 'H3' => 2, 'H4' => 3], $mpdfConfig['h2toc'], 'from toclevels');
+ $this->assertSame(3, $config->getMaxBookmarks(), 'from maxbookmarks');
+ $this->assertTrue($config->useNumberedHeaders(), 'from headernumber');
+ $this->assertSame('modern', $config->getTemplateName(), 'from template');
+ $this->assertSame('inline', $config->getOutputTarget(), 'from output');
+ $this->assertSame(['wrap', 'foo'], $config->getStyledExtensions(), 'from usestyles');
+ $this->assertSame('CONFIDENTIAL', $config->getWatermarkText(), 'from watermark');
+ $this->assertTrue($mpdfConfig['showWatermarkText'], 'from watermark');
+ $this->assertSame(2.5, $config->getQRScale(), 'from qrcodescale');
+ $this->assertTrue($config->isDebugEnabled(), 'from debug');
+ $this->assertSame('My Book', $config->getBookTitle(), 'from booktitle');
+ $this->assertSame('playground:sub', $config->getBookNamespace(), 'from booknamespace');
+ $this->assertSame('date', $config->getBookSortOrder(), 'from booksortorder');
+ $this->assertSame(2, $config->getBookNamespaceDepth(), 'from booknamespacedepth');
+ $this->assertSame(['playground:sub:skip'], $config->getBookExcludedPages(), 'from bookexcludepages');
+ $this->assertSame(['playground:private'], $config->getBookExcludedNamespaces(), 'from bookexcludenamespaces');
+ $this->assertTrue($config->hasLiveSelection(), 'from liveselection');
+ $this->assertSame('["playground:start","playground:Sub:Child"]', $config->getLiveSelection(), 'from liveselection');
+ $this->assertTrue($config->hasSavedSelection(), 'from savedselection');
+ $this->assertSame('fav:123', $config->getSavedSelection(), 'from savedselection');
+ $this->assertNotEmpty($config->getCacheKey(), 'from combined plugin config values');
+ }
+
+ /**
+ * Ensure toc levels are set to DokuWiki's default when toc is enabled but no levels are set
+ */
+ public function testDefaultTocLevels()
+ {
+ $config = new Config(['toc' => 1]);
+ $mpdfConfig = $config->getMPdfConfig();
+ $this->assertSame(['H1' => 0, 'H2' => 1, 'H3' => 2], $mpdfConfig['h2toc'], 'from toclevels');
+ }
+
+ /**
+ * Ensure request parameters take precedence over defaults
+ */
+ public function testloadInputConfig(): void
+ {
+ global $INPUT, $ID;
+ $ID = 'playground:start';
+ $INPUT->set('pagesize', 'Legal');
+ $INPUT->set('orientation', 'landscape');
+ $INPUT->set('font-size', '14');
+ $INPUT->set('doublesided', '0');
+ $INPUT->set('toc', '1');
+ $INPUT->set('toclevels', '2-4');
+ $INPUT->set('watermark', 'CONFIDENTIAL');
+ $INPUT->set('tpl', 'modern');
+ $INPUT->set('debug', '1');
+ $INPUT->set('outputTarget', 'inline');
+ $INPUT->set('book_title', 'My Book');
+ $INPUT->set('book_ns', 'playground:sub');
+ $INPUT->set('book_order', 'date');
+ $INPUT->set('book_nsdepth', 2);
+ $INPUT->set('excludes', ['playground:sub:skip']);
+ $INPUT->set('excludesns', ['playground:private']);
+ $INPUT->set('selection', '["playground:start","playground:Sub:Child"]');
+ $INPUT->set('savedselection', 'fav:123');
+
+
+ $config = new Config();
+ $mpdfConfig = $config->getMPdfConfig();
+
+ $this->assertSame('playground:start', $config->getExportId(), 'from $ID');
+ $this->assertSame('Legal-L', $config->getFormat(), 'from pagesize + orientation');
+ $this->assertSame(14, $mpdfConfig['default_font_size'], 'from font-size');
+ $this->assertFalse($mpdfConfig['mirrorMargins'], 'from doublesided');
+ $this->assertSame(['H2' => 1, 'H3' => 2, 'H4' => 3], $mpdfConfig['h2toc'], 'from toclevels');
+ $this->assertTrue($mpdfConfig['showWatermarkText'], 'from watermark');
+ $this->assertSame('CONFIDENTIAL', $config->getWatermarkText(), 'from watermark');
+ $this->assertSame('modern', $config->getTemplateName(), 'from tpl');
+ $this->assertTrue($config->isDebugEnabled(), 'from debug');
+ $this->assertSame('inline', $config->getOutputTarget(), 'from outputTarget');
+ $this->assertSame('My Book', $config->getBookTitle(), 'from book_title');
+ $this->assertSame('playground:sub', $config->getBookNamespace(), 'from book_ns');
+ $this->assertSame('date', $config->getBookSortOrder(), 'from book_order');
+ $this->assertSame(2, $config->getBookNamespaceDepth(), 'from book_nsdepth');
+ $this->assertSame(['playground:sub:skip'], $config->getBookExcludedPages(), 'from excludes');
+ $this->assertSame(['playground:private'], $config->getBookExcludedNamespaces(), 'from excludesns');
+ $this->assertTrue($config->hasLiveSelection(), 'from selection');
+ $this->assertSame('["playground:start","playground:Sub:Child"]', $config->getLiveSelection(), 'from selection');
+ $this->assertTrue($config->hasSavedSelection(), 'from savedselection');
+ $this->assertSame('fav:123', $config->getSavedSelection(), 'from savedselection');
+
+ }
+
+
+}
diff --git a/_test/DokuImageProcessorTest.php b/_test/DokuImageProcessorTest.php
deleted file mode 100644
index 556cadf9..00000000
--- a/_test/DokuImageProcessorTest.php
+++ /dev/null
@@ -1,73 +0,0 @@
-assertEquals($expected_file, $actual_file, '$file ' . $msg);
- $this->assertEquals($expected_orig_srcpath, $actual_orig_srcpath, '$orig_srcpath ' . $msg);
- }
-
-}
diff --git a/_test/EndToEndTest.php b/_test/EndToEndTest.php
new file mode 100644
index 00000000..0fbf9317
--- /dev/null
+++ b/_test/EndToEndTest.php
@@ -0,0 +1,292 @@
+prepareFixturePage($page);
+ }
+
+ $config = new Config(array_merge(
+ $conf,
+ [
+ 'debug' => 1,
+ 'liveselection' => json_encode($pages)
+ ]
+ ));
+ $collector = new BookCreatorLiveSelectionCollector($config);
+ $cache = new Cache($config, $collector);
+ $service = new PdfExportService($config, $collector, $cache, 'Contents', 'tester');
+ return $service->getDebugHtml();
+ }
+
+ /**
+ * Test that numbered headers are rendered correctly
+ */
+ public function testNumberedHeaders(): void
+ {
+ $html = $this->getDebugHTML('headers', ['headernumber' => 1]);
+
+ $dom = (new Document())->html($html);
+
+ $dom->find('h1')->each(function ($h) {
+ $this->assertMatchesRegularExpression('/^1\. Header/', $h->text());
+ });
+
+ $dom->find('h2')->each(function ($h) {
+ $this->assertMatchesRegularExpression('/^\d\.\d\. Header/', $h->text());
+ });
+
+ $dom->find('h3')->each(function ($h) {
+ $this->assertMatchesRegularExpression('/^\d\.\d\.\d\. Header/', $h->text());
+ });
+ }
+
+ /**
+ * Test that numbered headers are rendered correctly across multiple pages
+ *
+ * Each new page should increase the top-level header number
+ */
+ public function testNumberedHeadersMultipage(): void
+ {
+ $html = $this->getDebugHTML(['headers', 'simple'], ['headernumber' => 1]);
+
+ $dom = (new Document())->html($html);
+
+ $count = 1;
+ $dom->find('h1')->each(function ($h) use (&$count) {
+ $this->assertMatchesRegularExpression('/^' . ($count++) . '\. /', $h->text());
+ });
+ }
+
+ /**
+ * Ensure each rendered page begins with an anchor that namespaces intra-page links.
+ */
+ public function testDocumentStartCreatesPageAnchors(): void
+ {
+ $html = $this->getDebugHTML(['renderer_features', 'target']);
+
+ $dom = (new Document())->html($html);
+
+ foreach (['renderer_features', 'target'] as $pageId) {
+ $anchors = $dom->find('a[name="' . $pageId . '__"]');
+ $this->assertSame(
+ 1,
+ count($anchors),
+ 'Missing document_start anchor for ' . $pageId
+ );
+ }
+ }
+
+ /**
+ * Bookmarks should only be produced up to the configured level and include numbering.
+ */
+ public function testBookmarksRespectConfiguredLevels(): void
+ {
+ $html = $this->getDebugHTML('headers', ['headernumber' => 1, 'maxbookmarks' => 2]);
+
+ $dom = (new Document())->html($html);
+ $bookmarks = $dom->find('bookmark');
+
+ $this->assertGreaterThan(0, count($bookmarks));
+
+ foreach ($bookmarks as $bookmark) {
+ $this->assertLessThanOrEqual(
+ 1,
+ (int)$bookmark->attr('level'),
+ 'Bookmark level exceeded configured maximum'
+ );
+
+ $content = trim((string)$bookmark->attr('content'));
+ $this->assertMatchesRegularExpression('/^\d+(?:\.\d+)*\.\s+Header/', $content);
+ }
+
+ $this->assertSame(count($dom->find('h1, h2')), count($bookmarks));
+ }
+
+ /**
+ * Local section links should include the page-specific prefix.
+ */
+ public function testLocallinksArePrefixedWithPageId(): void
+ {
+ $html = $this->getDebugHTML(['renderer_features', 'target']);
+ $dom = (new Document())->html($html);
+
+ $link = $this->findLinkByText($dom, 'Jump to Remote Section');
+ $this->assertNotNull($link, 'Local section link missing');
+
+ $this->assertSame('#renderer_features__remote_section', $link->attr('href'));
+ }
+
+ /**
+ * Internal links must expose dw2pdf data attributes so the writer can retarget them.
+ */
+ public function testInternalLinksExposeDw2pdfMetadata(): void
+ {
+ $html = $this->getRawRendererHtml('renderer_features', [], ['target']);
+ $dom = (new Document())->html($html);
+
+ $pageLink = $this->findLinkByText($dom, 'Target page link');
+ $this->assertNotNull($pageLink, 'Page link missing');
+ $this->assertSame('target', $pageLink->attr('data-dw2pdf-target'));
+ $this->assertSame('', $pageLink->attr('data-dw2pdf-hash'));
+
+ $sectionLink = $this->findLinkByText($dom, 'Target section link');
+ $this->assertNotNull($sectionLink, 'Section link missing');
+ $this->assertSame('target', $sectionLink->attr('data-dw2pdf-target'));
+ $this->assertSame('sub_section', $sectionLink->attr('data-dw2pdf-hash'));
+ }
+
+ /**
+ * Centered media needs to be wrapped so CSS centering survives inside mPDF.
+ */
+ public function testCenteredMediaIsWrapped(): void
+ {
+ $html = $this->getDebugHTML('renderer_features');
+ $dom = (new Document())->html($html);
+
+ $wrappers = $dom->find('div[align="center"][style*="text-align: center"]');
+ $this->assertGreaterThan(0, count($wrappers), 'Centered media wrapper missing');
+ $this->assertGreaterThan(0, count($wrappers->first()->find('img')));
+ }
+
+ /**
+ * Acronyms should render as plain text to avoid useless hover hints in PDFs.
+ */
+ public function testAcronymOutputDropsHover(): void
+ {
+ $html = $this->getDebugHTML('renderer_features');
+ $dom = (new Document())->html($html);
+
+ $this->assertSame(0, count($dom->find('acronym')));
+ $this->assertStringContainsString('FAQ', $html);
+ $this->assertStringNotContainsString('Frequently Asked Questions', $html);
+ }
+
+ /**
+ * Email addresses must not be obfuscated so that mailto links remain readable.
+ */
+ public function testEmailLinksStayReadable(): void
+ {
+ $html = $this->getDebugHTML('renderer_features');
+ $dom = (new Document())->html($html);
+
+ $link = $this->findLinkByText($dom, 'test@example.com');
+ $this->assertNotNull($link, 'Email link missing');
+ $this->assertSame('mailto:test@example.com', $link->attr('href'));
+ }
+
+ /**
+ * Interwiki links should be prefixed with the respective icon.
+ */
+ public function testInterwikiLinksArePrefixedWithIcon(): void
+ {
+ $html = $this->getDebugHTML('renderer_features');
+ $dom = (new Document())->html($html);
+
+ $link = $dom->find('a.interwiki')->first();
+ $this->assertNotNull($link, 'Interwiki link missing');
+
+ $icon = $link->children()->first();
+ $this->assertNotNull($icon, 'Interwiki icon missing');
+ $this->assertSame('img', strtolower($icon->nodeName));
+ $this->assertStringContainsString('iw_doku', $icon->attr('class'));
+ }
+
+ /**
+ * Render a single page through the dw2pdf renderer without writer post-processing.
+ *
+ * @param string $pageId
+ * @param array $conf
+ * @param string[] $additionalPages
+ * @return string
+ */
+ protected function getRawRendererHtml(string $pageId, array $conf = [], array $additionalPages = []): string
+ {
+ $this->prepareFixturePage($pageId);
+ foreach ($additionalPages as $related) {
+ $this->prepareFixturePage($related);
+ }
+
+ /** @var \renderer_plugin_dw2pdf $renderer */
+ $renderer = plugin_load('renderer', 'dw2pdf', true);
+ $renderer->setConfig(new Config($conf));
+
+ global $ID;
+ $keep = $ID;
+ $ID = $pageId;
+
+ $file = wikiFN($pageId);
+ $instructions = p_get_instructions(io_readWikiPage($file, $pageId));
+ $info = [];
+ $html = p_render('dw2pdf', $instructions, $info);
+
+ $ID = $keep;
+
+ return $html;
+ }
+
+ /**
+ * Persist the given fixture page into the wiki so that it can be rendered.
+ *
+ * @param string $pageId
+ * @return void
+ */
+ protected function prepareFixturePage(string $pageId): void
+ {
+ $data = file_get_contents(__DIR__ . '/pages/' . $pageId . '.txt');
+ saveWikiText($pageId, $data, 'dw2pdf renderer test');
+ }
+
+ /**
+ * Locate the first hyperlink whose trimmed text matches the expected label.
+ *
+ * @param Document $dom
+ * @param string $text
+ * @return Element|null
+ */
+ protected function findLinkByText(Document $dom, string $text): ?Element
+ {
+ foreach ($dom->find('a') as $anchor) {
+ if (trim($anchor->text()) === $text) {
+ return $anchor;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/_test/GeneralTest.php b/_test/GeneralTest.php
index 8c6d5b07..a228afcc 100644
--- a/_test/GeneralTest.php
+++ b/_test/GeneralTest.php
@@ -32,9 +32,9 @@ public function testPluginInfo(): void
$this->assertArrayHasKey('url', $info);
$this->assertEquals('dw2pdf', $info['base']);
- $this->assertRegExp('/^https?:\/\//', $info['url']);
+ $this->assertMatchesRegularExpression('/^https?:\/\//', $info['url']);
$this->assertTrue(mail_isvalid($info['email']));
- $this->assertRegExp('/^\d\d\d\d-\d\d-\d\d$/', $info['date']);
+ $this->assertMatchesRegularExpression('/^\d\d\d\d-\d\d-\d\d$/', $info['date']);
$this->assertTrue(false !== strtotime($info['date']));
}
diff --git a/_test/LocalContentLoaderTest.php b/_test/LocalContentLoaderTest.php
new file mode 100644
index 00000000..9251e224
--- /dev/null
+++ b/_test/LocalContentLoaderTest.php
@@ -0,0 +1,47 @@
+markTestSkipped('Unable to create a temporary file');
+ }
+
+ $file = $temp . '.png';
+ if (!@rename($temp, $file)) {
+ $this->fail('Unable to prepare a temporary dw2pdf:// file');
+ }
+ file_put_contents($file, 'image-bytes');
+
+ $loader = new LocalContentLoader();
+ $this->assertSame('image-bytes', $loader->load('dw2pdf://' . $file));
+
+ @unlink($file);
+ }
+
+ /**
+ * Missing files should return null so mPDF can fall back to HTTP.
+ */
+ public function testReturnsNullForMissingFiles(): void
+ {
+ $file = sys_get_temp_dir() . '/dw2pdf_loader_missing.png';
+ @unlink($file);
+
+ $loader = new LocalContentLoader();
+ $this->assertNull($loader->load($file));
+ }
+}
diff --git a/_test/MediaLinkResolverTest.php b/_test/MediaLinkResolverTest.php
new file mode 100644
index 00000000..7f8ce123
--- /dev/null
+++ b/_test/MediaLinkResolverTest.php
@@ -0,0 +1,130 @@
+resolver = new MediaLinkResolver();
+ }
+
+ /**
+ * @return array
+ */
+ public static function resolveProvider(): array
+ {
+ global $conf;
+
+ return [
+ 'internal fetch url' => [
+ DOKU_URL . 'lib/exe/fetch.php?media=wiki:dokuwiki-128.png',
+ $conf['mediadir'] . '/wiki/dokuwiki-128.png',
+ 'image/png',
+ ],
+ 'static local file' => [
+ DOKU_URL . 'lib/images/throbber.gif',
+ DOKU_INC . 'lib/images/throbber.gif',
+ 'image/gif',
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider resolveProvider
+ */
+ public function testResolveReturnsLocalPathAndMime(string $input, string $expectedPath, string $expectedMime): void
+ {
+ $resolved = $this->resolver->resolve($input);
+
+ $this->assertNotNull($resolved);
+ $this->assertSame($expectedPath, $resolved['path']);
+ $this->assertSame($expectedMime, $resolved['mime']);
+ $this->assertFileExists($resolved['path']);
+ }
+
+ /**
+ * The resolver must support our dw2pdf:// pseudo scheme for temporary files.
+ */
+ public function testResolveDw2pdfScheme(): void
+ {
+ $temp = tempnam(sys_get_temp_dir(), 'dw2pdf');
+ if ($temp === false) {
+ $this->fail('Unable to create temp file for dw2pdf:// test');
+ }
+
+ $image = $temp . '.png';
+ if (!rename($temp, $image)) {
+ $this->fail('Unable to rename temp file for dw2pdf:// test');
+ }
+
+ $resolved = $this->resolver->resolve('dw2pdf://' . $image);
+
+ $this->assertNotNull($resolved);
+ $this->assertSame($image, $resolved['path']);
+ $this->assertSame('image/png', $resolved['mime']);
+
+ @unlink($image);
+ }
+
+ /**
+ * @group internet
+ */
+ public function testResolveFetchesExternalMedia(): void
+ {
+ global $conf;
+ $conf['fetchsize'] = 512 * 1024; // 512 KB
+
+ $external = 'https://php.net/images/php.gif';
+ $input = DOKU_URL . 'lib/exe/fetch.php?media=' . rawurlencode($external);
+ $resolved = $this->resolver->resolve($input);
+
+ if ($resolved === null) {
+ $this->markTestSkipped('External media fetching is not available in this environment.');
+ }
+
+ $this->assertNotNull($resolved);
+ $this->assertFileExists($resolved['path']);
+ $this->assertSame('image/gif', $resolved['mime']);
+ $this->assertSame(2523, filesize($resolved['path']));
+ }
+
+ /**
+ * Non-image payloads should never be returned to the PDF generator.
+ */
+ public function testResolveRejectsNonImages(): void
+ {
+ $resolved = $this->resolver->resolve(DOKU_URL . 'README');
+
+ $this->assertNull($resolved);
+ }
+
+ /**
+ * Resizing parameters from fetch.php should trigger scaled copies in cache.
+ */
+ public function testResolveAppliesResizeParameter(): void
+ {
+ global $conf;
+
+ $input = DOKU_URL . 'lib/exe/fetch.php?w=32&media=wiki:dokuwiki-128.png';
+ $original = $conf['mediadir'] . '/wiki/dokuwiki-128.png';
+ $resolved = $this->resolver->resolve($input);
+
+ $this->assertNotNull($resolved);
+ $this->assertNotSame($original, $resolved['path']);
+ $this->assertSame('image/png', $resolved['mime']);
+ $this->assertFileExists($resolved['path']);
+ }
+}
diff --git a/_test/NamespaceCollectorCollectTest.php b/_test/NamespaceCollectorCollectTest.php
new file mode 100644
index 00000000..37e7f75f
--- /dev/null
+++ b/_test/NamespaceCollectorCollectTest.php
@@ -0,0 +1,62 @@
+createPage("$ns:start", 'start page');
+ $this->createPage("$ns:keep", 'keep me');
+ $this->createPage("$ns:skip", 'skip me');
+ $this->createPage("$ns:child:page", 'child namespace');
+
+ $INPUT->set('book_ns', $ns);
+ $INPUT->set('book_order', 'pagename');
+ $INPUT->set('excludes', ["$ns:skip"]);
+ $INPUT->set('excludesns', ["$ns:child"]);
+
+ $collector = new NamespaceCollector(new Config());
+ $this->assertSame(
+ ["$ns:start", "$ns:keep"],
+ $collector->getPages()
+ );
+ }
+
+ /**
+ * Invalid namespaces must be ignored gracefully.
+ */
+ public function testCollectReturnsEmptyForMissingNamespace(): void
+ {
+ global $INPUT;
+ $INPUT->set('book_ns', 'missing:dw2pdfns');
+
+ $collector = new NamespaceCollector(new Config());
+ $this->assertSame([], $collector->getPages());
+ }
+
+ private function createPage(string $id, string $content): void
+ {
+ saveWikiText($id, $content, 'dw2pdf namespace test');
+ }
+}
diff --git a/_test/NamespaceCollectorSortTest.php b/_test/NamespaceCollectorSortTest.php
new file mode 100644
index 00000000..b9b207ac
--- /dev/null
+++ b/_test/NamespaceCollectorSortTest.php
@@ -0,0 +1,91 @@
+ [[
+ 'bar',
+ 'bar:start',
+ 'bar:alpha',
+ 'bar:bar',
+ ]],
+
+ 'pages and subspaces mixed' => [[
+ 'alpha',
+ 'beta:foo',
+ 'gamma',
+ ]],
+
+ 'full test' => [[
+ 'start',
+ '01_page',
+ '10_page',
+ 'bar',
+ 'bar:start',
+ 'bar:1_page',
+ 'bar:2_page',
+ 'bar:10_page',
+ 'bar:22_page',
+ 'bar:aa_page',
+ 'bar:aa_page:detail1',
+ 'bar:zz_page',
+ 'foo',
+ 'foo:start',
+ 'foo:01_page',
+ 'foo:10_page',
+ 'foo:foo',
+ 'foo:zz_page',
+ 'ns',
+ 'ns:01_page',
+ 'ns:10_page',
+ 'ns:ns',
+ 'ns:zz_page',
+ 'zz_page',
+ ]],
+ ];
+ }
+
+ /**
+ * Ensure natural name sorting remains stable for multiple namespace scenarios.
+ *
+ * @dataProvider providerPageNameSort
+ * @param array $expected
+ */
+ public function testPagenameSort(array $expected): void
+ {
+ // Build a namespace collector instance without running the heavy constructor logic.
+ $reflection = new ReflectionClass(NamespaceCollector::class);
+ /** @var NamespaceCollector $collector */
+ $collector = $reflection->newInstanceWithoutConstructor();
+
+ $prepared = [];
+ foreach ($expected as $line) {
+ $prepared[] = ['id' => $line];
+ }
+
+ $input = $prepared;
+ shuffle($input);
+
+ usort($input, [$collector, 'cbPagenameSort']);
+
+ $this->assertSame($prepared, $input);
+ }
+}
diff --git a/_test/PageCollectorTest.php b/_test/PageCollectorTest.php
new file mode 100644
index 00000000..748ebab1
--- /dev/null
+++ b/_test/PageCollectorTest.php
@@ -0,0 +1,49 @@
+set('id', $ID);
+ saveWikiText($ID, 'DW2PDF page content', 'create test page');
+
+ $collector = new PageCollector(new Config());
+ $this->assertSame([$ID], $collector->getPages());
+ }
+
+ /**
+ * Missing pages should not be exported.
+ */
+ public function testReturnsEmptyForMissingPage(): void
+ {
+ global $ID, $INPUT;
+ $ID = 'playground:missingdw2pdf';
+ $INPUT->set('id', $ID);
+
+ @unlink(wikiFN($ID));
+
+ $collector = new PageCollector(new Config());
+ $this->assertSame([], $collector->getPages());
+ }
+}
diff --git a/_test/TemplateTest.php b/_test/TemplateTest.php
new file mode 100644
index 00000000..737edb90
--- /dev/null
+++ b/_test/TemplateTest.php
@@ -0,0 +1,65 @@
+set('book_title', 'Export Title');
+ $ID = 'playground:templatepage';
+ saveWikiText($ID, 'template test', 'create');
+
+ $config = new Config([
+ 'qrcodescale' => 1.5,
+ ]);
+
+ $collector = new PageCollector($config);
+ $template = new Template($config);
+ $template->setContext($collector, $ID, 'username');
+
+ $html = $template->getHTML('unittest');
+
+ $this->assertStringContainsString('Page Number: {PAGENO}', $html);
+ $this->assertStringContainsString('Total Pages: {nbpg}', $html);
+ $this->assertStringContainsString('Document Title: Export Title', $html);
+ $this->assertStringContainsString('Wiki Title: ' . $conf['title'], $html);
+ $this->assertStringContainsString('Wiki URL: ' . DOKU_URL, $html);
+ $this->assertStringNotContainsString('@DATE@', $html);
+ $this->assertStringContainsString('User: username', $html);
+ $this->assertStringContainsString('Base Path: ' . DOKU_BASE, $html);
+ $this->assertStringContainsString('Include Dir: ' . DOKU_INC, $html);
+ $this->assertStringContainsString('Template Base Path: ' . DOKU_BASE . 'lib/plugins/dw2pdf/tpl/default/', $html);
+ $this->assertStringContainsString('Template Include Dir: ' . DOKU_INC . 'lib/plugins/dw2pdf/tpl/default/', $html);
+ $this->assertStringContainsString('Page ID: ' . $ID, $html);
+
+ $revisionDate = dformat(filemtime(wikiFN($ID)));
+ $this->assertStringContainsString('Revision: ' . $revisionDate, $html);
+
+ $pageUrl = wl($ID, [], true, '&');
+ $this->assertStringContainsString('Page URL: ' . $pageUrl, $html);
+ $this->assertStringContainsString('assertStringContainsString('size="1.5"', $html);
+ $this->assertStringNotContainsString('@QRCODE@', $html);
+ }
+}
diff --git a/_test/WriterInternalLinksTest.php b/_test/WriterInternalLinksTest.php
new file mode 100644
index 00000000..3c1c1161
--- /dev/null
+++ b/_test/WriterInternalLinksTest.php
@@ -0,0 +1,71 @@
+'
+ . 'First'
+ // missing:id must keep its original href because the collector will not output that page
+ . 'Second'
+ . '
';
+
+ $writer = (new \ReflectionClass(Writer::class))->newInstanceWithoutConstructor();
+ $method = (new \ReflectionClass(Writer::class))->getMethod('fixInternalLinks');
+ $method->setAccessible(true);
+ $result = $method->invoke($writer, $collector, $html);
+
+ $check = false;
+ $section = sectionID('playground:fixlinks', $check);
+ $this->assertStringContainsString('href="#' . $section . '__abc"', $result);
+ $this->assertStringNotContainsString('data-dw2pdf-target', $result);
+ $this->assertStringContainsString('href="doku.php?id=missing"', $result);
+ }
+}
+
+class WriterInternalLinksCollectorStub extends AbstractCollector
+{
+ /** @var string[] */
+ private array $collectedPages;
+
+ public function __construct(array $pages, Config $config)
+ {
+ $this->collectedPages = $pages;
+ $this->config = $config;
+ $this->pages = $pages;
+ $this->title = 'stub';
+ $this->rev = null;
+ $this->at = null;
+ }
+
+ protected function collect(): array
+ {
+ return $this->collectedPages;
+ }
+}
diff --git a/_test/pages/headers.txt b/_test/pages/headers.txt
new file mode 100644
index 00000000..5228aa95
--- /dev/null
+++ b/_test/pages/headers.txt
@@ -0,0 +1,17 @@
+====== Header A ======
+
+===== Header A.A =====
+
+==== Header A.A.a =====
+
+=== Header A.A.a.a ===
+
+=== Header A.A.a.b ===
+
+==== Header A.A.b =====
+
+==== Header A.A.c =====
+
+===== Header A.B ======
+
+===== Header A.C ======
diff --git a/_test/pages/renderer_features.txt b/_test/pages/renderer_features.txt
new file mode 100644
index 00000000..79c2c515
--- /dev/null
+++ b/_test/pages/renderer_features.txt
@@ -0,0 +1,29 @@
+====== Renderer Feature Playground ======
+
+===== Local Navigation =====
+
+[[#remote_section|Jump to Remote Section]]
+
+===== Internal Links =====
+
+[[target|Target page link]] and [[target#sub_section|Target section link]]
+
+===== Media =====
+
+{{ wiki:dokuwiki-128.png?50 }}
+
+===== Acronym =====
+
+This is an example of an acronym: FAQ.
+
+===== Email =====
+
+Contact for mission details.
+
+===== Interwiki =====
+
+See [[doku>syntax|DokuWiki Syntax]] for more.
+
+===== Remote Section =====
+
+This section anchors local jump links.
diff --git a/_test/pages/simple.txt b/_test/pages/simple.txt
new file mode 100644
index 00000000..fdbb21b2
--- /dev/null
+++ b/_test/pages/simple.txt
@@ -0,0 +1,3 @@
+====== A Simple Page ======
+
+This is a simple page with simple text for simple testing.
diff --git a/_test/pages/target.txt b/_test/pages/target.txt
new file mode 100644
index 00000000..ab9ee4f5
--- /dev/null
+++ b/_test/pages/target.txt
@@ -0,0 +1,5 @@
+====== Target Page ======
+
+===== Sub Section =====
+
+This page provides an anchor for cross-page links.
diff --git a/action.php b/action.php
index 1b665733..5ad6bf0e 100644
--- a/action.php
+++ b/action.php
@@ -1,11 +1,13 @@
*/
- protected $tpl;
- /** @var string title of exported pdf */
- protected $title;
- /** @var array list of pages included in exported pdf */
- protected $list = [];
- /** @var bool|string path to temporary cachefile */
- protected $onetimefile = false;
- protected $currentBookChapter = 0;
-
- /**
- * Constructor. Sets the correct template
- */
- public function __construct()
- {
- require_once __DIR__ . '/vendor/autoload.php';
-
- $this->tpl = $this->getExportConfig('template');
- }
-
- /**
- * Delete cached files that were for one-time use
- */
- public function __destruct()
- {
- if ($this->onetimefile) {
- unlink($this->onetimefile);
- }
- }
-
- /**
- * Return the value of currentBookChapter, which is the order of the file to be added in a book generation
- */
- public function getCurrentBookChapter()
- {
- return $this->currentBookChapter;
- }
-
/**
* Register the events
*
@@ -72,7 +30,6 @@ public function getCurrentBookChapter()
public function register(EventHandler $controller)
{
$controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'convert');
- $controller->register_hook('TEMPLATE_PAGETOOLS_DISPLAY', 'BEFORE', $this, 'addbutton');
$controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, 'addsvgbutton');
}
@@ -80,1003 +37,38 @@ public function register(EventHandler $controller)
* Do the HTML to PDF conversion work
*
* @param Event $event
+ * @throws MpdfException
*/
public function convert(Event $event)
{
- global $REV, $DATE_AT;
- global $conf, $INPUT;
+ global $REV, $DATE_AT, $INPUT;
// our event?
$allowedEvents = ['export_pdfbook', 'export_pdf', 'export_pdfns'];
if (!in_array($event->data, $allowedEvents)) {
return;
}
-
- try {
- //collect pages and check permissions
- [$this->title, $this->list] = $this->collectExportablePages($event);
-
- if ($event->data === 'export_pdf' && ($REV || $DATE_AT)) {
- $cachefile = tempnam($conf['tmpdir'] . '/dwpdf', 'dw2pdf_');
- $this->onetimefile = $cachefile;
- $generateNewPdf = true;
- } else {
- // prepare cache and its dependencies
- $depends = [];
- $cache = $this->prepareCache($depends);
- $cachefile = $cache->cache;
- $generateNewPdf = !$this->getConf('usecache')
- || $this->getExportConfig('isDebug')
- || !$cache->useCache($depends);
- }
-
- // hard work only when no cache available or needed for debugging
- if ($generateNewPdf) {
- // generating the pdf may take a long time for larger wikis / namespaces with many pages
- set_time_limit(0);
- //may throw Mpdf\MpdfException as well
- $this->generatePDF($cachefile, $event);
- }
- } catch (Exception $e) {
- if ($INPUT->has('selection')) {
- http_status(400);
- echo $e->getMessage();
- exit();
- } else {
- //prevent Action/Export()
- msg($e->getMessage(), -1);
- $event->data = 'redirect';
- return;
- }
- }
- $event->preventDefault(); // after prevent, $event->data cannot be changed
-
- // deliver the file
- $this->sendPDFFile($cachefile); //exits
- }
-
- /**
- * Obtain list of pages and title, for different methods of exporting the pdf.
- * - Return a title and selection, throw otherwise an exception
- * - Check permisions
- *
- * @param Event $event
- * @return array
- * @throws Exception
- */
- protected function collectExportablePages(Event $event)
- {
- global $ID, $REV;
- global $INPUT;
- global $conf, $lang;
-
- // list of one or multiple pages
- $list = [];
-
- if ($event->data == 'export_pdf') {
- if (auth_quickaclcheck($ID) < AUTH_READ) { // set more specific denied message
- throw new Exception($lang['accessdenied']);
- }
- $list[0] = $ID;
- $title = $INPUT->str('pdftitle'); //DEPRECATED
- $title = $INPUT->str('book_title', $title, true);
- if (empty($title)) {
- $title = p_get_first_heading($ID);
- }
- // use page name if title is still empty
- if (empty($title)) {
- $title = noNS($ID);
- }
-
- $filename = wikiFN($ID, $REV);
- if (!file_exists($filename)) {
- throw new Exception($this->getLang('notexist'));
- }
- } elseif ($event->data == 'export_pdfns') {
- //check input for title and ns
- if (!$title = $INPUT->str('book_title')) {
- throw new Exception($this->getLang('needtitle'));
- }
- $pdfnamespace = cleanID($INPUT->str('book_ns'));
- if (!@is_dir(dirname(wikiFN($pdfnamespace . ':dummy')))) {
- throw new Exception($this->getLang('needns'));
- }
-
- //sort order
- $order = $INPUT->str('book_order', 'natural', true);
- $sortoptions = ['pagename', 'date', 'natural'];
- if (!in_array($order, $sortoptions)) {
- $order = 'natural';
- }
-
- //search depth
- $depth = $INPUT->int('book_nsdepth', 0);
- if ($depth < 0) {
- $depth = 0;
- }
-
- //page search
- $result = [];
- $opts = ['depth' => $depth]; //recursive all levels
- $dir = utf8_encodeFN(str_replace(':', '/', $pdfnamespace));
- search($result, $conf['datadir'], 'search_allpages', $opts, $dir);
-
- // exclude ids
- $excludes = $INPUT->arr('excludes');
- if (!empty($excludes)) {
- $result = array_filter($result, function ($item) use ($excludes) {
- return !in_array($item['id'], $excludes);
- });
- }
- // exclude namespaces
- $excludesns = $INPUT->arr('excludesns');
- if (!empty($excludesns)) {
- $result = array_filter($result, function ($item) use ($excludesns) {
- foreach ($excludesns as $ns) {
- if (strpos($item['id'], $ns . ':') === 0) {
- return false;
- }
- }
- return true;
- });
- }
-
- //sorting
- if (count($result) > 0) {
- if ($order == 'date') {
- usort($result, [$this, 'cbDateSort']);
- } elseif ($order == 'pagename' || $order == 'natural') {
- usort($result, [$this, 'cbPagenameSort']);
- }
- }
-
- foreach ($result as $item) {
- $list[] = $item['id'];
- }
-
- if ($pdfnamespace !== '') {
- if (!in_array($pdfnamespace . ':' . $conf['start'], $list, true)) {
- if (file_exists(wikiFN(rtrim($pdfnamespace, ':')))) {
- array_unshift($list, rtrim($pdfnamespace, ':'));
- }
- }
- }
- } elseif (!empty($_COOKIE['list-pagelist'])) {
- /** @deprecated April 2016 replaced by localStorage version of Bookcreator */
- //is in Bookmanager of bookcreator plugin a title given?
- $title = $INPUT->str('pdfbook_title'); //DEPRECATED
- $title = $INPUT->str('book_title', $title, true);
- if (empty($title)) {
- throw new Exception($this->getLang('needtitle'));
- }
-
- $list = explode("|", $_COOKIE['list-pagelist']);
- } elseif ($INPUT->has('selection')) {
- //handle Bookcreator requests based at localStorage
-// if(!checkSecurityToken()) {
-// http_status(403);
-// print $this->getLang('empty');
-// exit();
-// }
-
- $list = json_decode($INPUT->str('selection', '', true), true);
- if (!is_array($list) || $list === []) {
- throw new Exception($this->getLang('empty'));
- }
-
- $title = $INPUT->str('pdfbook_title'); //DEPRECATED
- $title = $INPUT->str('book_title', $title, true);
- if (empty($title)) {
- throw new Exception($this->getLang('needtitle'));
- }
- } elseif ($INPUT->has('savedselection')) {
- //export a saved selection of the Bookcreator Plugin
- if (plugin_isdisabled('bookcreator')) {
- throw new Exception($this->getLang('missingbookcreator'));
- }
- /** @var action_plugin_bookcreator_handleselection $SelectionHandling */
- $SelectionHandling = plugin_load('action', 'bookcreator_handleselection');
- $savedselection = $SelectionHandling->loadSavedSelection($INPUT->str('savedselection'));
- $title = $savedselection['title'];
- $title = $INPUT->str('book_title', $title, true);
- $list = $savedselection['selection'];
-
- if (empty($title)) {
- throw new Exception($this->getLang('needtitle'));
- }
- } else {
- //show empty bookcreator message
- throw new Exception($this->getLang('empty'));
- }
-
- $list = array_map('cleanID', $list);
-
- $skippedpages = [];
- foreach ($list as $index => $pageid) {
- if (auth_quickaclcheck($pageid) < AUTH_READ) {
- $skippedpages[] = $pageid;
- unset($list[$index]);
- }
- }
- $list = array_filter($list, 'strlen'); //use of strlen() callback prevents removal of pagename '0'
-
- //if selection contains forbidden pages throw (overridable) warning
- if (!$INPUT->bool('book_skipforbiddenpages') && $skippedpages !== []) {
- $msg = hsc(implode(', ', $skippedpages));
- throw new Exception(sprintf($this->getLang('forbidden'), $msg));
- }
-
- return [$title, $list];
- }
-
- /**
- * Prepare cache
- *
- * @param array $depends (reference) array with dependencies
- * @return cache
- */
- protected function prepareCache(&$depends)
- {
- global $REV;
-
- $cachekey = implode(',', $this->list)
- . $REV
- . $this->getExportConfig('template')
- . $this->getExportConfig('pagesize')
- . $this->getExportConfig('orientation')
- . $this->getExportConfig('font-size')
- . $this->getExportConfig('doublesided')
- . $this->getExportConfig('headernumber')
- . ($this->getExportConfig('hasToC') ? implode('-', $this->getExportConfig('levels')) : '0')
- . $this->title;
- $cache = new Cache($cachekey, '.dw2.pdf');
-
- $dependencies = [];
- foreach ($this->list as $pageid) {
- $relations = p_get_metadata($pageid, 'relation');
-
- if (is_array($relations)) {
- if (array_key_exists('media', $relations) && is_array($relations['media'])) {
- foreach ($relations['media'] as $mediaid => $exists) {
- if ($exists) {
- $dependencies[] = mediaFN($mediaid);
- }
- }
- }
-
- if (array_key_exists('haspart', $relations) && is_array($relations['haspart'])) {
- foreach ($relations['haspart'] as $part_pageid => $exists) {
- if ($exists) {
- $dependencies[] = wikiFN($part_pageid);
- }
- }
- }
- }
-
- $dependencies[] = metaFN($pageid, '.meta');
- }
-
- $depends['files'] = array_map('wikiFN', $this->list);
- $depends['files'][] = __FILE__;
- $depends['files'][] = __DIR__ . '/renderer.php';
- $depends['files'][] = __DIR__ . '/mpdf/mpdf.php';
- $depends['files'] = array_merge(
- $depends['files'],
- $dependencies,
- getConfigFiles('main')
- );
- return $cache;
- }
-
- /**
- * Returns the parsed Wikitext in dw2pdf for the given id and revision
- *
- * @param string $id page id
- * @param string|int $rev revision timestamp or empty string
- * @param string $date_at
- * @return null|string
- */
- protected function wikiToDW2PDF($id, $rev = '', $date_at = '')
- {
- $file = wikiFN($id, $rev);
-
- if (!file_exists($file)) {
- return '';
- }
-
- //ensure $id is in global $ID (needed for parsing)
- global $ID;
- $keep = $ID;
- $ID = $id;
-
- if ($rev || $date_at) {
- //no caching on old revisions
- $ret = p_render('dw2pdf', p_get_instructions(io_readWikiPage($file, $id, $rev)), $info, $date_at);
- } else {
- $ret = p_cached_output($file, 'dw2pdf', $id);
- }
-
- //restore ID (just in case)
- $ID = $keep;
-
- return $ret;
- }
-
- /**
- * Build a pdf from the html
- *
- * @param string $cachefile
- * @param Event $event
- * @throws MpdfException
- */
- protected function generatePDF($cachefile, $event)
- {
- global $REV, $INPUT, $DATE_AT;
-
- if ($event->data == 'export_pdf') { //only one page is exported
- $rev = $REV;
- $date_at = $DATE_AT;
- } else {
- //we are exporting entire namespace, ommit revisions
- $rev = '';
- $date_at = '';
- }
-
- //some shortcuts to export settings
- $hasToC = $this->getExportConfig('hasToC');
- $levels = $this->getExportConfig('levels');
- $isDebug = $this->getExportConfig('isDebug');
- $watermark = $this->getExportConfig('watermark');
-
- // initialize PDF library
- require_once(__DIR__ . "/DokuPDF.class.php");
-
- $mpdf = new DokuPDF(
- $this->getExportConfig('pagesize'),
- $this->getExportConfig('orientation'),
- $this->getExportConfig('font-size'),
- $this->getDocumentLanguage($this->list[0]) //use language of first page
- );
-
- // let mpdf fix local links
- $self = parse_url(DOKU_URL);
- $url = $self['scheme'] . '://' . $self['host'];
- if (!empty($self['port'])) {
- $url .= ':' . $self['port'];
- }
- $mpdf->SetBasePath($url);
-
- // Set the title
- $mpdf->SetTitle($this->title);
-
- // some default document settings
- //note: double-sided document, starts at an odd page (first page is a right-hand side page)
- // single-side document has only odd pages
- $mpdf->mirrorMargins = $this->getExportConfig('doublesided');
- $mpdf->setAutoTopMargin = 'stretch';
- $mpdf->setAutoBottomMargin = 'stretch';
-// $mpdf->pagenumSuffix = '/'; //prefix for {nbpg}
- if ($hasToC) {
- $mpdf->h2toc = $levels;
- }
- $mpdf->PageNumSubstitutions[] = ['from' => 1, 'reset' => 0, 'type' => '1', 'suppress' => 'off'];
-
- // Watermarker
- if ($watermark) {
- $mpdf->SetWatermarkText($watermark);
- $mpdf->showWatermarkText = true;
- }
-
- // load the template
- $template = $this->loadTemplate();
-
- // prepare HTML header styles
- $html = '';
- if ($isDebug) {
- $html .= '';
- $html .= '';
- $html .= '';
- }
-
- $body_start = $template['html'];
- $body_start .= '';
-
- // insert the cover page
- $body_start .= $template['cover'];
-
- $mpdf->WriteHTML($body_start, 2, true, false); //start body html
- if ($isDebug) {
- $html .= $body_start;
- }
- if ($hasToC) {
- //Note: - for double-sided document the ToC is always on an even number of pages, so that the
- // following content is on a correct odd/even page
- // - first page of ToC starts always at odd page (so eventually an additional blank page
- // is included before)
- // - there is no page numbering at the pages of the ToC
- $mpdf->TOCpagebreakByArray([
- 'toc-preHTML' => '
' . $this->getLang('tocheader') . '
',
- 'toc-bookmarkText' => $this->getLang('tocheader'),
- 'links' => true,
- 'outdent' => '1em',
- 'pagenumstyle' => '1'
- ]);
- $html .= '
';
- }
-
- // loop over all pages
- $counter = 0;
- $no_pages = count($this->list);
- foreach ($this->list as $page) {
- $this->currentBookChapter = $counter;
- $counter++;
-
- $pagehtml = $this->wikiToDW2PDF($page, $rev, $date_at);
- //file doesn't exists
- if ($pagehtml == '') {
- continue;
- }
- $pagehtml .= $this->pageDependReplacements($template['cite'], $page);
- if ($counter < $no_pages) {
- $pagehtml .= '';
- }
-
- $mpdf->WriteHTML($pagehtml, 2, false, false); //intermediate body html
- if ($isDebug) {
- $html .= $pagehtml;
- }
- }
-
- // insert the back page
- $body_end = $template['back'];
-
- $body_end .= ' ';
-
- $mpdf->WriteHTML($body_end, 2, false); // finish body html
- if ($isDebug) {
- $html .= $body_end;
- $html .= '';
- $html .= '';
- }
-
- //Return html for debugging
- if ($isDebug) {
- if ($INPUT->str('debughtml', 'text', true) == 'text') {
- header('Content-Type: text/plain; charset=utf-8');
- }
- echo $html;
- exit();
- }
-
- // write to cache file
- $mpdf->Output($cachefile, 'F');
- }
-
- /**
- * @param string $cachefile
- */
- protected function sendPDFFile($cachefile)
- {
- header('Content-Type: application/pdf');
- header('Cache-Control: must-revalidate, no-transform, post-check=0, pre-check=0');
- header('Pragma: public');
- http_conditionalRequest(filemtime($cachefile));
- global $INPUT;
- $outputTarget = $INPUT->str('outputTarget', $this->getConf('output'));
-
- $filename = rawurlencode(cleanID(strtr($this->title, ':/;"', ' ')));
- if ($outputTarget === 'file') {
- header('Content-Disposition: attachment; filename="' . $filename . '.pdf";');
- } else {
- header('Content-Disposition: inline; filename="' . $filename . '.pdf";');
- }
-
- //Bookcreator uses jQuery.fileDownload.js, which requires a cookie.
- header('Set-Cookie: fileDownload=true; path=/');
-
- //try to send file, and exit if done
- http_sendfile($cachefile);
-
- $fp = @fopen($cachefile, "rb");
- if ($fp) {
- http_rangeRequest($fp, filesize($cachefile), 'application/pdf');
- } else {
- header("HTTP/1.0 500 Internal Server Error");
- echo "Could not read file - bad permissions?";
- }
- exit();
- }
-
- /**
- * Load the various template files and prepare the HTML/CSS for insertion
- *
- * @return array
- */
- protected function loadTemplate()
- {
- global $ID;
- global $conf;
- global $INFO;
-
- // this is what we'll return
- $output = [
- 'cover' => '',
- 'back' => '',
- 'html' => '',
- 'page' => '',
- 'first' => '',
- 'cite' => '',
- ];
-
- // prepare header/footer elements
- $html = '';
- foreach (['header', 'footer'] as $section) {
- foreach (['', '_odd', '_even', '_first'] as $order) {
- $file = DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->tpl . '/' . $section . $order . '.html';
- if (file_exists($file)) {
- $html .= '' . DOKU_LF;
- $html .= file_get_contents($file) . DOKU_LF;
- $html .= '' . DOKU_LF;
-
- // register the needed pseudo CSS
- if ($order == '_first') {
- $output['first'] .= $section . ': html_' . $section . $order . ';' . DOKU_LF;
- } elseif ($order == '_even') {
- $output['page'] .= 'even-' . $section . '-name: html_' . $section . $order . ';' . DOKU_LF;
- } elseif ($order == '_odd') {
- $output['page'] .= 'odd-' . $section . '-name: html_' . $section . $order . ';' . DOKU_LF;
- } else {
- $output['page'] .= $section . ': html_' . $section . $order . ';' . DOKU_LF;
- }
- }
- }
- }
-
- // prepare replacements
- $replace = [
- '@PAGE@' => '{PAGENO}',
- '@PAGES@' => '{nbpg}', //see also $mpdf->pagenumSuffix = ' / '
- '@TITLE@' => hsc($this->title),
- '@WIKI@' => $conf['title'],
- '@WIKIURL@' => DOKU_URL,
- '@DATE@' => dformat(time()),
- '@USERNAME@' => $INFO['userinfo']['name'] ?? '',
- '@BASE@' => DOKU_BASE,
- '@INC@' => DOKU_INC,
- '@TPLBASE@' => DOKU_BASE . 'lib/plugins/dw2pdf/tpl/' . $this->tpl . '/',
- '@TPLINC@' => DOKU_INC . 'lib/plugins/dw2pdf/tpl/' . $this->tpl . '/'
- ];
-
- // set HTML element
- $html = str_replace(array_keys($replace), array_values($replace), $html);
- //TODO For bookcreator $ID (= bookmanager page) makes no sense
- $output['html'] = $this->pageDependReplacements($html, $ID);
-
- // cover page
- $coverfile = DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->tpl . '/cover.html';
- if (file_exists($coverfile)) {
- $output['cover'] = file_get_contents($coverfile);
- $output['cover'] = str_replace(array_keys($replace), array_values($replace), $output['cover']);
- $output['cover'] = $this->pageDependReplacements($output['cover'], $ID);
- $output['cover'] .= '';
- }
-
- // cover page
- $backfile = DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->tpl . '/back.html';
- if (file_exists($backfile)) {
- $output['back'] = '';
- $output['back'] .= file_get_contents($backfile);
- $output['back'] = str_replace(array_keys($replace), array_values($replace), $output['back']);
- $output['back'] = $this->pageDependReplacements($output['back'], $ID);
- }
-
- // citation box
- $citationfile = DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->tpl . '/citation.html';
- if (file_exists($citationfile)) {
- $output['cite'] = file_get_contents($citationfile);
- $output['cite'] = str_replace(array_keys($replace), array_values($replace), $output['cite']);
- }
-
- return $output;
- }
-
- /**
- * @param string $raw code with placeholders
- * @param string $id pageid
- * @return string
- */
- protected function pageDependReplacements($raw, $id)
- {
- global $REV, $DATE_AT;
-
- // generate qr code for this page
- $qr_code = '';
- if ($this->getConf('qrcodescale')) {
- $url = hsc(wl($id, '', '&', true));
- $size = (float)$this->getConf('qrcodescale');
- $qr_code = sprintf(
- '',
- $url,
- $size
- );
- }
- // prepare replacements
- $replace['@ID@'] = $id;
- $replace['@UPDATE@'] = dformat(filemtime(wikiFN($id, $REV)));
-
- $params = [];
- if ($DATE_AT) {
- $params['at'] = $DATE_AT;
- } elseif ($REV) {
- $params['rev'] = $REV;
- }
- $replace['@PAGEURL@'] = wl($id, $params, true, "&");
- $replace['@QRCODE@'] = $qr_code;
-
- $content = $raw;
-
- // let other plugins define their own replacements
- $evdata = ['id' => $id, 'replace' => &$replace, 'content' => &$content];
- $event = new Event('PLUGIN_DW2PDF_REPLACE', $evdata);
- if ($event->advise_before()) {
- $content = str_replace(array_keys($replace), array_values($replace), $raw);
- }
-
- // plugins may post-process HTML, e.g to clean up unused replacements
- $event->advise_after();
-
- // @DATE([, ])@
- $content = preg_replace_callback(
- '/@DATE\((.*?)(?:,\s*(.*?))?\)@/',
- [$this, 'replaceDate'],
- $content
- );
-
- return $content;
- }
+ $event->preventDefault();
+ $event->stopPropagation();
- /**
- * (callback) Replace date by request datestring
- * e.g. '%m(30-11-1975)' is replaced by '11'
- *
- * @param array $match with [0]=>whole match, [1]=> first subpattern, [2] => second subpattern
- * @return string
- */
- public function replaceDate($match)
- {
- global $conf;
- //no 2nd argument for default date format
- if ($match[2] == null) {
- $match[2] = $conf['dformat'];
- }
- return strftime($match[2], strtotime($match[1]));
- }
-
- /**
- * Load all the style sheets and apply the needed replacements
- *
- * @return string css styles
- */
- protected function loadCSS()
- {
- global $conf;
- //reuse the CSS dispatcher functions without triggering the main function
- define('SIMPLE_TEST', 1);
- require_once(DOKU_INC . 'lib/exe/css.php');
+ $this->loadConfig();
+ $config = new Config($this->conf);
+ $collector = CollectorFactory::create($event->data, $config, ((int) $REV) ?: null, ((int) $DATE_AT) ?: null);
+ $cache = new Cache($config, $collector);
- // prepare CSS files
- $files = array_merge(
- [
- DOKU_INC . 'lib/styles/screen.css' => DOKU_BASE . 'lib/styles/',
- DOKU_INC . 'lib/styles/print.css' => DOKU_BASE . 'lib/styles/',
- ],
- $this->cssPluginPDFstyles(),
- [
- DOKU_PLUGIN . 'dw2pdf/conf/style.css' => DOKU_BASE . 'lib/plugins/dw2pdf/conf/',
- DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->tpl . '/style.css' =>
- DOKU_BASE . 'lib/plugins/dw2pdf/tpl/' . $this->tpl . '/',
- DOKU_PLUGIN . 'dw2pdf/conf/style.local.css' => DOKU_BASE . 'lib/plugins/dw2pdf/conf/',
- ]
+ $pdfService = new PdfExportService(
+ $config,
+ $collector,
+ $cache,
+ $this->getLang('tocheader'),
+ $INPUT->server->str('REMOTE_USER', '', true)
);
- $css = '';
- foreach ($files as $file => $location) {
- $display = str_replace(fullpath(DOKU_INC), '', fullpath($file));
- $css .= "\n/* XXXXXXXXX $display XXXXXXXXX */\n";
- $css .= css_loadfile($file, $location);
- }
- // apply pattern replacements
- if (function_exists('css_styleini')) {
- // compatiblity layer for pre-Greebo releases of DokuWiki
- $styleini = css_styleini($conf['template']);
- } else {
- // Greebo functionality
- $styleUtils = new StyleUtils();
- $styleini = $styleUtils->cssStyleini($conf['template']); // older versions need still the template
- }
- $css = css_applystyle($css, $styleini['replacements']);
-
- // parse less
- return css_parseless($css);
+ $cacheFile = $pdfService->getPdf(); // dumps HTML when in debug mode and exits
+ $pdfService->sendPdf($cacheFile); //exits after sending
}
- /**
- * Returns a list of possible Plugin PDF Styles
- *
- * Checks for a pdf.css, falls back to print.css
- *
- * @author Andreas Gohr
- */
- protected function cssPluginPDFstyles()
- {
- $list = [];
- $plugins = plugin_list();
-
- $usestyle = explode(',', $this->getConf('usestyles'));
- foreach ($plugins as $p) {
- if (in_array($p, $usestyle)) {
- $list[DOKU_PLUGIN . "$p/screen.css"] = DOKU_BASE . "lib/plugins/$p/";
- $list[DOKU_PLUGIN . "$p/screen.less"] = DOKU_BASE . "lib/plugins/$p/";
-
- $list[DOKU_PLUGIN . "$p/style.css"] = DOKU_BASE . "lib/plugins/$p/";
- $list[DOKU_PLUGIN . "$p/style.less"] = DOKU_BASE . "lib/plugins/$p/";
- }
-
- $list[DOKU_PLUGIN . "$p/all.css"] = DOKU_BASE . "lib/plugins/$p/";
- $list[DOKU_PLUGIN . "$p/all.less"] = DOKU_BASE . "lib/plugins/$p/";
-
- if (file_exists(DOKU_PLUGIN . "$p/pdf.css") || file_exists(DOKU_PLUGIN . "$p/pdf.less")) {
- $list[DOKU_PLUGIN . "$p/pdf.css"] = DOKU_BASE . "lib/plugins/$p/";
- $list[DOKU_PLUGIN . "$p/pdf.less"] = DOKU_BASE . "lib/plugins/$p/";
- } else {
- $list[DOKU_PLUGIN . "$p/print.css"] = DOKU_BASE . "lib/plugins/$p/";
- $list[DOKU_PLUGIN . "$p/print.less"] = DOKU_BASE . "lib/plugins/$p/";
- }
- }
-
- // template support
- foreach (
- [
- 'pdf.css',
- 'pdf.less',
- 'css/pdf.css',
- 'css/pdf.less',
- 'styles/pdf.css',
- 'styles/pdf.less'
- ] as $file
- ) {
- if (file_exists(tpl_incdir() . $file)) {
- $list[tpl_incdir() . $file] = tpl_basedir() . $file;
- }
- }
-
- return $list;
- }
-
- /**
- * Returns array of pages which will be included in the exported pdf
- *
- * @return array
- */
- public function getExportedPages()
- {
- return $this->list;
- }
-
- /**
- * usort callback to sort by file lastmodified time
- *
- * @param array $a
- * @param array $b
- * @return int
- */
- public function cbDateSort($a, $b)
- {
- if ($b['rev'] < $a['rev']) {
- return -1;
- }
- if ($b['rev'] > $a['rev']) {
- return 1;
- }
- return strcmp($b['id'], $a['id']);
- }
-
- /**
- * usort callback to sort by page id
- * @param array $a
- * @param array $b
- * @return int
- */
- public function cbPagenameSort($a, $b)
- {
- global $conf;
-
- $partsA = explode(':', $a['id']);
- $countA = count($partsA);
- $partsB = explode(':', $b['id']);
- $countB = count($partsB);
- $max = max($countA, $countB);
-
-
- // compare namepsace by namespace
- for ($i = 0; $i < $max; $i++) {
- $partA = $partsA[$i] ?: null;
- $partB = $partsB[$i] ?: null;
-
- // have we reached the page level?
- if ($i === ($countA - 1) || $i === ($countB - 1)) {
- // start page first
- if ($partA == $conf['start']) {
- return -1;
- }
- if ($partB == $conf['start']) {
- return 1;
- }
- }
-
- // prefer page over namespace
- if ($partA === $partB) {
- if (!isset($partsA[$i + 1])) {
- return -1;
- }
- if (!isset($partsB[$i + 1])) {
- return 1;
- }
- continue;
- }
-
-
- // simply compare
- return strnatcmp($partA, $partB);
- }
-
- return strnatcmp($a['id'], $b['id']);
- }
-
- /**
- * Collects settings from:
- * 1. url parameters
- * 2. plugin config
- * 3. global config
- */
- protected function loadExportConfig()
- {
- global $INPUT;
- global $conf;
-
- $this->exportConfig = [];
-
- // decide on the paper setup from param or config
- $this->exportConfig['pagesize'] = $INPUT->str('pagesize', $this->getConf('pagesize'), true);
- $this->exportConfig['orientation'] = $INPUT->str('orientation', $this->getConf('orientation'), true);
-
- // decide on the font-size from param or config
- $this->exportConfig['font-size'] = $INPUT->str('font-size', $this->getConf('font-size'), true);
-
- $doublesided = $INPUT->bool('doublesided', (bool)$this->getConf('doublesided'));
- $this->exportConfig['doublesided'] = $doublesided ? '1' : '0';
-
- $this->exportConfig['watermark'] = $INPUT->str('watermark', '');
-
- $hasToC = $INPUT->bool('toc', (bool)$this->getConf('toc'));
- $levels = [];
- if ($hasToC) {
- $toclevels = $INPUT->str('toclevels', $this->getConf('toclevels'), true);
- [$top_input, $max_input] = array_pad(explode('-', $toclevels, 2), 2, '');
- [$top_conf, $max_conf] = array_pad(explode('-', $this->getConf('toclevels'), 2), 2, '');
- $bounds_input = [
- 'top' => [
- (int)$top_input,
- (int)$top_conf
- ],
- 'max' => [
- (int)$max_input,
- (int)$max_conf
- ]
- ];
- $bounds = [
- 'top' => $conf['toptoclevel'],
- 'max' => $conf['maxtoclevel']
-
- ];
- foreach ($bounds_input as $bound => $values) {
- foreach ($values as $value) {
- if ($value > 0 && $value <= 5) {
- //stop at valid value and store
- $bounds[$bound] = $value;
- break;
- }
- }
- }
-
- if ($bounds['max'] < $bounds['top']) {
- $bounds['max'] = $bounds['top'];
- }
-
- for ($level = $bounds['top']; $level <= $bounds['max']; $level++) {
- $levels["H$level"] = $level - 1;
- }
- }
- $this->exportConfig['hasToC'] = $hasToC;
- $this->exportConfig['levels'] = $levels;
-
- $this->exportConfig['maxbookmarks'] = $INPUT->int('maxbookmarks', $this->getConf('maxbookmarks'), true);
-
- $tplconf = $this->getConf('template');
- $tpl = $INPUT->str('tpl', $tplconf, true);
- if (!is_dir(DOKU_PLUGIN . 'dw2pdf/tpl/' . $tpl)) {
- $tpl = $tplconf;
- }
- if (!$tpl) {
- $tpl = 'default';
- }
- $this->exportConfig['template'] = $tpl;
-
- $this->exportConfig['isDebug'] = $conf['allowdebug'] && $INPUT->has('debughtml');
- }
-
- /**
- * Returns requested config
- *
- * @param string $name
- * @param mixed $notset
- * @return mixed|bool
- */
- public function getExportConfig($name, $notset = false)
- {
- if ($this->exportConfig === null) {
- $this->loadExportConfig();
- }
-
- return $this->exportConfig[$name] ?? $notset;
- }
-
- /**
- * Add 'export pdf'-button to pagetools
- *
- * @param Event $event
- */
- public function addbutton(Event $event)
- {
- global $ID, $REV, $DATE_AT;
-
- if ($this->getConf('showexportbutton') && $event->data['view'] == 'main') {
- $params = ['do' => 'export_pdf'];
- if ($DATE_AT) {
- $params['at'] = $DATE_AT;
- } elseif ($REV) {
- $params['rev'] = $REV;
- }
-
- // insert button at position before last (up to top)
- $event->data['items'] = array_slice($event->data['items'], 0, -1, true) +
- ['export_pdf' => sprintf(
- '%s',
- wl($ID, $params),
- 'action export_pdf',
- $this->getLang('export_pdf_button'),
- $this->getLang('export_pdf_button')
- )] +
- array_slice($event->data['items'], -1, 1, true);
- }
- }
/**
* Add 'export pdf' button to page tools, new SVG based mechanism
@@ -1096,27 +88,4 @@ public function addsvgbutton(Event $event)
array_splice($event->data['items'], -1, 0, [new MenuItem()]);
}
-
- /**
- * Get the language of the current document
- *
- * Uses the translation plugin if available
- * @return string
- */
- protected function getDocumentLanguage($pageid)
- {
- global $conf;
-
- $lang = $conf['lang'];
- /** @var helper_plugin_translation $trans */
- $trans = plugin_load('helper', 'translation');
- if ($trans) {
- $tr = $trans->getLangPart($pageid);
- if ($tr) {
- $lang = $tr;
- }
- }
-
- return $lang;
- }
}
diff --git a/composer.json b/composer.json
index 65cf6e8b..0b526e85 100644
--- a/composer.json
+++ b/composer.json
@@ -8,12 +8,15 @@
],
"config": {
"platform": {
- "php": "7.2"
+ "php": "8.0"
}
},
"require": {
- "mpdf/mpdf": "8.0.*",
- "mpdf/qrcode": "^1.2"
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "mpdf/mpdf": "8.2.*",
+ "mpdf/qrcode": "^1.2",
+ "psr/log": "^1.0"
},
"replace": {
"paragonie/random_compat": "*"
diff --git a/composer.lock b/composer.lock
index c66b74c8..30af5c10 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,36 +4,39 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "7b00711d12ac1ef73bc21f9046c21eea",
+ "content-hash": "70e3f425d861b01c41a304e53af0cadc",
"packages": [
{
"name": "mpdf/mpdf",
- "version": "v8.0.17",
+ "version": "v8.2.7",
"source": {
"type": "git",
"url": "https://github.com/mpdf/mpdf.git",
- "reference": "5f64118317c8145c0abc606b310aa0a66808398a"
+ "reference": "b59670a09498689c33ce639bac8f5ba26721dab3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/mpdf/mpdf/zipball/5f64118317c8145c0abc606b310aa0a66808398a",
- "reference": "5f64118317c8145c0abc606b310aa0a66808398a",
+ "url": "https://api.github.com/repos/mpdf/mpdf/zipball/b59670a09498689c33ce639bac8f5ba26721dab3",
+ "reference": "b59670a09498689c33ce639bac8f5ba26721dab3",
"shasum": ""
},
"require": {
"ext-gd": "*",
"ext-mbstring": "*",
+ "mpdf/psr-http-message-shim": "^1.0 || ^2.0",
+ "mpdf/psr-log-aware-trait": "^2.0 || ^3.0",
"myclabs/deep-copy": "^1.7",
"paragonie/random_compat": "^1.4|^2.0|^9.99.99",
- "php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0",
- "psr/log": "^1.0 || ^2.0",
+ "php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
+ "psr/http-message": "^1.0 || ^2.0",
+ "psr/log": "^1.0 || ^2.0 || ^3.0",
"setasign/fpdi": "^2.1"
},
"require-dev": {
"mockery/mockery": "^1.3.0",
"mpdf/qrcode": "^1.1.0",
"squizlabs/php_codesniffer": "^3.5.0",
- "tracy/tracy": "^2.4",
+ "tracy/tracy": "~2.5",
"yoast/phpunit-polyfills": "^1.0"
},
"suggest": {
@@ -43,6 +46,9 @@
},
"type": "library",
"autoload": {
+ "files": [
+ "src/functions.php"
+ ],
"psr-4": {
"Mpdf\\": "src/"
}
@@ -69,7 +75,7 @@
"utf-8"
],
"support": {
- "docs": "http://mpdf.github.io",
+ "docs": "https://mpdf.github.io",
"issues": "https://github.com/mpdf/mpdf/issues",
"source": "https://github.com/mpdf/mpdf"
},
@@ -79,20 +85,112 @@
"type": "custom"
}
],
- "time": "2022-01-20T10:51:40+00:00"
+ "time": "2025-12-01T10:18:02+00:00"
+ },
+ {
+ "name": "mpdf/psr-http-message-shim",
+ "version": "v2.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/mpdf/psr-http-message-shim.git",
+ "reference": "f25a0153d645e234f9db42e5433b16d9b113920f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/mpdf/psr-http-message-shim/zipball/f25a0153d645e234f9db42e5433b16d9b113920f",
+ "reference": "f25a0153d645e234f9db42e5433b16d9b113920f",
+ "shasum": ""
+ },
+ "require": {
+ "psr/http-message": "^2.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Mpdf\\PsrHttpMessageShim\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mark Dorison",
+ "email": "mark@chromatichq.com"
+ },
+ {
+ "name": "Kristofer Widholm",
+ "email": "kristofer@chromatichq.com"
+ },
+ {
+ "name": "Nigel Cunningham",
+ "email": "nigel.cunningham@technocrat.com.au"
+ }
+ ],
+ "description": "Shim to allow support of different psr/message versions.",
+ "support": {
+ "issues": "https://github.com/mpdf/psr-http-message-shim/issues",
+ "source": "https://github.com/mpdf/psr-http-message-shim/tree/v2.0.1"
+ },
+ "time": "2023-10-02T14:34:03+00:00"
+ },
+ {
+ "name": "mpdf/psr-log-aware-trait",
+ "version": "v2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/mpdf/psr-log-aware-trait.git",
+ "reference": "7a077416e8f39eb626dee4246e0af99dd9ace275"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/mpdf/psr-log-aware-trait/zipball/7a077416e8f39eb626dee4246e0af99dd9ace275",
+ "reference": "7a077416e8f39eb626dee4246e0af99dd9ace275",
+ "shasum": ""
+ },
+ "require": {
+ "psr/log": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Mpdf\\PsrLogAwareTrait\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mark Dorison",
+ "email": "mark@chromatichq.com"
+ },
+ {
+ "name": "Kristofer Widholm",
+ "email": "kristofer@chromatichq.com"
+ }
+ ],
+ "description": "Trait to allow support of different psr/log versions.",
+ "support": {
+ "issues": "https://github.com/mpdf/psr-log-aware-trait/issues",
+ "source": "https://github.com/mpdf/psr-log-aware-trait/tree/v2.0.0"
+ },
+ "time": "2023-05-03T06:18:28+00:00"
},
{
"name": "mpdf/qrcode",
- "version": "v1.2.0",
+ "version": "v1.2.1",
"source": {
"type": "git",
"url": "https://github.com/mpdf/qrcode.git",
- "reference": "0c09fce8b28707611c3febdd1ca424d40f172184"
+ "reference": "5320c512776aa3c199bd8be8f707ec83d9779d85"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/mpdf/qrcode/zipball/0c09fce8b28707611c3febdd1ca424d40f172184",
- "reference": "0c09fce8b28707611c3febdd1ca424d40f172184",
+ "url": "https://api.github.com/repos/mpdf/qrcode/zipball/5320c512776aa3c199bd8be8f707ec83d9779d85",
+ "reference": "5320c512776aa3c199bd8be8f707ec83d9779d85",
"shasum": ""
},
"require": {
@@ -139,7 +237,7 @@
],
"support": {
"issues": "https://github.com/mpdf/qrcode/issues",
- "source": "https://github.com/mpdf/qrcode/tree/v1.2.0"
+ "source": "https://github.com/mpdf/qrcode/tree/v1.2.1"
},
"funding": [
{
@@ -147,41 +245,43 @@
"type": "custom"
}
],
- "time": "2022-01-11T09:42:41+00:00"
+ "time": "2024-06-04T13:40:39+00:00"
},
{
"name": "myclabs/deep-copy",
- "version": "1.10.1",
+ "version": "1.13.4",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5"
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5",
- "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
- "replace": {
- "myclabs/deep-copy": "self.version"
+ "conflict": {
+ "doctrine/collections": "<1.6.8",
+ "doctrine/common": "<2.13.3 || >=3 <3.2.2"
},
"require-dev": {
- "doctrine/collections": "^1.0",
- "doctrine/common": "^2.6",
- "phpunit/phpunit": "^7.1"
+ "doctrine/collections": "^1.6.8",
+ "doctrine/common": "^2.13.3 || ^3.2.2",
+ "phpspec/prophecy": "^1.10",
+ "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
},
"type": "library",
"autoload": {
- "psr-4": {
- "DeepCopy\\": "src/DeepCopy/"
- },
"files": [
"src/DeepCopy/deep_copy.php"
- ]
+ ],
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ }
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@@ -195,20 +295,83 @@
"object",
"object graph"
],
- "time": "2020-06-29T13:22:24+00:00"
+ "support": {
+ "issues": "https://github.com/myclabs/DeepCopy/issues",
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-01T08:46:24+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/2.0"
+ },
+ "time": "2023-04-04T09:54:51+00:00"
},
{
"name": "psr/log",
- "version": "1.1.3",
+ "version": "1.1.4",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
- "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
+ "reference": "d49695b909c3b7628b6289db5479a1c204601f11"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
- "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
+ "reference": "d49695b909c3b7628b6289db5479a1c204601f11",
"shasum": ""
},
"require": {
@@ -232,7 +395,7 @@
"authors": [
{
"name": "PHP-FIG",
- "homepage": "http://www.php-fig.org/"
+ "homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
@@ -242,34 +405,38 @@
"psr",
"psr-3"
],
- "time": "2020-03-23T09:12:05+00:00"
+ "support": {
+ "source": "https://github.com/php-fig/log/tree/1.1.4"
+ },
+ "time": "2021-05-03T11:20:27+00:00"
},
{
"name": "setasign/fpdi",
- "version": "v2.3.3",
+ "version": "v2.6.4",
"source": {
"type": "git",
"url": "https://github.com/Setasign/FPDI.git",
- "reference": "50c388860a73191e010810ed57dbed795578e867"
+ "reference": "4b53852fde2734ec6a07e458a085db627c60eada"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Setasign/FPDI/zipball/50c388860a73191e010810ed57dbed795578e867",
- "reference": "50c388860a73191e010810ed57dbed795578e867",
+ "url": "https://api.github.com/repos/Setasign/FPDI/zipball/4b53852fde2734ec6a07e458a085db627c60eada",
+ "reference": "4b53852fde2734ec6a07e458a085db627c60eada",
"shasum": ""
},
"require": {
"ext-zlib": "*",
- "php": "^5.6 || ^7.0"
+ "php": "^7.1 || ^8.0"
},
"conflict": {
"setasign/tfpdf": "<1.31"
},
"require-dev": {
- "phpunit/phpunit": "~5.7",
- "setasign/fpdf": "~1.8",
- "setasign/tfpdf": "1.31",
- "tecnickcom/tcpdf": "~6.2"
+ "phpunit/phpunit": "^7",
+ "setasign/fpdf": "~1.8.6",
+ "setasign/tfpdf": "~1.33",
+ "squizlabs/php_codesniffer": "^3.5",
+ "tecnickcom/tcpdf": "^6.8"
},
"suggest": {
"setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured."
@@ -303,19 +470,32 @@
"fpdi",
"pdf"
],
- "time": "2020-04-28T12:40:35+00:00"
+ "support": {
+ "issues": "https://github.com/Setasign/FPDI/issues",
+ "source": "https://github.com/Setasign/FPDI/tree/v2.6.4"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/setasign/fpdi",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-05T09:57:14+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
- "stability-flags": [],
+ "stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
- "platform": [],
- "platform-dev": [],
+ "platform": {
+ "ext-dom": "*",
+ "ext-libxml": "*"
+ },
+ "platform-dev": {},
"platform-overrides": {
- "php": "7.2"
+ "php": "8.0"
},
"plugin-api-version": "2.6.0"
}
diff --git a/conf/default.php b/conf/default.php
index 77aae8d1..df4ff749 100644
--- a/conf/default.php
+++ b/conf/default.php
@@ -8,8 +8,9 @@
$conf['toclevels'] = '';
$conf['headernumber'] = 0;
$conf['maxbookmarks'] = 5;
+$conf['watermark'] = '';
$conf['template'] = 'default';
-$conf['output'] = 'file';
+$conf['output'] = 'browser';
$conf['usecache'] = 1;
$conf['usestyles'] = 'wrap,';
$conf['qrcodescale'] = '1';
diff --git a/conf/metadata.php b/conf/metadata.php
index 321242f0..ec08ae1c 100644
--- a/conf/metadata.php
+++ b/conf/metadata.php
@@ -8,6 +8,7 @@
$meta['toclevels'] = array('string', '_pattern' => '/^(|[1-5]-[1-5])$/');
$meta['headernumber'] = array('onoff');
$meta['maxbookmarks'] = array('numeric');
+$meta['watermark'] = array('string');
$meta['template'] = array('dirchoice', '_dir' => DOKU_PLUGIN . 'dw2pdf/tpl/');
$meta['output'] = array('multichoice', '_choices' => array('browser', 'file'));
$meta['usecache'] = array('onoff');
diff --git a/lang/en/settings.php b/lang/en/settings.php
index facc067c..56538f62 100644
--- a/lang/en/settings.php
+++ b/lang/en/settings.php
@@ -16,6 +16,7 @@
$lang['toclevels'] = 'Define top level and maximum level depth which are added to ToC. Default wiki ToC levels toptoclevel and maxtoclevel are used. Format: <top>-<max>';
$lang['headernumber'] = 'Activate numbered headings';
$lang['maxbookmarks'] = 'How many section levels should be used in the PDF bookmarks? (0=none, 5=all)';
+$lang['watermark'] = 'Optional watermark text to show on each page (e.g. "CONFIDENTIAL")';
$lang['template'] = 'Which template should be used for formatting the PDFs?';
$lang['output'] = 'How should the PDF be presented to the user?';
$lang['output_o_browser'] = 'Show in browser';
diff --git a/plugin.info.txt b/plugin.info.txt
index 9a027e52..5eb53355 100644
--- a/plugin.info.txt
+++ b/plugin.info.txt
@@ -5,4 +5,5 @@ date 2023-11-25
name Dw2Pdf plugin
desc DokuWiki to PDF converter
url https://www.dokuwiki.org/plugin:dw2pdf
+phpmin 8.0
diff --git a/renderer.php b/renderer.php
index 8315c864..6e553f52 100644
--- a/renderer.php
+++ b/renderer.php
@@ -2,6 +2,8 @@
// phpcs:disable: PSR1.Methods.CamelCapsMethodName.NotCamelCaps
// phpcs:disable: PSR2.Methods.MethodDeclaration.Underscore
+use dokuwiki\plugin\dw2pdf\src\AbstractCollector;
+use dokuwiki\plugin\dw2pdf\src\Config;
/**
* DokuWiki Plugin dw2pdf (Renderer Component)
@@ -15,38 +17,52 @@ class renderer_plugin_dw2pdf extends Doku_Renderer_xhtml
private $lastHeaderLevel = -1;
private $originalHeaderLevel = 0;
private $difference = 0;
- private static $header_count = [];
- private static $previous_level = 0;
+ private $header_count = [];
+ private $previous_level = 0;
+ private int $chapter = 0;
+ private ?Config $config;
/**
- * Stores action instance
+ * The Writer will reinitialize the renderer for each export, but the object will be reused within one export.
*
- * @var action_plugin_dw2pdf
+ * @inheritdoc
*/
- private $actioninstance;
+ public function isSingleton()
+ {
+ return true;
+ }
/**
- * load action plugin instance
+ * Set the active configuration
+ *
+ * @param Config $config
+ * @return void
*/
- public function __construct()
+ public function setConfig(Config $config): void
{
- $this->actioninstance = plugin_load('action', 'dw2pdf');
+ $this->config = $config;
}
public function document_start()
{
global $ID;
+ if($this->config === null) {
+ throw new RuntimeException('DW2PDF Renderer configuration not set');
+ }
+
parent::document_start();
- //ancher for rewritten links to included pages
+ //anchor for rewritten links to included pages
$check = false;
$pid = sectionID($ID, $check);
$this->doc .= "";
$this->doc .= "";
- self::$header_count[1] = $this->actioninstance->getCurrentBookChapter();
+
+ $this->header_count[1] = $this->chapter;
+ $this->chapter++;
}
/**
@@ -89,28 +105,31 @@ public function header($text, $level, $pos, $returnonly = false)
// retrieve numbered headings option
- $isnumberedheadings = $this->actioninstance->getExportConfig('headernumber');
+ $isnumberedheadings = $this->config->useNumberedHeaders();
- $header_prefix = "";
+ $header_prefix = '';
if ($isnumberedheadings) {
if ($level > 0) {
- if (self::$previous_level > $level) {
- for ($i = $level + 1; $i <= self::$previous_level; $i++) {
- self::$header_count[$i] = 0;
+ if ($this->previous_level > $level) {
+ for ($i = $level + 1; $i <= $this->previous_level; $i++) {
+ $this->header_count[$i] = 0;
}
}
}
- self::$header_count[$level]++;
+ $this->header_count[$level] = ($this->header_count[$level] ?? 0) + 1;
// $header_prefix = "";
for ($i = 1; $i <= $level; $i++) {
- $header_prefix .= self::$header_count[$i] . ".";
+ $header_prefix .= $this->header_count[$i] . ".";
}
}
+ if($header_prefix !== '') {
+ $header_prefix .= ' ';
+ }
// add PDF bookmark
$bookmark = '';
- $maxbookmarklevel = $this->actioninstance->getExportConfig('maxbookmarks');
+ $maxbookmarklevel = $this->config->getMaxBookmarks();
// 0: off, 1-6: show down to this level
if ($maxbookmarklevel && $maxbookmarklevel >= $level) {
$bookmarklevel = $this->calculateBookmarklevel($level);
@@ -128,7 +147,7 @@ public function header($text, $level, $pos, $returnonly = false)
$this->doc .= $this->_xmlEntities($text);
$this->doc .= "";
$this->doc .= "" . DOKU_LF;
- self::$previous_level = $level;
+ $this->previous_level = $level;
}
/**
@@ -238,19 +257,29 @@ public function acronym($acronym)
/**
* reformat links if needed
*
+ * Because the output of this renderer will be cached, but might be part of a larger PDF
+ * including multiple pages, the links are not rewritten here.
+ * Instead they will be rewritten in the created HTML after rendering but before feeding to mPDF.
+ *
* @param array $link
* @return string
*/
public function _formatLink($link)
{
-
- // for internal links contains the title the pageid
- if (in_array($link['title'], $this->actioninstance->getExportedPages())) {
- [/* url */, $hash] = sexplode('#', $link['url'], 2, '');
-
- $check = false;
- $pid = sectionID($link['title'], $check);
- $link['url'] = "#" . $pid . '__' . $hash;
+ // mark internal wiki links for later processing by the writer
+ if (
+ !empty($link['more']) &&
+ str_contains($link['more'], 'data-wiki-id=')
+ ) {
+ [, $hash] = sexplode('#', $link['url'], 2, '');
+ $target = $link['title'] ?? ''; // for internal links, 'title' holds the target page id
+ if ($target !== '') {
+ $attrs = ['data-dw2pdf-target="' . hsc($target) . '"'];
+ if ($hash !== '') {
+ $attrs[] = 'data-dw2pdf-hash="' . hsc($hash) . '"';
+ }
+ $link['more'] = trim($link['more'] . ' ' . implode(' ', $attrs));
+ }
}
// prefix interwiki links with interwiki icon
@@ -277,7 +306,6 @@ public function _formatLink($link)
* no obfuscation for email addresses
*
* @param string $address
- * @param null $name
* @param bool $returnonly
* @return string|void
*/
diff --git a/src/AbstractCollector.php b/src/AbstractCollector.php
new file mode 100644
index 00000000..9c5c293b
--- /dev/null
+++ b/src/AbstractCollector.php
@@ -0,0 +1,143 @@
+config = $config;
+ $this->rev = $rev;
+ $this->at = $at;
+ $this->title = $config->getBookTitle() ?? '';
+
+ // collected pages are cleaned and checked for read access
+ $this->pages = array_filter(
+ array_map('cleanID', $this->collect()),
+ fn($page) => auth_quickaclcheck($page) >= AUTH_READ
+ );
+ }
+
+ /**
+ * Get the combined configuration/request context for this export.
+ *
+ * @return Config
+ */
+ protected function getConfig(): Config
+ {
+ return $this->config;
+ }
+
+ /**
+ * Collect the pages to be included in the PDF
+ *
+ * The collected pages will be cleaned and checked for read access automatically.
+ *
+ * This method should check for page existence, though (might depend on $rev/$at).
+ *
+ * @return string[] The list of page ids
+ */
+ abstract protected function collect(): array;
+
+ /**
+ * Get the title to be used for the PDF
+ *
+ * @return string
+ */
+ public function getTitle(): string
+ {
+ if (!$this->title && $this->pages) {
+ $this->title = p_get_first_heading($this->pages[0]) ?: noNS($this->pages[0]);
+ }
+
+ if (!$this->title) {
+ $this->title = 'PDF Export';
+ }
+
+ return $this->title;
+ }
+
+ /**
+ * Get the language to be used for the PDF
+ *
+ * Use the language of the first page if possible, otherwise fall back to the default language
+ *
+ * @return string
+ */
+ public function getLanguage()
+ {
+ global $conf;
+
+ $lang = $conf['lang'];
+ if ($this->pages == []) return $lang;
+
+
+ /** @var helper_plugin_translation $trans */
+ $trans = plugin_load('helper', 'translation');
+ if (!$trans) return $lang;
+ $tr = $trans->getLangPart($this->pages[0]);
+ if ($tr) return $tr;
+
+ return $lang;
+ }
+
+ /**
+ * Get the set revision if any
+ *
+ * @return int|null
+ */
+ public function getRev(): ?int
+ {
+ return $this->rev;
+ }
+
+ /**
+ * Get the set dateat timestamp if any
+ *
+ * @return int|null
+ */
+ public function getAt(): ?int
+ {
+ return $this->at;
+ }
+
+ /**
+ * Get the list of page ids to include in the PDF
+ *
+ * @return string[]
+ */
+ public function getPages(): array
+ {
+ return $this->pages;
+ }
+
+ /**
+ * Get the list of file paths to include in the PDF
+ *
+ * Handles $rev if set
+ *
+ * @return string[]
+ * @todo no handling of $at yet
+ */
+ public function getFiles(): array
+ {
+ return array_map(fn($id) => wikiFN($id, $this->rev), $this->pages);
+ }
+}
diff --git a/src/BookCreatorLiveSelectionCollector.php b/src/BookCreatorLiveSelectionCollector.php
new file mode 100644
index 00000000..057c2dba
--- /dev/null
+++ b/src/BookCreatorLiveSelectionCollector.php
@@ -0,0 +1,18 @@
+getConfig()->getLiveSelection();
+ if ($selection === null) return [];
+ $list = (array)json_decode($selection, true, 512, JSON_THROW_ON_ERROR);
+ return array_filter($list, fn($page) => page_exists($page));
+ }
+}
diff --git a/src/BookCreatorSavedSelectionCollector.php b/src/BookCreatorSavedSelectionCollector.php
new file mode 100644
index 00000000..00720ddd
--- /dev/null
+++ b/src/BookCreatorSavedSelectionCollector.php
@@ -0,0 +1,28 @@
+getConfig()->getSavedSelection();
+ if ($savedSelectionId === null) return [];
+
+ $savedselection = $bcPlugin->loadSavedSelection($savedSelectionId);
+ if (!$this->title && !empty($savedselection['title'])) {
+ $this->title = $savedselection['title'];
+ }
+
+ $list = (array) $savedselection['selection'];
+ return array_filter($list, fn($page) => page_exists($page));
+ }
+}
diff --git a/src/Cache.php b/src/Cache.php
new file mode 100644
index 00000000..004e16e2
--- /dev/null
+++ b/src/Cache.php
@@ -0,0 +1,90 @@
+collector = $collector;
+
+ $pages = $collector->getPages();
+ sort($pages);
+ $key = implode(':', [
+ implode(',', $pages),
+ $config->getCacheKey(),
+ $collector->getTitle(),
+ ]);
+
+ parent::__construct($key, '.dw2.pdf');
+
+ $this->addDependencies();
+ }
+
+ /**
+ * When this was a cache for a specific revision, remove it on destruction
+ */
+ public function __destruct()
+ {
+ if (!$this->collector->getRev()) return;
+ $this->removeCache();
+ }
+
+ /** @inheritdoc */
+ public function useCache($depends = [])
+ {
+ // when a specific revision is requested, do not use the cache
+ if ($this->collector->getRev()) {
+ return false;
+ }
+
+ return parent::useCache($depends);
+ }
+
+ /**
+ * Note: we do not set up any dependencies to the plugin source code itself. On normal installation,
+ * the config files will be touched which will invalidate the cache. During development, the developer
+ * should manually purge the cache when changing the plugin code.
+ * @inheritdoc
+ */
+ protected function addDependencies()
+ {
+ parent::addDependencies();
+
+ // images and included pages
+ $dependencies = [];
+ foreach ($this->collector->getPages() as $pageid) {
+ $relations = p_get_metadata($pageid, 'relation');
+
+ if (is_array($relations)) {
+ if (array_key_exists('media', $relations) && is_array($relations['media'])) {
+ foreach ($relations['media'] as $mediaid => $exists) {
+ if ($exists) {
+ $dependencies[] = mediaFN($mediaid);
+ }
+ }
+ }
+
+ if (array_key_exists('haspart', $relations) && is_array($relations['haspart'])) {
+ foreach ($relations['haspart'] as $part_pageid => $exists) {
+ if ($exists) {
+ $dependencies[] = wikiFN($part_pageid);
+ }
+ }
+ }
+ }
+
+ $dependencies[] = metaFN($pageid, '.meta');
+ }
+
+ // set up the dependencies
+ $this->depends['files'] = array_merge(
+ $dependencies,
+ $this->collector->getFiles(),
+ getConfigFiles('main')
+ );
+ }
+}
diff --git a/src/CollectorFactory.php b/src/CollectorFactory.php
new file mode 100644
index 00000000..650ccfba
--- /dev/null
+++ b/src/CollectorFactory.php
@@ -0,0 +1,35 @@
+hasLiveSelection()) {
+ return new BookCreatorLiveSelectionCollector($config, $rev, $at);
+ } elseif ($config->hasSavedSelection()) {
+ return new BookCreatorSavedSelectionCollector($config, $rev, $at);
+ }
+ // fallthrough
+ default:
+ throw new \InvalidArgumentException('Invalid export configuration');
+ }
+ }
+}
diff --git a/src/Config.php b/src/Config.php
new file mode 100644
index 00000000..a6375532
--- /dev/null
+++ b/src/Config.php
@@ -0,0 +1,450 @@
+tempDir = $conf['tmpdir'] . '/mpdf';
+ io_mkdir_p($this->tempDir);
+
+ // set default ToC levels from main config
+ $this->tocLevels = $this->parseTocLevels($conf['toptoclevel'] . '-' . $conf['maxtoclevel']);
+
+ $this->loadPluginConfig($pluginConf);
+ $this->loadInputConfig();
+ }
+
+ /**
+ * Set a property with type casting and custom parsing
+ *
+ * @param string $prop The property name to set
+ * @param string|null $type The property type
+ * @param mixed $value The value to set
+ * @return void
+ * @see loadPluginConfig
+ * @see loadInputConfig
+ */
+ protected function setProperty(string $prop, ?string $type, $value)
+ {
+ // custom value parsing (lowercased property names to avoid mistakes)
+ $value = match (strtolower($prop)) {
+ 'islandscape' => ($value === 'landscape'),
+ 'toclevels' => is_array($value) ? $value : $this->parseTocLevels((string)$value),
+ 'exportid' => cleanID((string)$value),
+ default => $value,
+ };
+
+ // standard type casting
+ $this->$prop = match ($type) {
+ 'int' => (int)$value,
+ 'bool' => (bool)$value,
+ 'float' => (float)$value,
+ 'array' => is_array($value)
+ ? $value
+ : array_filter(array_map('trim', explode(',', (string)$value))),
+ default => $value,
+ };
+ }
+
+ /**
+ * Apply the given configuration
+ *
+ * This will set all properties annotated with FromConfig
+ *
+ * @param array $conf (Plugin) configuration
+ */
+ public function loadPluginConfig(array $conf = [])
+ {
+ $reflection = new \ReflectionClass($this);
+ foreach ($reflection->getProperties() as $property) {
+ $attributes = $property->getAttributes(FromConfig::class);
+ if ($attributes === []) continue;
+ $attribute = $attributes[0]->newInstance(); // we only expect one
+
+ $prop = $property->getName();
+ $confName = $attribute->name ?? strtolower($prop);
+ $type = $property->getType()?->getName();
+
+ if (!isset($conf[$confName])) continue;
+ $this->setProperty($prop, $type, $conf[$confName]);
+ }
+ }
+
+ /**
+ * Load configuration provided by INPUT parameters
+ *
+ * Not all parameters are overridable here
+ *
+ * @return void
+ */
+ public function loadInputConfig()
+ {
+ global $INPUT, $ID;
+
+ if ($ID) $this->exportId = $ID; // default exportId to current page ID
+
+ $reflection = new \ReflectionClass($this);
+ foreach ($reflection->getProperties() as $property) {
+ $attributes = $property->getAttributes(FromInput::class);
+ if ($attributes === []) continue;
+ $attribute = $attributes[0]->newInstance(); // we only expect one
+
+ $prop = $property->getName();
+ $confName = $attribute->name ?? strtolower($prop);
+ $type = $property->getType()?->getName();
+
+ if (!$INPUT->has($confName)) continue;
+ $this->setProperty($prop, $type, $INPUT->param($confName));
+ }
+ }
+
+ /**
+ * Check whether ToC is enabled
+ *
+ * @return bool
+ */
+ public function hasToc(): bool
+ {
+ return $this->hasToC;
+ }
+
+ /**
+ * Check whether debug mode is enabled
+ *
+ * @return bool
+ */
+ public function isDebugEnabled(): bool
+ {
+ return $this->isDebug;
+ }
+
+ /**
+ * Get a list of extensions whose screen styles should be applied
+ *
+ * @return string[]
+ */
+ public function getStyledExtensions(): array
+ {
+ return $this->useStyles;
+ }
+
+ /**
+ * Get the name of the selected template
+ *
+ * @return string
+ */
+ public function getTemplateName(): string
+ {
+ return $this->template;
+ }
+
+ /**
+ * Get the maximum number of bookmarks to include
+ *
+ * @return int
+ */
+ public function getMaxBookmarks(): int
+ {
+ return $this->maxBookmarks;
+ }
+
+ /**
+ * Check whether numbered headers are to be used
+ *
+ * @return bool
+ */
+ public function useNumberedHeaders(): bool
+ {
+ return $this->numberedHeaders;
+ }
+
+ /**
+ * Get the QR code scale
+ *
+ * @return float
+ */
+ public function getQRScale(): float
+ {
+ return $this->qrCodeScale;
+ }
+
+ /**
+ * Get desired PDF delivery target (inline or file download)
+ *
+ * @return string
+ */
+ public function getOutputTarget(): string
+ {
+ return $this->outputTarget;
+ }
+
+ /**
+ * Get a unique cache key for the current configuration
+ *
+ * @return string
+ */
+ public function getCacheKey(): string
+ {
+ return implode(',', [
+ $this->template,
+ $this->pagesize,
+ $this->isLandscape ? 'L' : 'P',
+ $this->fontSize,
+ $this->isDoublesided ? 'D' : 'S',
+ $this->hasToC ? 'T' : 'N',
+ $this->maxBookmarks,
+ $this->numberedHeaders ? 'H' : 'N',
+ implode('-', $this->tocLevels)
+ ]);
+ }
+
+ /**
+ * Parses the ToC levels configuration into an array
+ *
+ * @param string $toclevels eg. "2-4"
+ * @return array
+ */
+ protected function parseTocLevels(string $toclevels): array
+ {
+ $levels = [];
+ [$top, $max] = sexplode('-', $toclevels, 2);
+ $top = max(1, min(5, (int)$top));
+ $max = max(1, min(5, (int)$max));
+
+ if ($max < $top) {
+ $max = $top;
+ }
+
+ for ($level = $top; $level <= $max; $level++) {
+ $levels["H$level"] = $level - 1;
+ }
+
+ return $levels;
+ }
+
+
+ /**
+ * Return the paper format
+ *
+ * @return string
+ */
+ public function getFormat()
+ {
+ $format = $this->pagesize;
+ if ($this->isLandscape) {
+ $format .= '-L';
+ }
+ return $format;
+ }
+
+ /**
+ * Return the watermark text if any
+ *
+ * @return string
+ */
+ public function getWatermarkText(): string
+ {
+ return $this->watermark;
+ }
+
+ /**
+ * Get all configuration for mpdf as array
+ *
+ * Note: mode and writing direction are set in DokuPDF based on the language
+ *
+ * @link https://mpdf.github.io/reference/mpdf-variables/overview.html
+ * @return array
+ */
+ public function getMPdfConfig(): array
+ {
+ return [
+ 'format' => $this->getFormat(),
+ 'default_font_size' => $this->fontSize,
+ 'tempDir' => $this->tempDir,
+ 'mirrorMargins' => $this->isDoublesided,
+ 'h2toc' => $this->hasToC ? $this->tocLevels : [],
+ 'showWatermarkText' => $this->watermark !== '',
+
+ 'setAutoTopMargin' => 'stretch',
+ 'setAutoBottomMargin' => 'stretch',
+ 'autoScriptToLang' => true,
+ 'baseScript' => 1,
+ 'autoVietnamese' => true,
+ 'autoArabic' => true,
+ 'autoLangToFont' => true,
+ 'ignore_invalid_utf8' => true,
+ 'tabSpaces' => 4,
+ ];
+ }
+
+ /**
+ * Get the requested book title override if provided.
+ *
+ * @return string|null
+ */
+ public function getBookTitle(): ?string
+ {
+ return $this->bookTitle;
+ }
+
+ /**
+ * Get the namespace to export for namespace/book selections.
+ *
+ * @return string
+ */
+ public function getBookNamespace(): string
+ {
+ return $this->bookNamespace;
+ }
+
+ /**
+ * Get the page sort order selected for namespace exports.
+ *
+ * @return string
+ */
+ public function getBookSortOrder(): string
+ {
+ return $this->bookSortOrder;
+ }
+
+ /**
+ * Get the maximum namespace depth to traverse for namespace exports.
+ *
+ * @return int
+ */
+ public function getBookNamespaceDepth(): int
+ {
+ return $this->bookNamespaceDepth;
+ }
+
+ /**
+ * Get the list of explicitly excluded page IDs.
+ *
+ * @return string[]
+ */
+ public function getBookExcludedPages(): array
+ {
+ return $this->bookExcludePages;
+ }
+
+ /**
+ * Get the list of excluded namespaces.
+ *
+ * @return string[]
+ */
+ public function getBookExcludedNamespaces(): array
+ {
+ return $this->bookExcludeNamespaces;
+ }
+
+ /**
+ * Get the raw JSON payload representing a live book selection.
+ *
+ * @return string|null
+ */
+ public function getLiveSelection(): ?string
+ {
+ return $this->liveSelection;
+ }
+
+ /**
+ * Check whether a live selection payload was supplied.
+ *
+ * @return bool
+ */
+ public function hasLiveSelection(): bool
+ {
+ return $this->liveSelection !== null;
+ }
+
+ /**
+ * Get the identifier of a saved book selection.
+ *
+ * @return string|null
+ */
+ public function getSavedSelection(): ?string
+ {
+ return $this->savedSelection;
+ }
+
+ /**
+ * Check whether a saved selection identifier was supplied.
+ *
+ * @return bool
+ */
+ public function hasSavedSelection(): bool
+ {
+ return $this->savedSelection !== null;
+ }
+
+ /**
+ * Get the requested page ID for single page exports.
+ *
+ * @return string
+ */
+ public function getExportId(): string
+ {
+ return $this->exportId;
+ }
+}
diff --git a/src/DokuPdf.php b/src/DokuPdf.php
new file mode 100644
index 00000000..d8a826a4
--- /dev/null
+++ b/src/DokuPdf.php
@@ -0,0 +1,105 @@
+
+ */
+class DokuPdf extends Mpdf
+{
+ /**
+ * DokuPDF constructor.
+ *
+ * @param Config $config
+ * @param string $lang The language code to use for this document
+ * @throws MpdfException
+ */
+ public function __construct(Config $config, string $lang)
+ {
+ $initConfig = $config->getMPdfConfig();
+ $initConfig['mode'] = $this->lang2mode($lang);
+
+ $container = new SimpleContainer([
+ 'httpClient' => new HttpClient(),
+ 'localContentLoader' => new LocalContentLoader(),
+ ]);
+
+ parent::__construct($initConfig, $container);
+ $this->SetDirectionality($this->lang2direction($lang));
+
+ // configure page numbering
+ // https://mpdf.github.io/paging/page-numbering.html
+ $this->PageNumSubstitutions[] = ['from' => 1, 'reset' => 0, 'type' => '1', 'suppress' => 'off'];
+ // add watermark text if configured
+ $this->setWatermarkText($config->getWatermarkText());
+
+ // let mpdf fix local links
+ $self = parse_url(DOKU_URL);
+ $url = $self['scheme'] . '://' . $self['host'];
+ if (!empty($self['port'])) {
+ $url .= ':' . $self['port'];
+ }
+ $this->SetBasePath($url);
+ }
+
+ /**
+ * Decode all paths, since DokuWiki uses XHTML compliant URLs
+ *
+ * @inheritdoc
+ */
+ public function GetFullPath(&$path, $basepath = '')
+ {
+ $path = htmlspecialchars_decode($path);
+ parent::GetFullPath($path, $basepath);
+ }
+
+ /**
+ * Get the mode to use based on the given language
+ *
+ * @link https://mpdf.github.io/reference/mpdf-functions/construct.html
+ * @link https://mpdf.github.io/reference/mpdf-variables/useadobecjk.html
+ * @todo it might be more sensible to pass a language string instead
+ * @param string $lang
+ * @return string
+ */
+ protected function lang2mode(string $lang): string
+ {
+ switch ($lang) {
+ case 'zh':
+ case 'zh-tw':
+ case 'ja':
+ case 'ko':
+ return '+aCJK';
+ default:
+ return 'UTF-8-s';
+ }
+ }
+
+ /**
+ * Return the writing direction based on the set language
+ *
+ * @param string $lang
+ * @return string
+ */
+ protected function lang2direction(string $lang): string
+ {
+ switch ($lang) {
+ case 'ar':
+ case 'he':
+ return 'rtl';
+ default:
+ return 'ltr';
+ }
+ }
+}
diff --git a/src/HttpClient.php b/src/HttpClient.php
new file mode 100644
index 00000000..94d34131
--- /dev/null
+++ b/src/HttpClient.php
@@ -0,0 +1,89 @@
+getUri();
+
+ $url = (string)$uri;
+
+ // short-circuit to cached/local copies whenever possible
+ $resolved = (new MediaLinkResolver())->resolve($url);
+ if ($resolved && is_readable($resolved['path'])) {
+ return (new Response())
+ ->withStatus(200)
+ ->withHeader('Content-Type', $resolved['mime'])
+ ->withBody(Stream::create(file_get_contents($resolved['path'])));
+ }
+
+ // fall back to the standard Dokuwiki HTTP client for any remote content
+ $client = new DokuHTTPClient();
+ $client->headers = $this->buildHeaders($request);
+ $client->referer = $request->getHeaderLine('Referer');
+ if ($agent = $request->getHeaderLine('User-Agent')) {
+ $client->agent = $agent;
+ }
+
+ $body = (string)$request->getBody();
+ if ($request->getBody()->isSeekable()) {
+ $request->getBody()->rewind();
+ }
+
+ $method = strtoupper($request->getMethod());
+ $client->sendRequest($url, $body, $method);
+
+ $response = (new Response())->withStatus($client->status ?: 500);
+
+ foreach ((array)$client->resp_headers as $name => $value) {
+ if (is_array($value)) {
+ foreach ($value as $single) {
+ $response = $response->withHeader($name, $single);
+ }
+ } else {
+ $response = $response->withHeader($name, $value);
+ }
+ }
+
+ return $response->withBody(Stream::create($client->resp_body));
+ }
+
+ /**
+ * Convert PSR-7 headers to the associative format expected by DokuHTTPClient.
+ *
+ * @param RequestInterface $request Original request from mPDF.
+ * @return array
+ */
+ private function buildHeaders(RequestInterface $request): array
+ {
+ $headers = [];
+ foreach ($request->getHeaders() as $name => $values) {
+ $headers[$name] = implode(', ', $values);
+ }
+
+ return $headers;
+ }
+}
diff --git a/src/LocalContentLoader.php b/src/LocalContentLoader.php
new file mode 100644
index 00000000..86f4e439
--- /dev/null
+++ b/src/LocalContentLoader.php
@@ -0,0 +1,33 @@
+resolve($path);
+ ;
+ if ($resolved) {
+ $path = $resolved['path'];
+ }
+
+ if (!is_readable($path)) {
+ return null;
+ }
+
+ return file_get_contents($path);
+ }
+}
diff --git a/src/MediaLinkResolver.php b/src/MediaLinkResolver.php
new file mode 100644
index 00000000..a51ee085
--- /dev/null
+++ b/src/MediaLinkResolver.php
@@ -0,0 +1,186 @@
+ string, 'mime' => string] when resolution succeeds, null otherwise.
+ */
+ public function resolve(string $file): ?array
+ {
+ $mediaID = $this->extractMediaID($file);
+ if ($mediaID !== null) {
+ [$w, $h, $rev] = $this->extractMediaParams($file);
+ [$ext, $mime] = mimetype($mediaID);
+ if (!$ext) return null;
+ $localFile = $this->localMediaFile($mediaID, $ext, $rev);
+ if (!$localFile) return null;
+ if (str_starts_with($mime, 'image/')) {
+ $localFile = $this->resizedMedia($localFile, $ext, $w, $h);
+ }
+ } else {
+ [, $mime] = mimetype($file);
+ if (!str_starts_with($mime, 'image/')) return null;
+ $localFile = $this->extractLocalImage($file);
+ }
+
+ if (!$localFile) return null;
+ return ['path' => $localFile, 'mime' => $mime];
+ }
+
+ /**
+ * Check if the given file URL corresponds to a Dokuwiki media ID and extract it.
+ *
+ * Handles rewritten media URLs (/media/*) and fetch.php calls by building a regex
+ * from the result of calling ml() for a fake media ID.
+ *
+ * Note that the returned media ID could still be an external URL!
+ *
+ * @param string $file
+ * @return string|null The extracted media ID, or null if not found.
+ */
+ protected function extractMediaID(string $file): ?string
+ {
+ // build regex to parse URL back to media info (matches fetch.php calls)
+ $fetchRegex = preg_quote(ml('xxx123yyy', '', true, '&', true), '/');
+ $fetchRegex = str_replace('xxx123yyy', '([^&\?]*)', $fetchRegex);
+
+ // extract the real media from a fetch.php URI and determine mime
+ if (
+ preg_match("/^$fetchRegex/", $file, $matches) ||
+ preg_match('/[&?]media=([^&?]*)/', $file, $matches)
+ ) {
+ return rawurldecode($matches[1]);
+ }
+
+ return null;
+ }
+
+ /**
+ * Extract media parameters (width, height, revision) from the given file URL.
+ *
+ * When a parameter is not found, its value will be 0.
+ *
+ * @param string $file Source string (fetch call)
+ * @return array{int,int,int} Array containing width, height, and revision.
+ */
+ protected function extractMediaParams(string $file): array
+ {
+ $width = $this->extractInt($file, 'w');
+ $height = $this->extractInt($file, 'h');
+ $rev = $this->extractInt($file, 'rev');
+ return [$width, $height, $rev];
+ }
+
+ /**
+ * Returns a local, absolute path for the given media ID and revision
+ *
+ * This method will download external media files to the local cache if needed. ACLs are
+ * checked here as well.
+ *
+ * Returns null when the media file is not accessible.
+ *
+ * @param string $mediaID A media ID or external URL.
+ * @param string $ext File extension (used for external media caching).
+ * @param int $rev Revision number (0 for latest).
+ * @return string|null Absolute path to the local media file, or null when not accessible.
+ */
+ protected function localMediaFile(string $mediaID, string $ext, int $rev): ?string
+ {
+ global $conf;
+
+ if (media_isexternal($mediaID)) {
+ $local = media_get_from_URL($mediaID, $ext, $conf['cachetime']);
+ if (!$local) return null;
+ } else {
+ $mediaID = cleanID($mediaID);
+ // check permissions (namespace only)
+ if (auth_quickaclcheck(getNS($mediaID) . ':X') < AUTH_READ) {
+ return null;
+ }
+ $local = mediaFN($mediaID, $rev ?: '');
+ }
+ if (!file_exists($local)) return null;
+
+ return $local;
+ }
+
+ /**
+ * Resize or crop the given media file as needed.
+ *
+ * @param string $mediaFile Absolute path to the local media file.
+ * @param string $ext File extension.
+ * @param int $width Desired width
+ * @param int $height Desired height
+ * @return string|null Absolute path to the resized/cropped media file, or null on failure.
+ */
+ protected function resizedMedia($mediaFile, $ext, $width, $height)
+ {
+ if ($width && $height) {
+ $mediaFile = media_crop_image($mediaFile, $ext, $width, $height);
+ } elseif ($width || $height) {
+ $mediaFile = media_resize_image($mediaFile, $ext, $width, $height);
+ }
+ if (!file_exists($mediaFile)) return null;
+ return $mediaFile;
+ }
+
+ /**
+ * Extract an integer parameter from the given subject URL.
+ *
+ * @param string $subject Source string, usually the media URL.
+ * @param string $param Name of the parameter to extract.
+ * @return int
+ */
+ protected function extractInt(string $subject, string $param): int
+ {
+ $pattern = '/[?&]' . $param . '=(\d+)/';
+ if (preg_match($pattern, $subject, $match)) {
+ return (int)$match[1];
+ }
+
+ return 0;
+ }
+
+ /**
+ * Attempt to extract a local file path from the given URL.
+ *
+ * This only works for static files that are directly accessible on disk. Or our
+ * custom dw2pdf:// scheme for local files passed from plugins.
+ *
+ * @param string $file Source URL.
+ * @return string|null Absolute path to the local file, or null when not accessible.
+ */
+ protected function extractLocalImage($file)
+ {
+ $local = null;
+ if (str_starts_with($file, 'dw2pdf://')) {
+ // support local files passed from plugins
+ $local = substr($file, 9);
+ } elseif (!preg_match('/(\.php|\?)/', $file)) {
+ // directly access local files instead of using HTTP (skipping dynamic content)
+ $base = preg_quote(DOKU_URL, '/');
+ $local = preg_replace("/^$base/i", DOKU_INC, $file, 1);
+ }
+
+ if (!file_exists($local)) return null;
+ if (!is_readable($local)) return null;
+
+ return $local;
+ }
+}
diff --git a/src/NamespaceCollector.php b/src/NamespaceCollector.php
new file mode 100644
index 00000000..026e5d62
--- /dev/null
+++ b/src/NamespaceCollector.php
@@ -0,0 +1,198 @@
+getConfig();
+
+ $this->namespace = $config->getBookNamespace();
+ $this->sortorder = $config->getBookSortOrder();
+ $this->depth = $config->getBookNamespaceDepth();
+ if ($this->depth < 0) $this->depth = 0;
+ $this->excludePages = $config->getBookExcludedPages();
+ $this->excludeNamespaces = $config->getBookExcludedNamespaces();
+
+ // check namespace exists
+ $nsdir = dirname(wikiFN($this->namespace . ':dummy'));
+ if (!@is_dir($nsdir)) throw new \Exception('needns');
+ }
+
+ /**
+ * @inheritdoc
+ * @triggers DW2PDF_NAMESPACEEXPORT_SORT
+ * @todo currently we do not support the 'at' parameter. We would need to search pages in the attic for this.
+ */
+ protected function collect(): array
+ {
+ global $conf;
+
+ try {
+ $this->initVars();
+ } catch (\Exception $e) {
+ return [];
+ }
+
+ //page search
+ $result = [];
+ $opts = ['depth' => $this->depth]; //recursive all levels
+ $dir = utf8_encodeFN(str_replace(':', '/', $this->namespace));
+ search($result, $conf['datadir'], 'search_allpages', $opts, $dir);
+
+ // remove excluded pages and namespaces
+ $result = $this->excludePages($result);
+
+
+
+ // Sort pages, let plugins modify sorting
+ $eventData = ['pages' => &$result, 'sort' => $this->sortorder];
+ $event = new Event('DW2PDF_NAMESPACEEXPORT_SORT', $eventData);
+ if ($event->advise_before()) {
+ $result = $this->sortPages($result);
+ }
+ $event->advise_after();
+
+ // extract page ids
+ $pages = array_column($result, 'id');
+
+ // if a there is a namespace start page outside the namespace, add it at the beginning
+ if ($this->namespace !== '') {
+ if (!in_array($this->namespace . ':' . $conf['start'], $pages, true)) {
+ if (file_exists(wikiFN(rtrim($this->namespace, ':')))) {
+ array_unshift($pages, rtrim($this->namespace, ':'));
+ }
+ }
+ }
+
+ return $pages;
+ }
+
+ /**
+ * Remove excluded pages and namespaces from the given list of pages
+ *
+ * @param array $pages The list of pages as returned by search()
+ * @return array The filtered list of pages
+ */
+ protected function excludePages(array $pages)
+ {
+ $pages = array_filter($pages, fn($page) => !in_array($page['id'], $this->excludePages));
+ $pages = array_filter($pages, function ($page) {
+ foreach ($this->excludeNamespaces as $ns) {
+ if (str_starts_with($page['id'], $ns . ':')) {
+ return false;
+ }
+ }
+ return true;
+ });
+ return $pages;
+ }
+
+ /**
+ * Sort the given list of pages according to the selected sort order
+ *
+ * @param array $pages The list of pages as returned by search()
+ * @return array The sorted list of pages
+ */
+ protected function sortPages(array $pages): array
+ {
+ $sortoptions = ['pagename', 'date', 'natural'];
+ if (!in_array($this->sortorder, $sortoptions)) {
+ $this->sortorder = 'natural';
+ }
+
+ if ($this->sortorder == 'date') {
+ usort($pages, [$this, 'cbDateSort']);
+ } else {
+ usort($pages, [$this, 'cbPagenameSort']);
+ }
+
+ return $pages;
+ }
+
+ /**
+ * usort callback to sort by file lastmodified time
+ *
+ * @param array $a
+ * @param array $b
+ * @return int
+ */
+ public function cbDateSort($a, $b)
+ {
+ if ($b['rev'] < $a['rev']) {
+ return -1;
+ }
+ if ($b['rev'] > $a['rev']) {
+ return 1;
+ }
+ return strcmp($b['id'], $a['id']);
+ }
+
+ /**
+ * usort callback to sort by page id
+ * @param array $a
+ * @param array $b
+ * @return int
+ */
+ public function cbPagenameSort($a, $b)
+ {
+ global $conf;
+
+ $partsA = explode(':', $a['id']);
+ $countA = count($partsA);
+ $partsB = explode(':', $b['id']);
+ $countB = count($partsB);
+ $max = max($countA, $countB);
+
+
+ // compare namepsace by namespace
+ for ($i = 0; $i < $max; $i++) {
+ $partA = $partsA[$i] ?: null;
+ $partB = $partsB[$i] ?: null;
+
+ // have we reached the page level?
+ if ($i === ($countA - 1) || $i === ($countB - 1)) {
+ // start page first
+ if ($partA == $conf['start']) {
+ return -1;
+ }
+ if ($partB == $conf['start']) {
+ return 1;
+ }
+ }
+
+ // prefer page over namespace
+ if ($partA === $partB) {
+ if (!isset($partsA[$i + 1])) {
+ return -1;
+ }
+ if (!isset($partsB[$i + 1])) {
+ return 1;
+ }
+ continue;
+ }
+
+
+ // simply compare
+ return strnatcmp($partA, $partB);
+ }
+
+ return strnatcmp($a['id'], $b['id']);
+ }
+}
diff --git a/src/PageCollector.php b/src/PageCollector.php
new file mode 100644
index 00000000..9e02858a
--- /dev/null
+++ b/src/PageCollector.php
@@ -0,0 +1,22 @@
+getConfig()->getExportId();
+ if ($exportID === '') {
+ return [];
+ }
+
+ // no export for non existing page
+ if (!page_exists($exportID, $this->rev)) {
+ return [];
+ }
+
+ return [$exportID];
+ }
+}
diff --git a/src/PdfExportService.php b/src/PdfExportService.php
new file mode 100644
index 00000000..ac71daca
--- /dev/null
+++ b/src/PdfExportService.php
@@ -0,0 +1,167 @@
+config = $config;
+ $this->collector = $collector;
+ $this->cache = $cache;
+ $this->tocHeader = $tocHeader;
+ $this->remoteUser = $remoteUser;
+ }
+
+ /**
+ * Build the PDF (or return cached version) and provide its filesystem path.
+ *
+ * @return string
+ * @throws MpdfException
+ */
+ public function getPdf(): string
+ {
+ if (!$this->cache->useCache() || $this->config->isDebugEnabled()) {
+ set_time_limit(0);
+ $this->buildDocument($this->cache->cache);
+ }
+
+ return $this->cache->cache;
+ }
+
+ /**
+ * Send the PDF to the browser. When $cacheFile is omitted, the PDF will be built (or loaded) first.
+ *
+ * @param string|null $cacheFile Absolute path to an already generated PDF file, if available
+ * @return void
+ * @throws MpdfException
+ */
+ public function sendPdf(?string $cacheFile = null): void
+ {
+ $cacheFile ??= $this->getPdf();
+ $title = $this->collector->getTitle();
+
+ header('Content-Type: application/pdf');
+ header('Cache-Control: must-revalidate, no-transform, post-check=0, pre-check=0');
+ header('Pragma: public');
+ http_conditionalRequest(filemtime($cacheFile));
+
+ $outputTarget = $this->config->getOutputTarget();
+ $filename = rawurlencode(cleanID(strtr($title, ':/;"', ' ')));
+ if ($outputTarget === 'file') {
+ header('Content-Disposition: attachment; filename="' . $filename . '.pdf";');
+ } else {
+ header('Content-Disposition: inline; filename="' . $filename . '.pdf";');
+ }
+
+ header('Set-Cookie: fileDownload=true; path=/');
+
+ http_sendfile($cacheFile);
+
+ $fp = @fopen($cacheFile, 'rb');
+ if ($fp) {
+ http_rangeRequest($fp, filesize($cacheFile), 'application/pdf');
+ } else {
+ header('HTTP/1.0 500 Internal Server Error');
+ echo 'Could not read file - bad permissions?';
+ }
+ exit();
+ }
+
+ /**
+ * Build the PDF document and write it to the cache file.
+ *
+ * @param string $cacheFile Destination path for the generated PDF file
+ * @return void
+ * @throws MpdfException
+ */
+ protected function buildDocument(string $cacheFile): void
+ {
+ $writer = $this->renderDocument();
+
+ if ($this->config->isDebugEnabled()) {
+ header('Content-Type: text/html; charset=utf-8');
+ echo $writer->getDebugHTML();
+ exit();
+ }
+
+ $writer->outputToFile($cacheFile);
+ }
+
+ /**
+ * Build the PDF document without writing it to disk and expose the debug HTML.
+ *
+ * @return string Debug HTML collected while rendering the document
+ * @throws MpdfException
+ */
+ public function getDebugHtml(): string
+ {
+ if (!$this->config->isDebugEnabled()) {
+ throw new \RuntimeException('Debug HTML is only available when debug mode is enabled');
+ }
+
+ return $this->renderDocument()->getDebugHTML();
+ }
+
+ /**
+ * Compose the document using the collector and return the writer.
+ *
+ * @return Writer Writer instance containing the rendered document
+ * @throws MpdfException
+ */
+ protected function renderDocument(): Writer
+ {
+ $mpdf = new DokuPdf($this->config, $this->collector->getLanguage());
+ $styles = new Styles($this->config);
+ $template = new Template($this->config);
+ $writer = new Writer($mpdf, $this->config, $template, $styles);
+
+ $writer->startDocument($this->collector->getTitle());
+ $writer->cover();
+
+ if ($this->config->hasToC()) {
+ $writer->toc($this->tocHeader);
+ }
+
+ foreach ($this->collector->getPages() as $page) {
+ $template->setContext($this->collector, $page, $this->remoteUser);
+ $writer->renderWikiPage($this->collector, $page);
+ }
+
+ $writer->back();
+ $writer->endDocument();
+ return $writer;
+ }
+}
diff --git a/src/Styles.php b/src/Styles.php
new file mode 100644
index 00000000..d8d40c00
--- /dev/null
+++ b/src/Styles.php
@@ -0,0 +1,130 @@
+config = $config;
+ }
+
+ /**
+ * Get the full CSS to include in the PDF
+ *
+ * Gathers all relevant CSS files, applies style replacements and parses LESS.
+ *
+ * @return string
+ */
+ public function getCSS(): string
+ {
+ //reuse the CSS dispatcher functions without triggering the main function
+ if (!defined('SIMPLE_TEST')) {
+ define('SIMPLE_TEST', 1);
+ }
+ require_once(DOKU_INC . 'lib/exe/css.php');
+
+ // prepare CSS files
+ $files = $this->getStyleFiles();
+ $css = '';
+ foreach ($files as $file => $location) {
+ $display = str_replace(fullpath(DOKU_INC), '', fullpath($file));
+ $css .= "\n/* XXXXXXXXX $display XXXXXXXXX */\n";
+ $css .= css_loadfile($file, $location);
+ }
+
+ // apply style replacements
+ $styleUtils = new StyleUtils();
+ $styleini = $styleUtils->cssStyleini();
+ $css = css_applystyle($css, $styleini['replacements']);
+
+ // parse less
+ return css_parseless($css);
+ }
+
+
+ /**
+ * Returns the list of style files to include in the PDF
+ *
+ * The array keys are the file paths on disk, the values are the
+ * paths as used inside the Styles (for resolving relative links).
+ *
+ * @return array
+ */
+ protected function getStyleFiles(): array
+ {
+ $tpl = $this->config->getTemplateName();
+
+ return array_merge(
+ [
+ DOKU_INC . 'lib/styles/screen.css' => DOKU_BASE . 'lib/styles/',
+ DOKU_INC . 'lib/styles/print.css' => DOKU_BASE . 'lib/styles/',
+ ],
+ $this->getExtensionStyles(),
+ [
+ DOKU_PLUGIN . 'dw2pdf/conf/style.css' => DOKU_BASE . 'lib/plugins/dw2pdf/conf/',
+ DOKU_PLUGIN . 'dw2pdf/tpl/' . $tpl . '/style.css' => DOKU_BASE . 'lib/plugins/dw2pdf/tpl/' . $tpl . '/',
+ DOKU_PLUGIN . 'dw2pdf/conf/style.local.css' => DOKU_BASE . 'lib/plugins/dw2pdf/conf/',
+ ]
+ );
+ }
+
+ /**
+ * Returns a list of possible Plugin and Template PDF Styles
+ *
+ * Checks for a pdf.css, falls back to print.css. For configured usestyles plugins
+ * the screen.css and style.css are also included.
+ */
+ protected function getExtensionStyles()
+ {
+ $list = [];
+ $plugins = plugin_list();
+
+ $usestyle = $this->config->getStyledExtensions();
+ foreach ($plugins as $p) {
+ if (in_array($p, $usestyle)) {
+ $list[DOKU_PLUGIN . "$p/screen.css"] = DOKU_BASE . "lib/plugins/$p/";
+ $list[DOKU_PLUGIN . "$p/screen.less"] = DOKU_BASE . "lib/plugins/$p/";
+
+ $list[DOKU_PLUGIN . "$p/style.css"] = DOKU_BASE . "lib/plugins/$p/";
+ $list[DOKU_PLUGIN . "$p/style.less"] = DOKU_BASE . "lib/plugins/$p/";
+ }
+
+ $list[DOKU_PLUGIN . "$p/all.css"] = DOKU_BASE . "lib/plugins/$p/";
+ $list[DOKU_PLUGIN . "$p/all.less"] = DOKU_BASE . "lib/plugins/$p/";
+
+ if (file_exists(DOKU_PLUGIN . "$p/pdf.css") || file_exists(DOKU_PLUGIN . "$p/pdf.less")) {
+ $list[DOKU_PLUGIN . "$p/pdf.css"] = DOKU_BASE . "lib/plugins/$p/";
+ $list[DOKU_PLUGIN . "$p/pdf.less"] = DOKU_BASE . "lib/plugins/$p/";
+ } else {
+ $list[DOKU_PLUGIN . "$p/print.css"] = DOKU_BASE . "lib/plugins/$p/";
+ $list[DOKU_PLUGIN . "$p/print.less"] = DOKU_BASE . "lib/plugins/$p/";
+ }
+ }
+
+ // template support
+ foreach (
+ [
+ 'pdf.css',
+ 'pdf.less',
+ 'css/pdf.css',
+ 'css/pdf.less',
+ 'styles/pdf.css',
+ 'styles/pdf.less'
+ ] as $file
+ ) {
+ if (file_exists(tpl_incdir() . $file)) {
+ $list[tpl_incdir() . $file] = tpl_basedir() . $file;
+ }
+ }
+
+ return $list;
+ }
+}
diff --git a/src/Template.php b/src/Template.php
new file mode 100644
index 00000000..510a0801
--- /dev/null
+++ b/src/Template.php
@@ -0,0 +1,185 @@
+ '',
+ 'rev' => '',
+ 'at' => '',
+ 'title' => '',
+ 'username' => '',
+ ];
+
+
+ /**
+ * Constructor
+ *
+ * @param Config $config The DW2PDF configuration
+ */
+ public function __construct(Config $config)
+ {
+ $this->name = $config->getTemplateName();
+ $this->qrScale = $config->getQRScale();
+ $this->dir = DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->name;
+ if (!is_dir($this->dir)) {
+ throw new \RuntimeException("Template directory $this->dir does not exist");
+ }
+ }
+
+ /**
+ * Set the context for the current page
+ *
+ * This will be used in placeholders in headers/footers/cover/back pages.
+ *
+ * @param string $id The page ID
+ * @param string $title The title of the page
+ * @param string|null $rev The revision of the page (if any)
+ * @param string|null $at The dateat mechanism in use (if any)
+ * @param string|null $username The username of the user generating the PDF (if any)
+ * @return void
+ */
+ public function setContext(AbstractCollector $collector, string $id, ?string $username): void
+ {
+ $this->context = [
+ 'title' => $collector->getTitle(),
+ 'id' => $id,
+ 'rev' => $collector->getRev() ?? '',
+ 'at' => $collector->getAt() ?? '',
+ 'username' => $username ?? '',
+ ];
+ }
+
+ /**
+ * Get the HTML content for the given type and order
+ *
+ * Will fall back to non-ordered version if ordered version is not found. Placeholders
+ * will be replaced.
+ *
+ * @param string $type header, footer, cover, back, citation
+ * @param string $order first, even, odd or empty string for default
+ * @return string
+ */
+ public function getHTML(string $type, string $order = ''): string
+ {
+ if ($order) $order = "_$order";
+
+ $file = $this->dir . '/' . $type . $order . '.html';
+ if (!is_file($file)) $file = $this->dir . '/' . $type . '.html';
+ if (!is_file($file)) return '';
+
+ $html = file_get_contents($file);
+ $html = $this->replacePlaceholders($html);
+ return $html;
+ }
+
+ /**
+ * Applies the placeholder replacements to the given HTML
+ *
+ * Called for headers, footers, cover and back pages on each call.
+ *
+ * Accesses global DokuWiki variables to fill in page specific data.
+ *
+ * @triggers PLUGIN_DW2PDF_REPLACE
+ * @param string $html The template's HTML content
+ * @return string The HTML with placeholders replaced
+ */
+ protected function replacePlaceholders(string $html): string
+ {
+ global $conf;
+
+ $params = [];
+ if (!empty($this->context['at'])) {
+ $params['at'] = $this->context['at'];
+ } elseif (!empty($this->context['rev'])) {
+ $params['rev'] = $this->context['rev'];
+ }
+ $url = wl($this->context['id'], $params, true, "&");
+
+ $replace = [
+ '@PAGE@' => '{PAGENO}',
+ '@PAGES@' => '{nbpg}', //see also $mpdf->pagenumSuffix = ' / '
+ '@TITLE@' => hsc($this->context['title'] ?? ''),
+ '@WIKI@' => $conf['title'],
+ '@WIKIURL@' => DOKU_URL,
+ '@DATE@' => dformat(time()),
+ '@USERNAME@' => hsc($this->context['username'] ?? ''),
+ '@BASE@' => DOKU_BASE,
+ '@INC@' => DOKU_INC,
+ '@TPLBASE@' => DOKU_BASE . 'lib/plugins/dw2pdf/tpl/' . $this->name . '/',
+ '@TPLINC@' => DOKU_INC . 'lib/plugins/dw2pdf/tpl/' . $this->name . '/',
+ // page dependent placeholders
+ '@ID' => $this->context['id'] ?? '',
+ '@UPDATE@' => dformat(filemtime(wikiFN($this->context['id'], $this->context['rev'] ?? ''))),
+ '@PAGEURL@' => $url,
+ '@QRCODE@' => $this->generateQRCode($url),
+ ];
+
+ // let other plugins define their own replacements
+ $evdata = [
+ 'id' => $this->context['id'],
+ 'replace' => &$replace,
+ 'content' => &$html,
+ 'context' => $this->context
+ ];
+ $event = new Event('PLUGIN_DW2PDF_REPLACE', $evdata);
+ if ($event->advise_before()) {
+ $html = str_replace(array_keys($replace), array_values($replace), $html);
+ }
+ // plugins may post-process HTML, e.g to clean up unused replacements
+ $event->advise_after();
+
+ // @DATE([, ])@
+ $html = preg_replace_callback(
+ '/@DATE\((.*?)(?:,\s*(.*?))?\)@/',
+ function ($match) {
+ global $conf;
+ //no 2nd argument for default date format
+ if ($match[2] == null) {
+ $match[2] = $conf['dformat'];
+ }
+ return strftime($match[2], strtotime($match[1]));
+ },
+ $html
+ );
+
+ return $html;
+ }
+
+ /**
+ * Generate QR code pseudo-HTML
+ *
+ * @param string $url The URL to encode
+ * @return string
+ */
+ protected function generateQRCode($url): string
+ {
+ if ($this->qrScale <= 0.0) return '';
+
+ $url = hsc($url);
+ return sprintf(
+ '',
+ $url,
+ $this->qrScale
+ );
+ }
+}
diff --git a/src/Writer.php b/src/Writer.php
new file mode 100644
index 00000000..645440c7
--- /dev/null
+++ b/src/Writer.php
@@ -0,0 +1,440 @@
+mpdf = $mpdf;
+ $this->config = $config;
+ $this->template = $template;
+ $this->styles = $styles;
+ $this->debug = $config->isDebugEnabled();
+
+ /**
+ * initialize a new renderer instance (singleton instance will be reused in later p_* calls)
+ * @var \renderer_plugin_dw2pdf $renderer
+ */
+ $renderer = plugin_load('renderer', 'dw2pdf', true);
+ $renderer->setConfig($config);
+ }
+
+ /**
+ * Initialize the document
+ *
+ * @param string $title
+ * @return void
+ * @throws MpdfException
+ */
+ public function startDocument(string $title): void
+ {
+ $this->mpdf->SetTitle($title);
+
+ // Set the styles
+ $styles = '@page landscape-page { size:landscape }';
+ $styles .= 'div.dw2pdf-landscape { page:landscape-page }';
+ $styles .= '@page portrait-page { size:portrait }';
+ $styles .= 'div.dw2pdf-portrait { page:portrait-page }';
+ $styles .= $this->styles->getCSS();
+ $this->write($styles, HTMLParserMode::HEADER_CSS);
+
+ //start body html
+ $this->write('', HTMLParserMode::HTML_BODY, true, false);
+ }
+
+ /**
+ * Insert a page break
+ *
+ * @return void
+ * @throws MpdfException
+ */
+ public function pageBreak(): void
+ {
+ $this->write('
', 2, false, false);
+ }
+
+ /**
+ * Write a wiki page into the PDF
+ *
+ * @param string $html The rendered HTML of the wiki page
+ * @return void
+ * @throws MpdfException
+ */
+ public function wikiPage(string $html): void
+ {
+ $this->conditionalPageBreak();
+ $this->applyHeaderFooters();
+ $this->write($html, HTMLParserMode::HTML_BODY, false, false);
+
+ // add citation box if any
+ $cite = $this->template->getHTML('citation');
+ if ($cite) {
+ $this->write($cite, HTMLParserMode::HTML_BODY, false, false);
+ }
+
+ $this->breakAfterMe();
+ }
+
+ /**
+ * Render and write a wiki page into the PDF
+ *
+ * This caches the rendered page individually (unless a specific revision is requested). So even
+ * when PDF needs to be regenerated, pages that have not changed will be loaded from cache.
+ *
+ * @param AbstractCollector $collector The collector providing the page context
+ * @param string $pageId The page ID to render
+ * @return void
+ * @throws MpdfException
+ */
+ public function renderWikiPage(AbstractCollector $collector, string $pageId): void
+ {
+ $rev = $collector->getRev();
+ $at = $collector->getAt();
+ $file = wikiFN($pageId,);
+
+ //ensure $id is in global $ID (needed for parsing)
+ global $ID;
+ $keep = $ID;
+ $ID = $pageId;
+
+ if ($collector->getRev()) {
+ //no caching on old revisions
+ $html = p_render('dw2pdf', p_get_instructions(io_readWikiPage($file, $pageId, $rev)), $info, $at);
+ } else {
+ $html = p_cached_output($file, 'dw2pdf', $pageId);
+ }
+
+ //restore ID (just in case)
+ $ID = $keep;
+
+ // Fix internal links then write the page
+ $html = $this->fixInternalLinks($collector, $html);
+ $this->wikiPage($html);
+ }
+
+ /**
+ * If the given HTML contains internal links to pages that are part of the exported PDF,
+ * fix the links to point to the correct section within the PDF.
+ *
+ * @param AbstractCollector $collector
+ * @param string $html The rendered HTML of the wiki page
+ * @return string
+ */
+ protected function fixInternalLinks(AbstractCollector $collector, string $html): string
+ {
+ if ($html === '') return $html;
+
+ // quick bail out if the page has no internal link markers
+ if (!str_contains($html, 'data-dw2pdf-target')) {
+ return $html;
+ }
+
+ $pages = $collector->getPages();
+ if ($pages === []) {
+ return $html;
+ }
+ $pages = array_fill_keys($pages, true);
+
+ $dom = new DOMDocument('1.0', 'UTF-8');
+ $previous = libxml_use_internal_errors(true);
+ $loaded = $dom->loadHTML(
+ '' . '
' . $html . '
',
+ LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
+ );
+ libxml_clear_errors();
+ libxml_use_internal_errors($previous);
+
+ if (!$loaded) {
+ return $html;
+ }
+
+ $anchors = $dom->getElementsByTagName('a');
+ if ($anchors->length === 0) {
+ return $html;
+ }
+
+ $pageAnchors = [];
+ foreach ($anchors as $anchor) {
+ /** @var \DOMElement $anchor */
+ if (!$anchor->hasAttribute('data-dw2pdf-target')) {
+ continue;
+ }
+
+ $target = $anchor->getAttribute('data-dw2pdf-target');
+ if ($target === '' || !isset($pages[$target])) {
+ $anchor->removeAttribute('data-dw2pdf-target');
+ $anchor->removeAttribute('data-dw2pdf-hash');
+ continue;
+ }
+
+ if (!isset($pageAnchors[$target])) {
+ $check = false;
+ $pageAnchors[$target] = sectionID($target, $check);
+ }
+
+ $hash = $anchor->getAttribute('data-dw2pdf-hash');
+ $anchor->setAttribute(
+ 'href',
+ '#' . $pageAnchors[$target] . '__' . $hash
+ );
+
+ $anchor->removeAttribute('data-dw2pdf-target');
+ $anchor->removeAttribute('data-dw2pdf-hash');
+ }
+
+ $wrapper = $dom->getElementsByTagName('div')->item(0);
+ if (!$wrapper) {
+ return $html;
+ }
+
+ $result = '';
+ foreach ($wrapper->childNodes as $node) {
+ $result .= $dom->saveHTML($node);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Write the Table of Contents
+ *
+ * For double-sided documents the ToC is always on an even number of pages, so that the
+ * following content is on the correct odd/even page.
+ * The first page of ToC starts always at an odd page, so an additional blank page might
+ * be included before.
+ * There is no page numbering at the pages of the ToC.
+ *
+ * @param string $header The header text for the ToC (localized))
+ * @return void
+ * @throws MpdfException
+ */
+ public function toc(string $header): void
+ {
+ $this->mpdf->TOCpagebreakByArray([
+ 'toc-preHTML' => '
' . $header . '
',
+ 'toc-bookmarkText' => $header,
+ 'links' => true,
+ 'outdent' => '1em',
+ 'pagenumstyle' => '1'
+ ]);
+
+ $this->write('
', HTMLParserMode::HTML_BODY, false, false);
+ }
+
+ /**
+ * Insert a cover page
+ *
+ * Should be called once at the beginning of the PDF generation. Will do nothing if
+ * no cover page is configured.
+ *
+ * @return void
+ * @throws MpdfException
+ */
+ public function cover(): void
+ {
+ $html = $this->template->getHTML('cover');
+ if (!$html) return;
+
+ $this->conditionalPageBreak();
+ $this->applyHeaderFooters();
+ $this->write($html, HTMLParserMode::HTML_BODY, false, false);
+
+ $this->breakAfterMe();
+ }
+
+ /**
+ * Insert a back page
+ *
+ * Should be called once at the end of the PDF generation. Will do nothing if
+ * no back page is configured.
+ *
+ * @return void
+ * @throws MpdfException
+ */
+ public function back(): void
+ {
+ $html = $this->template->getHTML('back');
+ if (!$html) return;
+
+ $this->conditionalPageBreak();
+ $this->write($html, HTMLParserMode::HTML_BODY, false, false);
+ }
+
+ /**
+ * Finalize the document
+ *
+ * @return void
+ * @throws MpdfException
+ */
+ public function endDocument(): void
+ {
+ // adds the closing div and finalizes the document
+ $this->write(' ', HTMLParserMode::HTML_BODY, false, true);
+ }
+
+ /**
+ * Set new headers and footers
+ *
+ * This will call the appropriate mpdf methods to set headers and footers. It should be called
+ * before each wiki page is added to the PDF.
+ *
+ * On first call on this instance it will set the headers/footers for the first page, afterwards
+ * it will use the standard headers/footers.
+ *
+ * We always set even and odd headers/footers, though they may be identical.
+ * @return void
+ */
+ protected function applyHeaderFooters(): void
+ {
+ if ($this->isFirstPage) {
+ $header = $this->template->getHTML('header', 'first');
+ $footer = $this->template->getHTML('footer', 'first');
+
+ if ($header) {
+ $this->mpdf->SetHTMLHeader($header, 'O');
+ $this->mpdf->SetHTMLHeader($header, 'E');
+ }
+ if ($footer) {
+ $this->mpdf->SetHTMLFooter($footer, 'O');
+ $this->mpdf->SetHTMLFooter($footer, 'E');
+ }
+ $this->isFirstPage = false;
+ } else {
+ $headerOdd = $this->template->getHTML('header', 'odd');
+ $headerEven = $this->template->getHTML('header', 'even');
+ $footerOdd = $this->template->getHTML('footer', 'odd');
+ $footerEven = $this->template->getHTML('footer', 'even');
+
+ if ($headerOdd) {
+ $this->mpdf->SetHTMLHeader($headerOdd, 'O');
+ }
+ if ($headerEven) {
+ $this->mpdf->SetHTMLHeader($headerEven, 'E');
+ }
+ if ($footerOdd) {
+ $this->mpdf->SetHTMLFooter($footerOdd, 'O');
+ }
+ if ($footerEven) {
+ $this->mpdf->SetHTMLFooter($footerEven, 'E');
+ }
+ }
+ }
+
+ /**
+ * Insert a page break if there was previous content
+ *
+ * @return void
+ * @throws MpdfException
+ */
+ protected function conditionalPageBreak(): void
+ {
+ if ($this->breakBeforeNext) {
+ $this->pageBreak();
+ $this->breakBeforeNext = false;
+ }
+ }
+
+ /**
+ * Signal that a page break should be inserted before the next content
+ *
+ * @return void
+ */
+ protected function breakAfterMe(): void
+ {
+ $this->breakBeforeNext = true;
+ }
+
+ /**
+ * Return the debug HTML collected so far
+ *
+ * Will return an empty string if debugging is not enabled.
+ *
+ * @return string The collected debug HTML
+ */
+ public function getDebugHTML(): string
+ {
+ return $this->debugHTML;
+ }
+
+ /**
+ * Persist the generated PDF to the provided destination file.
+ *
+ * @param string $cacheFile Absolute file path that should receive the PDF output
+ * @return void
+ * @throws MpdfException
+ */
+ public function outputToFile(string $cacheFile): void
+ {
+ $this->mpdf->Output($cacheFile, 'F');
+ }
+
+ /**
+ * A wrapper around MPDF::WriteHTML
+ *
+ * When debugging is enabled, the output is written to a debug buffer instead of the PDF.
+ *
+ * @param string $html The HTML code to write
+ * @param int $mode Use HTMLParserMode constants. Controls what parts of the $html code is parsed.
+ * @param bool $init Clears and sets buffers to Top level block etc.
+ * @param bool $close If false leaves buffers etc. in current state, so that it can continue a block etc.
+ * @throws MpdfException
+ */
+ protected function write(
+ string $html,
+ int $mode = HTMLParserMode::DEFAULT_MODE,
+ bool $init = true,
+ bool $close = true
+ ) {
+ if (!$this->debug) {
+ try {
+ $this->mpdf->WriteHTML($html, $mode, $init, $close);
+ } catch (MpdfException $e) {
+ ErrorHandler::logException($e); // ensure the issue is logged
+ throw $e;
+ }
+ return;
+ }
+
+ // when debugging, just store the HTML
+ if ($mode === HTMLParserMode::HEADER_CSS) {
+ $this->debugHTML .= "\n\n";
+ } else {
+ $this->debugHTML .= "\n" . $html . "\n";
+ }
+ }
+}
diff --git a/src/attributes/FromConfig.php b/src/attributes/FromConfig.php
new file mode 100644
index 00000000..8392f966
--- /dev/null
+++ b/src/attributes/FromConfig.php
@@ -0,0 +1,19 @@
+
+
+Page Number: @PAGE@
+Total Pages: @PAGES@
+Document Title: @TITLE@
+Wiki Title: @WIKI@
+Wiki URL: @WIKIURL@
+Date: @DATE@
+User: @USERNAME@
+Base Path: @BASE@
+Include Dir: @INC@
+Template Base Path: @TPLBASE@
+Template Include Dir: @TPLINC@
+Page ID: @ID@
+Revision: @UPDATE@
+Page URL: @PAGEURL@
+QR Code: @QRCODE@
diff --git a/vendor/autoload.php b/vendor/autoload.php
index 2b2076d9..14d9b31d 100644
--- a/vendor/autoload.php
+++ b/vendor/autoload.php
@@ -14,10 +14,7 @@
echo $err;
}
}
- trigger_error(
- $err,
- E_USER_ERROR
- );
+ throw new RuntimeException($err);
}
require_once __DIR__ . '/composer/autoload_real.php';
diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php
index 51e734a7..2052022f 100644
--- a/vendor/composer/InstalledVersions.php
+++ b/vendor/composer/InstalledVersions.php
@@ -26,12 +26,23 @@
*/
class InstalledVersions
{
+ /**
+ * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
+ * @internal
+ */
+ private static $selfDir = null;
+
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null
*/
private static $installed;
+ /**
+ * @var bool
+ */
+ private static $installedIsLocalDir;
+
/**
* @var bool|null
*/
@@ -309,6 +320,24 @@ public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
+
+ // when using reload, we disable the duplicate protection to ensure that self::$installed data is
+ // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
+ // so we have to assume it does not, and that may result in duplicate data being returned when listing
+ // all installed packages for example
+ self::$installedIsLocalDir = false;
+ }
+
+ /**
+ * @return string
+ */
+ private static function getSelfDir()
+ {
+ if (self::$selfDir === null) {
+ self::$selfDir = strtr(__DIR__, '\\', '/');
+ }
+
+ return self::$selfDir;
}
/**
@@ -322,19 +351,27 @@ private static function getInstalled()
}
$installed = array();
+ $copiedLocalDir = false;
if (self::$canGetVendors) {
+ $selfDir = self::getSelfDir();
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
+ $vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */
$required = require $vendorDir.'/composer/installed.php';
- $installed[] = self::$installedByVendor[$vendorDir] = $required;
- if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
- self::$installed = $installed[count($installed) - 1];
+ self::$installedByVendor[$vendorDir] = $required;
+ $installed[] = $required;
+ if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
+ self::$installed = $required;
+ self::$installedIsLocalDir = true;
}
}
+ if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
+ $copiedLocalDir = true;
+ }
}
}
@@ -350,7 +387,7 @@ private static function getInstalled()
}
}
- if (self::$installed !== array()) {
+ if (self::$installed !== array() && !$copiedLocalDir) {
$installed[] = self::$installed;
}
diff --git a/vendor/composer/autoload_files.php b/vendor/composer/autoload_files.php
index 40ed7678..35034b68 100644
--- a/vendor/composer/autoload_files.php
+++ b/vendor/composer/autoload_files.php
@@ -7,4 +7,5 @@
return array(
'6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
+ 'db356362850385d08a5381de2638b5fd' => $vendorDir . '/mpdf/mpdf/src/functions.php',
);
diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php
index 9b0e6acc..1baeb5f1 100644
--- a/vendor/composer/autoload_psr4.php
+++ b/vendor/composer/autoload_psr4.php
@@ -8,7 +8,10 @@
return array(
'setasign\\Fpdi\\' => array($vendorDir . '/setasign/fpdi/src'),
'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'),
+ 'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src'),
'Mpdf\\QrCode\\' => array($vendorDir . '/mpdf/qrcode/src'),
+ 'Mpdf\\PsrLogAwareTrait\\' => array($vendorDir . '/mpdf/psr-log-aware-trait/src'),
+ 'Mpdf\\PsrHttpMessageShim\\' => array($vendorDir . '/mpdf/psr-http-message-shim/src'),
'Mpdf\\' => array($vendorDir . '/mpdf/mpdf/src'),
'DeepCopy\\' => array($vendorDir . '/myclabs/deep-copy/src/DeepCopy'),
);
diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php
index dc54eac1..d22f2282 100644
--- a/vendor/composer/autoload_static.php
+++ b/vendor/composer/autoload_static.php
@@ -8,6 +8,7 @@ class ComposerStaticInitb71fb58cdf4c29fb0d05b258cce42b04
{
public static $files = array (
'6124b4c8570aa390c21fafd04a26c69f' => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
+ 'db356362850385d08a5381de2638b5fd' => __DIR__ . '/..' . '/mpdf/mpdf/src/functions.php',
);
public static $prefixLengthsPsr4 = array (
@@ -18,10 +19,13 @@ class ComposerStaticInitb71fb58cdf4c29fb0d05b258cce42b04
'P' =>
array (
'Psr\\Log\\' => 8,
+ 'Psr\\Http\\Message\\' => 17,
),
'M' =>
array (
'Mpdf\\QrCode\\' => 12,
+ 'Mpdf\\PsrLogAwareTrait\\' => 22,
+ 'Mpdf\\PsrHttpMessageShim\\' => 24,
'Mpdf\\' => 5,
),
'D' =>
@@ -39,10 +43,22 @@ class ComposerStaticInitb71fb58cdf4c29fb0d05b258cce42b04
array (
0 => __DIR__ . '/..' . '/psr/log/Psr/Log',
),
+ 'Psr\\Http\\Message\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/psr/http-message/src',
+ ),
'Mpdf\\QrCode\\' =>
array (
0 => __DIR__ . '/..' . '/mpdf/qrcode/src',
),
+ 'Mpdf\\PsrLogAwareTrait\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/mpdf/psr-log-aware-trait/src',
+ ),
+ 'Mpdf\\PsrHttpMessageShim\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/mpdf/psr-http-message-shim/src',
+ ),
'Mpdf\\' =>
array (
0 => __DIR__ . '/..' . '/mpdf/mpdf/src',
diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json
index 12b00b9b..eb5ca37b 100644
--- a/vendor/composer/installed.json
+++ b/vendor/composer/installed.json
@@ -2,33 +2,36 @@
"packages": [
{
"name": "mpdf/mpdf",
- "version": "v8.0.17",
- "version_normalized": "8.0.17.0",
+ "version": "v8.2.7",
+ "version_normalized": "8.2.7.0",
"source": {
"type": "git",
"url": "https://github.com/mpdf/mpdf.git",
- "reference": "5f64118317c8145c0abc606b310aa0a66808398a"
+ "reference": "b59670a09498689c33ce639bac8f5ba26721dab3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/mpdf/mpdf/zipball/5f64118317c8145c0abc606b310aa0a66808398a",
- "reference": "5f64118317c8145c0abc606b310aa0a66808398a",
+ "url": "https://api.github.com/repos/mpdf/mpdf/zipball/b59670a09498689c33ce639bac8f5ba26721dab3",
+ "reference": "b59670a09498689c33ce639bac8f5ba26721dab3",
"shasum": ""
},
"require": {
"ext-gd": "*",
"ext-mbstring": "*",
+ "mpdf/psr-http-message-shim": "^1.0 || ^2.0",
+ "mpdf/psr-log-aware-trait": "^2.0 || ^3.0",
"myclabs/deep-copy": "^1.7",
"paragonie/random_compat": "^1.4|^2.0|^9.99.99",
- "php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0",
- "psr/log": "^1.0 || ^2.0",
+ "php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
+ "psr/http-message": "^1.0 || ^2.0",
+ "psr/log": "^1.0 || ^2.0 || ^3.0",
"setasign/fpdi": "^2.1"
},
"require-dev": {
"mockery/mockery": "^1.3.0",
"mpdf/qrcode": "^1.1.0",
"squizlabs/php_codesniffer": "^3.5.0",
- "tracy/tracy": "^2.4",
+ "tracy/tracy": "~2.5",
"yoast/phpunit-polyfills": "^1.0"
},
"suggest": {
@@ -36,10 +39,13 @@
"ext-xml": "Needed mainly for SVG manipulation",
"ext-zlib": "Needed for compression of embedded resources, such as fonts"
},
- "time": "2022-01-20T10:51:40+00:00",
+ "time": "2025-12-01T10:18:02+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
+ "files": [
+ "src/functions.php"
+ ],
"psr-4": {
"Mpdf\\": "src/"
}
@@ -66,7 +72,7 @@
"utf-8"
],
"support": {
- "docs": "http://mpdf.github.io",
+ "docs": "https://mpdf.github.io",
"issues": "https://github.com/mpdf/mpdf/issues",
"source": "https://github.com/mpdf/mpdf"
},
@@ -78,19 +84,117 @@
],
"install-path": "../mpdf/mpdf"
},
+ {
+ "name": "mpdf/psr-http-message-shim",
+ "version": "v2.0.1",
+ "version_normalized": "2.0.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/mpdf/psr-http-message-shim.git",
+ "reference": "f25a0153d645e234f9db42e5433b16d9b113920f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/mpdf/psr-http-message-shim/zipball/f25a0153d645e234f9db42e5433b16d9b113920f",
+ "reference": "f25a0153d645e234f9db42e5433b16d9b113920f",
+ "shasum": ""
+ },
+ "require": {
+ "psr/http-message": "^2.0"
+ },
+ "time": "2023-10-02T14:34:03+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "Mpdf\\PsrHttpMessageShim\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mark Dorison",
+ "email": "mark@chromatichq.com"
+ },
+ {
+ "name": "Kristofer Widholm",
+ "email": "kristofer@chromatichq.com"
+ },
+ {
+ "name": "Nigel Cunningham",
+ "email": "nigel.cunningham@technocrat.com.au"
+ }
+ ],
+ "description": "Shim to allow support of different psr/message versions.",
+ "support": {
+ "issues": "https://github.com/mpdf/psr-http-message-shim/issues",
+ "source": "https://github.com/mpdf/psr-http-message-shim/tree/v2.0.1"
+ },
+ "install-path": "../mpdf/psr-http-message-shim"
+ },
+ {
+ "name": "mpdf/psr-log-aware-trait",
+ "version": "v2.0.0",
+ "version_normalized": "2.0.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/mpdf/psr-log-aware-trait.git",
+ "reference": "7a077416e8f39eb626dee4246e0af99dd9ace275"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/mpdf/psr-log-aware-trait/zipball/7a077416e8f39eb626dee4246e0af99dd9ace275",
+ "reference": "7a077416e8f39eb626dee4246e0af99dd9ace275",
+ "shasum": ""
+ },
+ "require": {
+ "psr/log": "^1.0 || ^2.0"
+ },
+ "time": "2023-05-03T06:18:28+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "Mpdf\\PsrLogAwareTrait\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mark Dorison",
+ "email": "mark@chromatichq.com"
+ },
+ {
+ "name": "Kristofer Widholm",
+ "email": "kristofer@chromatichq.com"
+ }
+ ],
+ "description": "Trait to allow support of different psr/log versions.",
+ "support": {
+ "issues": "https://github.com/mpdf/psr-log-aware-trait/issues",
+ "source": "https://github.com/mpdf/psr-log-aware-trait/tree/v2.0.0"
+ },
+ "install-path": "../mpdf/psr-log-aware-trait"
+ },
{
"name": "mpdf/qrcode",
- "version": "v1.2.0",
- "version_normalized": "1.2.0.0",
+ "version": "v1.2.1",
+ "version_normalized": "1.2.1.0",
"source": {
"type": "git",
"url": "https://github.com/mpdf/qrcode.git",
- "reference": "0c09fce8b28707611c3febdd1ca424d40f172184"
+ "reference": "5320c512776aa3c199bd8be8f707ec83d9779d85"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/mpdf/qrcode/zipball/0c09fce8b28707611c3febdd1ca424d40f172184",
- "reference": "0c09fce8b28707611c3febdd1ca424d40f172184",
+ "url": "https://api.github.com/repos/mpdf/qrcode/zipball/5320c512776aa3c199bd8be8f707ec83d9779d85",
+ "reference": "5320c512776aa3c199bd8be8f707ec83d9779d85",
"shasum": ""
},
"require": {
@@ -107,7 +211,7 @@
"ext-gd": "To output QR codes to PNG files",
"ext-simplexml": "To output QR codes to SVG files"
},
- "time": "2022-01-11T09:42:41+00:00",
+ "time": "2024-06-04T13:40:39+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -139,7 +243,7 @@
],
"support": {
"issues": "https://github.com/mpdf/qrcode/issues",
- "source": "https://github.com/mpdf/qrcode/tree/v1.2.0"
+ "source": "https://github.com/mpdf/qrcode/tree/v1.2.1"
},
"funding": [
{
@@ -151,40 +255,42 @@
},
{
"name": "myclabs/deep-copy",
- "version": "1.10.1",
- "version_normalized": "1.10.1.0",
+ "version": "1.13.4",
+ "version_normalized": "1.13.4.0",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5"
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5",
- "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
- "replace": {
- "myclabs/deep-copy": "self.version"
+ "conflict": {
+ "doctrine/collections": "<1.6.8",
+ "doctrine/common": "<2.13.3 || >=3 <3.2.2"
},
"require-dev": {
- "doctrine/collections": "^1.0",
- "doctrine/common": "^2.6",
- "phpunit/phpunit": "^7.1"
+ "doctrine/collections": "^1.6.8",
+ "doctrine/common": "^2.13.3 || ^3.2.2",
+ "phpspec/prophecy": "^1.10",
+ "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
},
- "time": "2020-06-29T13:22:24+00:00",
+ "time": "2025-08-01T08:46:24+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
- "psr-4": {
- "DeepCopy\\": "src/DeepCopy/"
- },
"files": [
"src/DeepCopy/deep_copy.php"
- ]
+ ],
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ }
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@@ -198,27 +304,93 @@
"object",
"object graph"
],
+ "support": {
+ "issues": "https://github.com/myclabs/DeepCopy/issues",
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+ "type": "tidelift"
+ }
+ ],
"install-path": "../myclabs/deep-copy"
},
+ {
+ "name": "psr/http-message",
+ "version": "2.0",
+ "version_normalized": "2.0.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "time": "2023-04-04T09:54:51+00:00",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/2.0"
+ },
+ "install-path": "../psr/http-message"
+ },
{
"name": "psr/log",
- "version": "1.1.3",
- "version_normalized": "1.1.3.0",
+ "version": "1.1.4",
+ "version_normalized": "1.1.4.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
- "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
+ "reference": "d49695b909c3b7628b6289db5479a1c204601f11"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
- "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
+ "reference": "d49695b909c3b7628b6289db5479a1c204601f11",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
- "time": "2020-03-23T09:12:05+00:00",
+ "time": "2021-05-03T11:20:27+00:00",
"type": "library",
"extra": {
"branch-alias": {
@@ -238,7 +410,7 @@
"authors": [
{
"name": "PHP-FIG",
- "homepage": "http://www.php-fig.org/"
+ "homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
@@ -248,40 +420,44 @@
"psr",
"psr-3"
],
+ "support": {
+ "source": "https://github.com/php-fig/log/tree/1.1.4"
+ },
"install-path": "../psr/log"
},
{
"name": "setasign/fpdi",
- "version": "v2.3.3",
- "version_normalized": "2.3.3.0",
+ "version": "v2.6.4",
+ "version_normalized": "2.6.4.0",
"source": {
"type": "git",
"url": "https://github.com/Setasign/FPDI.git",
- "reference": "50c388860a73191e010810ed57dbed795578e867"
+ "reference": "4b53852fde2734ec6a07e458a085db627c60eada"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Setasign/FPDI/zipball/50c388860a73191e010810ed57dbed795578e867",
- "reference": "50c388860a73191e010810ed57dbed795578e867",
+ "url": "https://api.github.com/repos/Setasign/FPDI/zipball/4b53852fde2734ec6a07e458a085db627c60eada",
+ "reference": "4b53852fde2734ec6a07e458a085db627c60eada",
"shasum": ""
},
"require": {
"ext-zlib": "*",
- "php": "^5.6 || ^7.0"
+ "php": "^7.1 || ^8.0"
},
"conflict": {
"setasign/tfpdf": "<1.31"
},
"require-dev": {
- "phpunit/phpunit": "~5.7",
- "setasign/fpdf": "~1.8",
- "setasign/tfpdf": "1.31",
- "tecnickcom/tcpdf": "~6.2"
+ "phpunit/phpunit": "^7",
+ "setasign/fpdf": "~1.8.6",
+ "setasign/tfpdf": "~1.33",
+ "squizlabs/php_codesniffer": "^3.5",
+ "tecnickcom/tcpdf": "^6.8"
},
"suggest": {
"setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured."
},
- "time": "2020-04-28T12:40:35+00:00",
+ "time": "2025-08-05T09:57:14+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -312,6 +488,16 @@
"fpdi",
"pdf"
],
+ "support": {
+ "issues": "https://github.com/Setasign/FPDI/issues",
+ "source": "https://github.com/Setasign/FPDI/tree/v2.6.4"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/setasign/fpdi",
+ "type": "tidelift"
+ }
+ ],
"install-path": "../setasign/fpdi"
}
],
diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php
index 41dd26d4..d3053b11 100644
--- a/vendor/composer/installed.php
+++ b/vendor/composer/installed.php
@@ -3,7 +3,7 @@
'name' => 'splitbrain/dw2pdf',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
- 'reference' => '7d636181fc5bb007c30530a67e7c156f19ded673',
+ 'reference' => '212a1ea54602e857db1c810a68e294f2d9486e20',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -11,34 +11,49 @@
),
'versions' => array(
'mpdf/mpdf' => array(
- 'pretty_version' => 'v8.0.17',
- 'version' => '8.0.17.0',
- 'reference' => '5f64118317c8145c0abc606b310aa0a66808398a',
+ 'pretty_version' => 'v8.2.7',
+ 'version' => '8.2.7.0',
+ 'reference' => 'b59670a09498689c33ce639bac8f5ba26721dab3',
'type' => 'library',
'install_path' => __DIR__ . '/../mpdf/mpdf',
'aliases' => array(),
'dev_requirement' => false,
),
+ 'mpdf/psr-http-message-shim' => array(
+ 'pretty_version' => 'v2.0.1',
+ 'version' => '2.0.1.0',
+ 'reference' => 'f25a0153d645e234f9db42e5433b16d9b113920f',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../mpdf/psr-http-message-shim',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ 'mpdf/psr-log-aware-trait' => array(
+ 'pretty_version' => 'v2.0.0',
+ 'version' => '2.0.0.0',
+ 'reference' => '7a077416e8f39eb626dee4246e0af99dd9ace275',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../mpdf/psr-log-aware-trait',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
'mpdf/qrcode' => array(
- 'pretty_version' => 'v1.2.0',
- 'version' => '1.2.0.0',
- 'reference' => '0c09fce8b28707611c3febdd1ca424d40f172184',
+ 'pretty_version' => 'v1.2.1',
+ 'version' => '1.2.1.0',
+ 'reference' => '5320c512776aa3c199bd8be8f707ec83d9779d85',
'type' => 'library',
'install_path' => __DIR__ . '/../mpdf/qrcode',
'aliases' => array(),
'dev_requirement' => false,
),
'myclabs/deep-copy' => array(
- 'pretty_version' => '1.10.1',
- 'version' => '1.10.1.0',
- 'reference' => '969b211f9a51aa1f6c01d1d2aef56d3bd91598e5',
+ 'pretty_version' => '1.13.4',
+ 'version' => '1.13.4.0',
+ 'reference' => '07d290f0c47959fd5eed98c95ee5602db07e0b6a',
'type' => 'library',
'install_path' => __DIR__ . '/../myclabs/deep-copy',
'aliases' => array(),
'dev_requirement' => false,
- 'replaced' => array(
- 0 => '1.10.1',
- ),
),
'paragonie/random_compat' => array(
'dev_requirement' => false,
@@ -46,19 +61,28 @@
0 => '*',
),
),
+ 'psr/http-message' => array(
+ 'pretty_version' => '2.0',
+ 'version' => '2.0.0.0',
+ 'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../psr/http-message',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
'psr/log' => array(
- 'pretty_version' => '1.1.3',
- 'version' => '1.1.3.0',
- 'reference' => '0f73288fd15629204f9d42b7055f72dacbe811fc',
+ 'pretty_version' => '1.1.4',
+ 'version' => '1.1.4.0',
+ 'reference' => 'd49695b909c3b7628b6289db5479a1c204601f11',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/log',
'aliases' => array(),
'dev_requirement' => false,
),
'setasign/fpdi' => array(
- 'pretty_version' => 'v2.3.3',
- 'version' => '2.3.3.0',
- 'reference' => '50c388860a73191e010810ed57dbed795578e867',
+ 'pretty_version' => 'v2.6.4',
+ 'version' => '2.6.4.0',
+ 'reference' => '4b53852fde2734ec6a07e458a085db627c60eada',
'type' => 'library',
'install_path' => __DIR__ . '/../setasign/fpdi',
'aliases' => array(),
@@ -67,7 +91,7 @@
'splitbrain/dw2pdf' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
- 'reference' => '7d636181fc5bb007c30530a67e7c156f19ded673',
+ 'reference' => '212a1ea54602e857db1c810a68e294f2d9486e20',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php
index 6d3407db..6cd6b536 100644
--- a/vendor/composer/platform_check.php
+++ b/vendor/composer/platform_check.php
@@ -4,8 +4,8 @@
$issues = array();
-if (!(PHP_VERSION_ID >= 70100)) {
- $issues[] = 'Your Composer dependencies require a PHP version ">= 7.1.0". You are running ' . PHP_VERSION . '.';
+if (!(PHP_VERSION_ID >= 70200)) {
+ $issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
@@ -19,8 +19,7 @@
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
- trigger_error(
- 'Composer detected issues in your platform: ' . implode(' ', $issues),
- E_USER_ERROR
+ throw new \RuntimeException(
+ 'Composer detected issues in your platform: ' . implode(' ', $issues)
);
}
diff --git a/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/01_bug_report.yml b/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/01_bug_report.yml
new file mode 100644
index 00000000..a88906a2
--- /dev/null
+++ b/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/01_bug_report.yml
@@ -0,0 +1,35 @@
+name: Bug report 🐛
+description: The library does not work as expected
+body:
+
+ - type: checkboxes
+ attributes:
+ label: Guidelines
+ description: Please confirm this is a bug report and not general troubleshooting.
+ options:
+ - label: I understand that [if I fail to adhere to contribution guidelines and/or fail to provide all required details, this issue may be closed without review](https://github.com/mpdf/mpdf/blob/development/.github/CONTRIBUTING.md).
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Description of the bug
+ validations:
+ required: true
+
+ - type: input
+ attributes:
+ label: mPDF version
+ validations:
+ required: true
+
+ - type: input
+ attributes:
+ label: PHP Version and environment (server type, cli provider etc., enclosing libraries and their respective versions)
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Reproducible PHP+CSS+HTML snippet suffering by the error
+ validations:
+ required: true
diff --git a/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/02_feature_request.yml b/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/02_feature_request.yml
new file mode 100644
index 00000000..88d9bdca
--- /dev/null
+++ b/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/02_feature_request.yml
@@ -0,0 +1,8 @@
+name: Feature request 🚀
+description: I would like to have a new functionality added
+body:
+ - type: textarea
+ attributes:
+ label: Please describe the new functionality as best as you can.
+ validations:
+ required: true
\ No newline at end of file
diff --git a/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/Bug_report.md b/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/Bug_report.md
deleted file mode 100644
index cbf80c85..00000000
--- a/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/Bug_report.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
-name: Bug report 🐛
-about: The library does not work as expected (please use not for troubleshooting)
----
-
-
-
-### I found this bug
-
-### This is mPDF and PHP version and environment (server/fpm/cli etc) I am using
-
-### This is the PHP code snippet I use
-
-```
-
diff --git a/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/config.yml b/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/config.yml
index 901d9c25..3c03ea0d 100644
--- a/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/config.yml
+++ b/vendor/mpdf/mpdf/.github/ISSUE_TEMPLATE/config.yml
@@ -1,3 +1,4 @@
+blank_issues_enabled: false
contact_links:
- name: General questions and troubleshooting ❓
url: https://github.com/mpdf/mpdf/discussions
diff --git a/vendor/mpdf/mpdf/.github/SECURITY.md b/vendor/mpdf/mpdf/.github/SECURITY.md
new file mode 100644
index 00000000..71878d08
--- /dev/null
+++ b/vendor/mpdf/mpdf/.github/SECURITY.md
@@ -0,0 +1,5 @@
+How to disclose potential security issues
+============
+
+As mPDF does not have a domain or a dedicated contact apart from its Github repository, to prevent
+disclosing maintainers' contacts publicly, please use [GitHub's Security Advisories system](https://github.com/mpdf/mpdf/security/advisories).
\ No newline at end of file
diff --git a/vendor/mpdf/mpdf/.github/workflows/coverage.yml b/vendor/mpdf/mpdf/.github/workflows/coverage.yml
index 13b1207b..9ae56589 100644
--- a/vendor/mpdf/mpdf/.github/workflows/coverage.yml
+++ b/vendor/mpdf/mpdf/.github/workflows/coverage.yml
@@ -25,7 +25,7 @@ jobs:
steps:
- name: "Checkout"
- uses: "actions/checkout@v2"
+ uses: "actions/checkout@v4"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
diff --git a/vendor/mpdf/mpdf/.github/workflows/cs.yml b/vendor/mpdf/mpdf/.github/workflows/cs.yml
index ff003e01..c2a46d08 100644
--- a/vendor/mpdf/mpdf/.github/workflows/cs.yml
+++ b/vendor/mpdf/mpdf/.github/workflows/cs.yml
@@ -26,7 +26,7 @@ jobs:
steps:
- name: "Checkout"
- uses: "actions/checkout@v2"
+ uses: "actions/checkout@v4"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
diff --git a/vendor/mpdf/mpdf/.github/workflows/static-analysis.yml b/vendor/mpdf/mpdf/.github/workflows/static-analysis.yml
new file mode 100644
index 00000000..3f97c7c9
--- /dev/null
+++ b/vendor/mpdf/mpdf/.github/workflows/static-analysis.yml
@@ -0,0 +1,44 @@
+# https://help.github.com/en/categories/automating-your-workflow-with-github-actions
+
+name: "Static Analysis check"
+
+on:
+ pull_request:
+ push:
+ branches:
+ - "master"
+ - "development"
+ - "test"
+
+jobs:
+
+ stan:
+
+ name: "Static Analysis check"
+
+ runs-on: ${{ matrix.operating-system }}
+
+ strategy:
+ matrix:
+ php-version:
+ - "8.2"
+
+ operating-system: [ubuntu-latest]
+
+ steps:
+ - name: "Checkout"
+ uses: "actions/checkout@v4"
+
+ - name: "Install PHP"
+ uses: "shivammathur/setup-php@v2"
+ with:
+ coverage: "none"
+ php-version: "${{ matrix.php-version }}"
+ extensions: "mbstring"
+ tools: composer:v2
+
+ - name: "Install dependencies"
+ run: "composer install --no-interaction --no-progress && composer require \"phpstan/phpstan:^2.0\""
+
+ - name: "Static Analysis check"
+ run: vendor/bin/phpstan --no-progress
diff --git a/vendor/mpdf/mpdf/.github/workflows/tests.yml b/vendor/mpdf/mpdf/.github/workflows/tests.yml
index ff64a816..a7dd7f45 100644
--- a/vendor/mpdf/mpdf/.github/workflows/tests.yml
+++ b/vendor/mpdf/mpdf/.github/workflows/tests.yml
@@ -1,6 +1,6 @@
# https://help.github.com/en/categories/automating-your-workflow-with-github-actions
-name: "CI"
+name: "Tests"
on:
pull_request:
@@ -30,11 +30,15 @@ jobs:
- "7.4"
- "8.0"
- "8.1"
+ - "8.2"
+ - "8.3"
+ - "8.4"
+ - "8.5"
operating-system: [ubuntu-latest, windows-latest]
steps:
- name: "Checkout"
- uses: "actions/checkout@v2"
+ uses: "actions/checkout@v4"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
diff --git a/vendor/mpdf/mpdf/.gitignore b/vendor/mpdf/mpdf/.gitignore
index 0bfe817f..60278b15 100644
--- a/vendor/mpdf/mpdf/.gitignore
+++ b/vendor/mpdf/mpdf/.gitignore
@@ -1,2 +1,3 @@
vendor/*
+tests/Mpdf/tmp/*
composer.lock
diff --git a/vendor/mpdf/mpdf/CHANGELOG.md b/vendor/mpdf/mpdf/CHANGELOG.md
index 9c184e29..4c7b13fd 100644
--- a/vendor/mpdf/mpdf/CHANGELOG.md
+++ b/vendor/mpdf/mpdf/CHANGELOG.md
@@ -1,3 +1,44 @@
+mPDF 8.2.x
+===========================
+
+New features
+------------
+* Watermark text can now be colored using \Mpdf\Watermark DTO. \Mpdf\WatermarkImage DTO for images. (#1876)
+* Added support for `psr/http-message` v2 without dropping v1. (@markdorison, @apotek, @greg-1-anderson, @NigelCunningham #1907)
+* PHP 8.3 support in mPDF 8.2.1
+* Add support for `page-break-before: avoid;` and `page-break-after: avoid;` for tr elements inside tables
+
+Bugfixes
+--------
+
+* Replace character entities with characters when processing the `code` attribute in the `` tag
+* Escape XML predefined entities in XMP metadata (Fix for #2090)
+* Enable Font Subsetting by Default (Fix for #1315)
+
+mPDF 8.1.x
+===========================
+
+New features
+------------
+
+* Service container for internal services
+* Set /Lang entry for better accessibility when document language is available (@cuongmits, #1418)
+* More verbose helper methods for `Output`: `OutputBinaryData`, `OutputHttpInline`, `OutputHttpDownload`, `OutputFile` (since v8.1.2)
+* Set font-size to `auto` in textarea and input in active forms to resize the font-size (@ChrisB9, #1721)
+* PHP 8.2 support in mPDF 8.1.3
+* Added support for `psr/log` v3 without dropping v2. (@markdorison, @apotek, @greg-1-anderson, #1857)
+
+Bugfixes
+--------
+
+* Better exception message about fonts with MarkGlyphSets (Fix for #1408)
+* Updated Garuda font with fixed "k" character (Fix for #1440)
+* Testing and suppressing PNG file conversion errors
+* Prevent hyphenation of urls starting with https and e-mail addresses (@HKandulla, #1634)
+* Colorspace restrictor reads mode from Mpdf and works again (Fix for #1094)
+* Prevent exception when multiple columns wrap to next page
+* Update default `curlUserAgent` configuration variable from Firefox 13 to 108
+
mPDF 8.0.x
===========================
@@ -37,6 +78,7 @@ mPDF 8.0.x
* Fix: Using mpdf in phar package leads to weird errors (#1504, @sandreas)
* WEBP images support (#1525)
+
mPDF 8.0.0
===========================
diff --git a/vendor/mpdf/mpdf/README.md b/vendor/mpdf/mpdf/README.md
index 366ddace..c58e6660 100644
--- a/vendor/mpdf/mpdf/README.md
+++ b/vendor/mpdf/mpdf/README.md
@@ -18,11 +18,15 @@ Requirements
PHP versions and extensions
---------------------------
-- `mPDF >=7.0` is supported on `PHP ^5.6 || ~7.0.0 || ~7.1.0 || ~7.2.0`
+- `PHP >=5.6 <7.3.0` is supported for `mPDF >= 7.0`
- `PHP 7.3` is supported since `mPDF v7.1.7`
- `PHP 7.4` is supported since `mPDF v8.0.4`
- `PHP 8.0` is supported since `mPDF v8.0.10`
- `PHP 8.1` is supported as of `mPDF v8.0.13`
+- `PHP 8.2` is supported as of `mPDF v8.1.3`
+- `PHP 8.3` is supported as of `mPDF v8.2.1`
+- `PHP 8.4` is supported as of `mPDF v8.2.5`
+- `PHP 8.5` is supported as of `mPDF v8.2.6`
PHP `mbstring` and `gd` extensions have to be loaded.
diff --git a/vendor/mpdf/mpdf/composer.json b/vendor/mpdf/mpdf/composer.json
index c93e87d2..a8481f86 100644
--- a/vendor/mpdf/mpdf/composer.json
+++ b/vendor/mpdf/mpdf/composer.json
@@ -18,22 +18,25 @@
"support": {
"issues": "https://github.com/mpdf/mpdf/issues",
"source": "https://github.com/mpdf/mpdf",
- "docs": "http://mpdf.github.io"
+ "docs": "https://mpdf.github.io"
},
"require": {
- "php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0",
+ "php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"ext-gd": "*",
"ext-mbstring": "*",
+ "mpdf/psr-http-message-shim": "^1.0 || ^2.0",
+ "mpdf/psr-log-aware-trait": "^2.0 || ^3.0",
"myclabs/deep-copy": "^1.7",
"paragonie/random_compat": "^1.4|^2.0|^9.99.99",
- "psr/log": "^1.0 || ^2.0",
+ "psr/http-message": "^1.0 || ^2.0",
+ "psr/log": "^1.0 || ^2.0 || ^3.0",
"setasign/fpdi": "^2.1"
},
"require-dev": {
"mockery/mockery": "^1.3.0",
"mpdf/qrcode": "^1.1.0",
"squizlabs/php_codesniffer": "^3.5.0",
- "tracy/tracy": "^2.4",
+ "tracy/tracy": "~2.5",
"yoast/phpunit-polyfills": "^1.0"
},
"suggest": {
@@ -44,7 +47,10 @@
"autoload": {
"psr-4": {
"Mpdf\\": "src/"
- }
+ },
+ "files": [
+ "src/functions.php"
+ ]
},
"autoload-dev": {
"psr-4": {
@@ -58,7 +64,7 @@
"post-install-cmd": [
"php -r \"chmod('./tmp', 0777);\""
],
- "cs": "@php vendor/bin/phpcs -v --report-width=160 --standard=ruleset.xml --severity=1 --warning-severity=0 --extensions=php src utils tests",
+ "cs": "@php vendor/bin/phpcs --report-width=160 --standard=ruleset.xml --severity=1 --warning-severity=0 --extensions=php src utils tests",
"test": "@php vendor/bin/phpunit",
"coverage": "@php vendor/bin/phpunit --coverage-text"
},
diff --git a/vendor/mpdf/mpdf/phpstan-baseline.neon b/vendor/mpdf/mpdf/phpstan-baseline.neon
new file mode 100644
index 00000000..eed44e96
--- /dev/null
+++ b/vendor/mpdf/mpdf/phpstan-baseline.neon
@@ -0,0 +1,1459 @@
+parameters:
+ ignoreErrors:
+ -
+ message: '#^Method Mpdf\\Barcode\\Code39\:\:init\(\) should return array\ but return statement is missing\.$#'
+ identifier: return.missing
+ count: 1
+ path: src/Barcode/Code39.php
+
+ -
+ message: '#^Binary operation "%%" between string and 4 results in an error\.$#'
+ identifier: binaryOp.invalid
+ count: 1
+ path: src/Barcode/EanExt.php
+
+ -
+ message: '#^Binary operation "\+" between non\-empty\-string and non\-empty\-string results in an error\.$#'
+ identifier: binaryOp.invalid
+ count: 2
+ path: src/Barcode/EanExt.php
+
+ -
+ message: '#^Variable \$upceCode in isset\(\) always exists and is not nullable\.$#'
+ identifier: isset.variable
+ count: 1
+ path: src/Barcode/EanUpc.php
+
+ -
+ message: '#^Variable \$ret might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Color/ColorConverter.php
+
+ -
+ message: '#^PHPDoc tag @param references unknown parameter\: \$mode$#'
+ identifier: parameter.notFound
+ count: 1
+ path: src/Color/ColorSpaceRestrictor.php
+
+ -
+ message: '#^Binary operation "\+" between non\-empty\-string and 0 results in an error\.$#'
+ identifier: binaryOp.invalid
+ count: 1
+ path: src/CssManager.php
+
+ -
+ message: '#^Variable \$tag might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/CssManager.php
+
+ -
+ message: '#^Method Mpdf\\Mpdf\:\:GetJspacing\(\) invoked with 4 parameters, 5 required\.$#'
+ identifier: arguments.count
+ count: 2
+ path: src/DirectWrite.php
+
+ -
+ message: '#^Undefined variable\: \$false$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/DirectWrite.php
+
+ -
+ message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#'
+ identifier: phpDoc.parseError
+ count: 2
+ path: src/Form.php
+
+ -
+ message: '#^Variable \$js in isset\(\) always exists and is not nullable\.$#'
+ identifier: isset.variable
+ count: 2
+ path: src/Form.php
+
+ -
+ message: '#^Variable \$radio_background_color might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Form.php
+
+ -
+ message: '#^Variable \$radio_color might not be defined\.$#'
+ identifier: variable.undefined
+ count: 10
+ path: src/Form.php
+
+ -
+ message: '#^Binary operation "\+" between non\-empty\-string and 0 results in an error\.$#'
+ identifier: binaryOp.invalid
+ count: 1
+ path: src/Gradient.php
+
+ -
+ message: '#^Variable \$angle in isset\(\) always exists and is not nullable\.$#'
+ identifier: isset.variable
+ count: 1
+ path: src/Gradient.php
+
+ -
+ message: '#^Variable \$repeat might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Gradient.php
+
+ -
+ message: '#^Variable \$startx in isset\(\) always exists and is not nullable\.$#'
+ identifier: isset.variable
+ count: 1
+ path: src/Gradient.php
+
+ -
+ message: '#^Variable \$starty in isset\(\) always exists and is not nullable\.$#'
+ identifier: isset.variable
+ count: 1
+ path: src/Gradient.php
+
+ -
+ message: '#^Call to method getHost\(\) on an unknown class Mpdf\\Http\\Uri\.$#'
+ identifier: class.notFound
+ count: 2
+ path: src/Http/SocketHttpClient.php
+
+ -
+ message: '#^Call to method getPath\(\) on an unknown class Mpdf\\Http\\Uri\.$#'
+ identifier: class.notFound
+ count: 1
+ path: src/Http/SocketHttpClient.php
+
+ -
+ message: '#^Call to method getPort\(\) on an unknown class Mpdf\\Http\\Uri\.$#'
+ identifier: class.notFound
+ count: 2
+ path: src/Http/SocketHttpClient.php
+
+ -
+ message: '#^Call to method getQuery\(\) on an unknown class Mpdf\\Http\\Uri\.$#'
+ identifier: class.notFound
+ count: 1
+ path: src/Http/SocketHttpClient.php
+
+ -
+ message: '#^Call to method getScheme\(\) on an unknown class Mpdf\\Http\\Uri\.$#'
+ identifier: class.notFound
+ count: 1
+ path: src/Http/SocketHttpClient.php
+
+ -
+ message: '#^Instantiated class Mpdf\\Http\\Uri not found\.$#'
+ identifier: class.notFound
+ count: 1
+ path: src/Http/SocketHttpClient.php
+
+ -
+ message: '#^Variable \$c might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Image/Bmp.php
+
+ -
+ message: '#^Variable \$str might not be defined\.$#'
+ identifier: variable.undefined
+ count: 12
+ path: src/Image/Bmp.php
+
+ -
+ message: '#^Variable \$bgColor in isset\(\) always exists and is not nullable\.$#'
+ identifier: isset.variable
+ count: 2
+ path: src/Image/ImageProcessor.php
+
+ -
+ message: '#^Variable \$info might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Image/ImageProcessor.php
+
+ -
+ message: '#^Variable \$ncols might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Image/ImageProcessor.php
+
+ -
+ message: '#^Binary operation "\*" between 0\.3333333333333333 and string results in an error\.$#'
+ identifier: binaryOp.invalid
+ count: 2
+ path: src/Image/Svg.php
+
+ -
+ message: '#^Binary operation "\*" between string and 2 results in an error\.$#'
+ identifier: binaryOp.invalid
+ count: 2
+ path: src/Image/Svg.php
+
+ -
+ message: '#^Binary operation "\+" between non\-empty\-string and \(float\|int\) results in an error\.$#'
+ identifier: binaryOp.invalid
+ count: 1
+ path: src/Image/Svg.php
+
+ -
+ message: '#^Binary operation "\+" between string and \(float\|int\) results in an error\.$#'
+ identifier: binaryOp.invalid
+ count: 3
+ path: src/Image/Svg.php
+
+ -
+ message: '#^Binary operation "\+\=" between 0\|string and non\-empty\-string results in an error\.$#'
+ identifier: assignOp.invalid
+ count: 4
+ path: src/Image/Svg.php
+
+ -
+ message: '#^Binary operation "\-" between string and string results in an error\.$#'
+ identifier: binaryOp.invalid
+ count: 2
+ path: src/Image/Svg.php
+
+ -
+ message: '#^PHPDoc tag @var has invalid value \(\$styleNode \\DOMNode\)\: Unexpected token "\$styleNode", expected type at offset 9 on line 1$#'
+ identifier: phpDoc.parseError
+ count: 1
+ path: src/Image/Svg.php
+
+ -
+ message: '#^Unary operation "\-" on non\-empty\-string results in an error\.$#'
+ identifier: unaryOp.invalid
+ count: 2
+ path: src/Image/Svg.php
+
+ -
+ message: '#^Unary operation "\-" on string results in an error\.$#'
+ identifier: unaryOp.invalid
+ count: 50
+ path: src/Image/Svg.php
+
+ -
+ message: '#^Variable \$c in isset\(\) always exists and is not nullable\.$#'
+ identifier: isset.variable
+ count: 1
+ path: src/Image/Svg.php
+
+ -
+ message: '#^Variable \$class in empty\(\) always exists and is not falsy\.$#'
+ identifier: empty.variable
+ count: 1
+ path: src/Image/Svg.php
+
+ -
+ message: '#^Variable \$color_final might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Image/Svg.php
+
+ -
+ message: '#^Variable \$inners might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Image/Svg.php
+
+ -
+ message: '#^Variable \$path_style might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Image/Svg.php
+
+ -
+ message: '#^Variable \$op might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Image/Wmf.php
+
+ -
+ message: '#^Variable \$parms might not be defined\.$#'
+ identifier: variable.undefined
+ count: 9
+ path: src/Image/Wmf.php
+
+ -
+ message: '#^Binary operation "\*" between string and 100 results in an error\.$#'
+ identifier: binaryOp.invalid
+ count: 4
+ path: src/Mpdf.php
+
+ -
+ message: '#^Binary operation "\*" between string and 2 results in an error\.$#'
+ identifier: binaryOp.invalid
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Binary operation "\+" between non\-empty\-string and 0 results in an error\.$#'
+ identifier: binaryOp.invalid
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Binary operation "\+" between string and \(float\|int\) results in an error\.$#'
+ identifier: binaryOp.invalid
+ count: 2
+ path: src/Mpdf.php
+
+ -
+ message: '#^Binary operation "\-" between int\<0, max\> and string results in an error\.$#'
+ identifier: binaryOp.invalid
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Binary operation "\-" between string and \(float\|int\) results in an error\.$#'
+ identifier: binaryOp.invalid
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Call to function unset\(\) contains undefined variable \$cell\.$#'
+ identifier: unset.variable
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Call to function unset\(\) contains undefined variable \$content\.$#'
+ identifier: unset.variable
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Call to sprintf contains 0 placeholders, 1 value given\.$#'
+ identifier: argument.sprintf
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#'
+ identifier: phpDoc.parseError
+ count: 2
+ path: src/Mpdf.php
+
+ -
+ message: '#^PHPDoc tag @throws with type Mpdf\\FilterException\|Mpdf\\PdfParserException\|Mpdf\\PdfReaderException\|setasign\\Fpdi\\PdfParser\\CrossReference\\CrossReferenceException\|setasign\\Fpdi\\PdfParser\\Type\\PdfTypeException is not subtype of Throwable$#'
+ identifier: throws.notThrowable
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^PHPDoc tag @var has invalid value \(\$value PdfIndirectObject\)\: Unexpected token "\$value", expected type at offset 15 on line 2$#'
+ identifier: phpDoc.parseError
+ count: 2
+ path: src/Mpdf.php
+
+ -
+ message: '#^PHPDoc tag @var has invalid value \(\$value PdfIndirectObject\)\: Unexpected token "\$value", expected type at offset 16 on line 2$#'
+ identifier: phpDoc.parseError
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Undefined variable\: \$bcor$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Mpdf.php
+
+ -
+ message: '#^Undefined variable\: \$t_tod$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Undefined variable\: \$xadj$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$aarr might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$adjx might not be defined\.$#'
+ identifier: variable.undefined
+ count: 4
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$adjy might not be defined\.$#'
+ identifier: variable.undefined
+ count: 4
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$adv might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$b2 might not be defined\.$#'
+ identifier: variable.undefined
+ count: 4
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$bb might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$bbox_bb might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$bbox_bl might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$bbox_br might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$bbox_bt might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$bbox_pb might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$bbox_pl might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$bbox_pr might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$bbox_pt might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$bbox_x might not be defined\.$#'
+ identifier: variable.undefined
+ count: 10
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$bbox_y might not be defined\.$#'
+ identifier: variable.undefined
+ count: 10
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$blm might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$blw might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$blx might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$bodystyle in isset\(\) always exists and is not nullable\.$#'
+ identifier: isset.variable
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$cbord might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$cell in isset\(\) always exists and is not nullable\.$#'
+ identifier: isset.variable
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$charLI might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$charLO might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$charRI might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$chars might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$charspacing might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$codestr_fontsize might not be defined\.$#'
+ identifier: variable.undefined
+ count: 5
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$cw might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$desc might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$ep_present might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$ep_reset might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$ep_suppress might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$ep_type might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$extraHeight might not be defined\.$#'
+ identifier: variable.undefined
+ count: 3
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$extraWidth might not be defined\.$#'
+ identifier: variable.undefined
+ count: 3
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$fx might not be defined\.$#'
+ identifier: variable.undefined
+ count: 7
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$glyphYorigin might not be defined\.$#'
+ identifier: variable.undefined
+ count: 5
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$h might not be defined\.$#'
+ identifier: variable.undefined
+ count: 12
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$info might not be defined\.$#'
+ identifier: variable.undefined
+ count: 4
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$inner_h might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$inner_w might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$innerp might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$leveladj might not be defined\.$#'
+ identifier: variable.undefined
+ count: 4
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$llm might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$lx1 might not be defined\.$#'
+ identifier: variable.undefined
+ count: 7
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$lx2 might not be defined\.$#'
+ identifier: variable.undefined
+ count: 7
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$ly1 might not be defined\.$#'
+ identifier: variable.undefined
+ count: 7
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$ly2 might not be defined\.$#'
+ identifier: variable.undefined
+ count: 7
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$newarr might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$notfullwidth might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$outerfilled might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$outerfontsize might not be defined\.$#'
+ identifier: variable.undefined
+ count: 8
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$outerp might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$p might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$preroth might not be defined\.$#'
+ identifier: variable.undefined
+ count: 6
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$prerotw might not be defined\.$#'
+ identifier: variable.undefined
+ count: 6
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$ratio might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$rlm might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$rot_bpos might not be defined\.$#'
+ identifier: variable.undefined
+ count: 4
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$rot_rpos might not be defined\.$#'
+ identifier: variable.undefined
+ count: 4
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$sarr might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$save_fill might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$sp_present might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$sp_reset might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$sp_suppress might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$sp_type might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$t might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$texto might not be defined\.$#'
+ identifier: variable.undefined
+ count: 9
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$textw might not be defined\.$#'
+ identifier: variable.undefined
+ count: 3
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$thuborddet might not be defined\.$#'
+ identifier: variable.undefined
+ count: 3
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$tisbnm might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$tlm might not be defined\.$#'
+ identifier: variable.undefined
+ count: 3
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$tntborddet might not be defined\.$#'
+ identifier: variable.undefined
+ count: 7
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$tp_present might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$tp_reset might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$tp_suppress might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$tp_type might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$up might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$ut might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$va in isset\(\) always exists and is not nullable\.$#'
+ identifier: isset.variable
+ count: 3
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$w might not be defined\.$#'
+ identifier: variable.undefined
+ count: 12
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$x0 might not be defined\.$#'
+ identifier: variable.undefined
+ count: 3
+ path: src/Mpdf.php
+
+ -
+ message: '#^Variable \$zarr might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Mpdf.php
+
+ -
+ message: '#^Array has 2 duplicate keys with value ''rphf'' \(''rphf'', ''rphf''\)\.$#'
+ identifier: array.duplicateKey
+ count: 1
+ path: src/Otl.php
+
+ -
+ message: '#^Offset int on array\{\} in isset\(\) does not exist\.$#'
+ identifier: isset.offset
+ count: 2
+ path: src/Otl.php
+
+ -
+ message: '#^Offset int\<0, max\> on array\{\} in isset\(\) does not exist\.$#'
+ identifier: isset.offset
+ count: 4
+ path: src/Otl.php
+
+ -
+ message: '#^Offset int\<1, max\> on array\{\} in isset\(\) does not exist\.$#'
+ identifier: isset.offset
+ count: 2
+ path: src/Otl.php
+
+ -
+ message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#'
+ identifier: phpDoc.parseError
+ count: 2
+ path: src/Otl.php
+
+ -
+ message: '#^Variable \$Backtrack might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Otl.php
+
+ -
+ message: '#^Variable \$ChainSubRule might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Otl.php
+
+ -
+ message: '#^Variable \$CoverageBacktrackOffset might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Otl.php
+
+ -
+ message: '#^Variable \$CoverageInputOffset might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Otl.php
+
+ -
+ message: '#^Variable \$CoverageLookaheadOffset might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Otl.php
+
+ -
+ message: '#^Variable \$GlyphID might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Otl.php
+
+ -
+ message: '#^Variable \$Input might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Otl.php
+
+ -
+ message: '#^Variable \$Lookahead might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Otl.php
+
+ -
+ message: '#^Variable \$LookupListIndex might not be defined\.$#'
+ identifier: variable.undefined
+ count: 4
+ path: src/Otl.php
+
+ -
+ message: '#^Variable \$PosLookupRecord might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Otl.php
+
+ -
+ message: '#^Variable \$SequenceIndex might not be defined\.$#'
+ identifier: variable.undefined
+ count: 4
+ path: src/Otl.php
+
+ -
+ message: '#^Variable \$SubstLookupRecord might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Otl.php
+
+ -
+ message: '#^Variable \$Value might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Otl.php
+
+ -
+ message: '#^Variable \$backtrackGlyphs might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Otl.php
+
+ -
+ message: '#^Variable \$cctr might not be defined\.$#'
+ identifier: variable.undefined
+ count: 3
+ path: src/Otl.php
+
+ -
+ message: '#^Variable \$ic might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Otl.php
+
+ -
+ message: '#^Variable \$lookaheadGlyphs might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Otl.php
+
+ -
+ message: '#^Variable \$newOTLdata might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Otl.php
+
+ -
+ message: '#^Variable \$next_level might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Otl.php
+
+ -
+ message: '#^Variable \$shift might not be defined\.$#'
+ identifier: variable.undefined
+ count: 6
+ path: src/Otl.php
+
+ -
+ message: '#^Variable \$useGSUBtags might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Otl.php
+
+ -
+ message: '#^Undefined variable\: \$rtlPUAarr$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/OtlDump.php
+
+ -
+ message: '#^Undefined variable\: \$rtlPUAstr$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/OtlDump.php
+
+ -
+ message: '#^Variable \$GSLookup might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/OtlDump.php
+
+ -
+ message: '#^Variable \$GSUBScriptLang might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/OtlDump.php
+
+ -
+ message: '#^Variable \$MarkGlyphSetsDef_offset might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/OtlDump.php
+
+ -
+ message: '#^Variable \$arr might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/OtlDump.php
+
+ -
+ message: '#^Variable \$backtrackGlyphs might not be defined\.$#'
+ identifier: variable.undefined
+ count: 4
+ path: src/OtlDump.php
+
+ -
+ message: '#^Variable \$gsub might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/OtlDump.php
+
+ -
+ message: '#^Variable \$lookaheadGlyphs might not be defined\.$#'
+ identifier: variable.undefined
+ count: 4
+ path: src/OtlDump.php
+
+ -
+ message: '#^Variable \$subRule might not be defined\.$#'
+ identifier: variable.undefined
+ count: 4
+ path: src/OtlDump.php
+
+ -
+ message: '#^Variable \$type might not be defined\.$#'
+ identifier: variable.undefined
+ count: 3
+ path: src/OtlDump.php
+
+ -
+ message: '#^Variable \$new_reph_pos might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Shaper/Indic.php
+
+ -
+ message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#'
+ identifier: phpDoc.parseError
+ count: 2
+ path: src/TTFontFile.php
+
+ -
+ message: '#^Undefined variable\: \$fsSelection$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/TTFontFile.php
+
+ -
+ message: '#^Undefined variable\: \$post$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/TTFontFile.php
+
+ -
+ message: '#^Variable \$MarkGlyphSetsDef_offset might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/TTFontFile.php
+
+ -
+ message: '#^Variable \$arr might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/TTFontFile.php
+
+ -
+ message: '#^Variable \$backtrackGlyphs might not be defined\.$#'
+ identifier: variable.undefined
+ count: 6
+ path: src/TTFontFile.php
+
+ -
+ message: '#^Variable \$format might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/TTFontFile.php
+
+ -
+ message: '#^Variable \$glyphData might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/TTFontFile.php
+
+ -
+ message: '#^Variable \$head_start might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/TTFontFile.php
+
+ -
+ message: '#^Variable \$kk might not be defined\.$#'
+ identifier: variable.undefined
+ count: 4
+ path: src/TTFontFile.php
+
+ -
+ message: '#^Variable \$locaH might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/TTFontFile.php
+
+ -
+ message: '#^Variable \$locax might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/TTFontFile.php
+
+ -
+ message: '#^Variable \$lookaheadGlyphs might not be defined\.$#'
+ identifier: variable.undefined
+ count: 5
+ path: src/TTFontFile.php
+
+ -
+ message: '#^Variable \$type might not be defined\.$#'
+ identifier: variable.undefined
+ count: 3
+ path: src/TTFontFile.php
+
+ -
+ message: '#^Variable \$up might not be defined\.$#'
+ identifier: variable.undefined
+ count: 3
+ path: src/TTFontFile.php
+
+ -
+ message: '#^Binary operation "&" between '''' and 1 results in an error\.$#'
+ identifier: binaryOp.invalid
+ count: 1
+ path: src/TTFontFileAnalysis.php
+
+ -
+ message: '#^Binary operation "&" between '''' and 32 results in an error\.$#'
+ identifier: binaryOp.invalid
+ count: 1
+ path: src/TTFontFileAnalysis.php
+
+ -
+ message: '#^Variable \$TOC_end might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/TableOfContents.php
+
+ -
+ message: '#^Variable \$TOC_npages might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/TableOfContents.php
+
+ -
+ message: '#^Variable \$TOC_start might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/TableOfContents.php
+
+ -
+ message: '#^Method Mpdf\\Tag\:\:getTagInstance\(\) should return Mpdf\\Tag\\Tag but return statement is missing\.$#'
+ identifier: return.missing
+ count: 1
+ path: src/Tag.php
+
+ -
+ message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#'
+ identifier: phpDoc.parseError
+ count: 2
+ path: src/Tag.php
+
+ -
+ message: '#^Variable \$blockstate might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Tag/BlockTag.php
+
+ -
+ message: '#^Variable \$added_page might not be defined\.$#'
+ identifier: variable.undefined
+ count: 5
+ path: src/Tag/Table.php
+
+ -
+ message: '#^Variable \$blockstate might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Tag/Table.php
+
+ -
+ message: '#^Variable \$fullpage might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Tag/Table.php
+
+ -
+ message: '#^Variable \$maxfirstrowheight might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Tag/Table.php
+
+ -
+ message: '#^Variable \$maxrowheight might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Tag/Table.php
+
+ -
+ message: '#^Variable \$remainingpage might not be defined\.$#'
+ identifier: variable.undefined
+ count: 6
+ path: src/Tag/Table.php
+
+ -
+ message: '#^Variable \$save_table might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Tag/Table.php
+
+ -
+ message: '#^Variable \$tableheight might not be defined\.$#'
+ identifier: variable.undefined
+ count: 4
+ path: src/Tag/Table.php
+
+ -
+ message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#'
+ identifier: phpDoc.parseError
+ count: 2
+ path: src/Tag/Tag.php
+
+ -
+ message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#'
+ identifier: phpDoc.parseError
+ count: 2
+ path: src/Writer/BackgroundWriter.php
+
+ -
+ message: '#^Variable \$f1 might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Writer/BackgroundWriter.php
+
+ -
+ message: '#^Variable \$f2 might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Writer/BackgroundWriter.php
+
+ -
+ message: '#^Variable \$fo_h might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Writer/BackgroundWriter.php
+
+ -
+ message: '#^Variable \$fo_w might not be defined\.$#'
+ identifier: variable.undefined
+ count: 3
+ path: src/Writer/BackgroundWriter.php
+
+ -
+ message: '#^Variable \$img might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Writer/BackgroundWriter.php
+
+ -
+ message: '#^Variable \$wmf_x might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Writer/BackgroundWriter.php
+
+ -
+ message: '#^Variable \$wmf_y might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Writer/BackgroundWriter.php
+
+ -
+ message: '#^Binary operation "/" between non\-falsy\-string and 2\.834645669291339 results in an error\.$#'
+ identifier: binaryOp.invalid
+ count: 2
+ path: src/Writer/BaseWriter.php
+
+ -
+ message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#'
+ identifier: phpDoc.parseError
+ count: 2
+ path: src/Writer/BaseWriter.php
+
+ -
+ message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#'
+ identifier: phpDoc.parseError
+ count: 2
+ path: src/Writer/BookmarkWriter.php
+
+ -
+ message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#'
+ identifier: phpDoc.parseError
+ count: 2
+ path: src/Writer/ColorWriter.php
+
+ -
+ message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#'
+ identifier: phpDoc.parseError
+ count: 2
+ path: src/Writer/FontWriter.php
+
+ -
+ message: '#^Variable \$codeToGlyph might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Writer/FontWriter.php
+
+ -
+ message: '#^Variable \$fontstream might not be defined\.$#'
+ identifier: variable.undefined
+ count: 2
+ path: src/Writer/FontWriter.php
+
+ -
+ message: '#^Variable \$ttfontsize might not be defined\.$#'
+ identifier: variable.undefined
+ count: 1
+ path: src/Writer/FontWriter.php
+
+ -
+ message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#'
+ identifier: phpDoc.parseError
+ count: 2
+ path: src/Writer/FormWriter.php
+
+ -
+ message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#'
+ identifier: phpDoc.parseError
+ count: 2
+ path: src/Writer/ImageWriter.php
+
+ -
+ message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#'
+ identifier: phpDoc.parseError
+ count: 2
+ path: src/Writer/JavaScriptWriter.php
+
+ -
+ message: '#^Binary operation "\*" between 2 and string results in an error\.$#'
+ identifier: binaryOp.invalid
+ count: 1
+ path: src/Writer/MetadataWriter.php
+
+ -
+ message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#'
+ identifier: phpDoc.parseError
+ count: 2
+ path: src/Writer/MetadataWriter.php
+
+ -
+ message: '#^Access to constant TYPE_STREAM on an unknown class pdf_parser\.$#'
+ identifier: class.notFound
+ count: 1
+ path: src/Writer/ObjectWriter.php
+
+ -
+ message: '#^Call to an undefined method Mpdf\\Mpdf\:\:pdf_write_value\(\)\.$#'
+ identifier: method.notFound
+ count: 2
+ path: src/Writer/ObjectWriter.php
+
+ -
+ message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#'
+ identifier: phpDoc.parseError
+ count: 2
+ path: src/Writer/ObjectWriter.php
+
+ -
+ message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#'
+ identifier: phpDoc.parseError
+ count: 2
+ path: src/Writer/OptionalContentWriter.php
+
+ -
+ message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#'
+ identifier: phpDoc.parseError
+ count: 2
+ path: src/Writer/PageWriter.php
+
+ -
+ message: '#^PHPDoc tag @throws has invalid value \(\\Kdyby\\StrictObjects\\\\Mpdf\\MpdfException\)\: Unexpected token "\\\\\\\\Mpdf\\\\MpdfException", expected TOKEN_HORIZONTAL_WS at offset 74 on line 3$#'
+ identifier: phpDoc.parseError
+ count: 2
+ path: src/Writer/ResourceWriter.php
diff --git a/vendor/mpdf/mpdf/phpstan.neon b/vendor/mpdf/mpdf/phpstan.neon
new file mode 100644
index 00000000..ad4fa5de
--- /dev/null
+++ b/vendor/mpdf/mpdf/phpstan.neon
@@ -0,0 +1,8 @@
+parameters:
+ level: 2
+ paths:
+ - src
+ treatPhpDocTypesAsCertain: false
+
+includes:
+ - phpstan-baseline.neon
\ No newline at end of file
diff --git a/vendor/mpdf/mpdf/src/AssetFetcher.php b/vendor/mpdf/mpdf/src/AssetFetcher.php
new file mode 100644
index 00000000..267ee996
--- /dev/null
+++ b/vendor/mpdf/mpdf/src/AssetFetcher.php
@@ -0,0 +1,120 @@
+mpdf = $mpdf;
+ $this->contentLoader = $contentLoader;
+ $this->http = $http;
+ $this->logger = $logger;
+ }
+
+ public function fetchDataFromPath($path, $originalSrc = null)
+ {
+ /**
+ * Prevents insecure PHP object injection through phar:// wrapper
+ * @see https://github.com/mpdf/mpdf/issues/949
+ * @see https://github.com/mpdf/mpdf/issues/1381
+ */
+ $wrapperChecker = new StreamWrapperChecker($this->mpdf);
+
+ if ($wrapperChecker->hasBlacklistedStreamWrapper($path)) {
+ throw new \Mpdf\Exception\AssetFetchingException('File contains an invalid stream. Only ' . implode(', ', $wrapperChecker->getWhitelistedStreamWrappers()) . ' streams are allowed.');
+ }
+
+ if ($originalSrc && $wrapperChecker->hasBlacklistedStreamWrapper($originalSrc)) {
+ throw new \Mpdf\Exception\AssetFetchingException('File contains an invalid stream. Only ' . implode(', ', $wrapperChecker->getWhitelistedStreamWrappers()) . ' streams are allowed.');
+ }
+
+ $this->mpdf->GetFullPath($path);
+
+ return $this->isPathLocal($path) || ($originalSrc !== null && $this->isPathLocal($originalSrc))
+ ? $this->fetchLocalContent($path, $originalSrc)
+ : $this->fetchRemoteContent($path);
+ }
+
+ public function fetchLocalContent($path, $originalSrc)
+ {
+ $data = '';
+
+ if ($originalSrc && $this->mpdf->basepathIsLocal && $check = @fopen($originalSrc, 'rb')) {
+ fclose($check);
+ $path = $originalSrc;
+ $this->logger->debug(sprintf('Fetching content of file "%s" with local basepath', $path), ['context' => LogContext::REMOTE_CONTENT]);
+
+ return $this->contentLoader->load($path);
+ }
+
+ if ($path && $check = @fopen($path, 'rb')) {
+ fclose($check);
+ $this->logger->debug(sprintf('Fetching content of file "%s" with non-local basepath', $path), ['context' => LogContext::REMOTE_CONTENT]);
+
+ return $this->contentLoader->load($path);
+ }
+
+ return $data;
+ }
+
+ public function fetchRemoteContent($path)
+ {
+ $data = '';
+
+ try {
+
+ $this->logger->debug(sprintf('Fetching remote content of file "%s"', $path), ['context' => LogContext::REMOTE_CONTENT]);
+
+ /** @var \Mpdf\PsrHttpMessageShim\Response $response */
+ $response = $this->http->sendRequest(new Request('GET', $path));
+
+ if (!str_starts_with((string) $response->getStatusCode(), '2')) {
+
+ $message = sprintf('Non-OK HTTP response "%s" on fetching remote content "%s" because of an error', $response->getStatusCode(), $path);
+ if ($this->mpdf->debug) {
+ throw new \Mpdf\MpdfException($message);
+ }
+
+ $this->logger->info($message);
+
+ return $data;
+ }
+
+ $data = $response->getBody()->getContents();
+
+ } catch (\InvalidArgumentException $e) {
+ $message = sprintf('Unable to fetch remote content "%s" because of an error "%s"', $path, $e->getMessage());
+ if ($this->mpdf->debug) {
+ throw new \Mpdf\MpdfException($message, 0, E_ERROR, null, null, $e);
+ }
+
+ $this->logger->warning($message);
+ }
+
+ return $data;
+ }
+
+ public function isPathLocal($path)
+ {
+ return str_starts_with($path, 'file://') || strpos($path, '://') === false; // @todo More robust implementation
+ }
+
+}
diff --git a/vendor/mpdf/mpdf/src/Cache.php b/vendor/mpdf/mpdf/src/Cache.php
index 5554575e..5161145f 100644
--- a/vendor/mpdf/mpdf/src/Cache.php
+++ b/vendor/mpdf/mpdf/src/Cache.php
@@ -28,10 +28,6 @@ public function __construct($basePath, $cleanupInterval = 3600)
protected function createBasePath($basePath)
{
if (!file_exists($basePath)) {
- if (!$this->createBasePath(dirname($basePath))) {
- return false;
- }
-
if (!$this->createDirectory($basePath)) {
return false;
}
@@ -46,15 +42,33 @@ protected function createBasePath($basePath)
protected function createDirectory($basePath)
{
- if (!mkdir($basePath)) {
+ $permissions = $this->getPermission($this->getExistingParentDirectory($basePath));
+ if (! mkdir($basePath, $permissions, true)) {
return false;
}
- if (!chmod($basePath, 0777)) {
- return false;
+ return true;
+ }
+
+ protected function getExistingParentDirectory($basePath)
+ {
+ $targetParent = dirname($basePath);
+ while ($targetParent !== '.' && ! is_dir($targetParent) && dirname($targetParent) !== $targetParent) {
+ $targetParent = dirname($targetParent);
}
- return true;
+ return realpath($targetParent);
+ }
+
+ protected function getPermission($basePath, $fallbackPermission = 0777)
+ {
+ if (! is_dir($basePath)) {
+ return $fallbackPermission;
+ }
+
+ $result = fileperms($basePath);
+
+ return $result ? $result & 0007777 : $fallbackPermission;
}
public function tempFilename($filename)
diff --git a/vendor/mpdf/mpdf/src/Color/ColorConverter.php b/vendor/mpdf/mpdf/src/Color/ColorConverter.php
index fe5e547d..9cb59ea4 100644
--- a/vendor/mpdf/mpdf/src/Color/ColorConverter.php
+++ b/vendor/mpdf/mpdf/src/Color/ColorConverter.php
@@ -193,6 +193,10 @@ private function convertPlain($color, array &$PDFAXwarnings = [])
} elseif (strpos($color, '#') === 0) { // case of #nnnnnn or #nnn
$c = $this->processHashColor($color);
} elseif (preg_match('/(rgba|rgb|device-cmyka|cmyka|device-cmyk|cmyk|hsla|hsl|spot)\((.*?)\)/', $color, $m)) {
+ // ignore colors containing CSS variables
+ if (str_starts_with(mb_strtolower($m[2]), 'var(--')) {
+ $m[2] = '0, 0, 0, 100';
+ }
$c = $this->processModeColor($m[1], explode(',', $m[2]));
}
diff --git a/vendor/mpdf/mpdf/src/Color/ColorSpaceRestrictor.php b/vendor/mpdf/mpdf/src/Color/ColorSpaceRestrictor.php
index 195138f5..aeed5dbb 100644
--- a/vendor/mpdf/mpdf/src/Color/ColorSpaceRestrictor.php
+++ b/vendor/mpdf/mpdf/src/Color/ColorSpaceRestrictor.php
@@ -38,11 +38,10 @@ class ColorSpaceRestrictor
* @param \Mpdf\Color\ColorModeConverter $colorModeConverter
* @param int $mode
*/
- public function __construct(Mpdf $mpdf, ColorModeConverter $colorModeConverter, $mode)
+ public function __construct(Mpdf $mpdf, ColorModeConverter $colorModeConverter)
{
$this->mpdf = $mpdf;
$this->colorModeConverter = $colorModeConverter;
- $this->mode = $mode;
}
/**
@@ -93,11 +92,11 @@ private function restrictSpotColorSpace($c, &$PDFAXwarnings = [])
if ($this->mpdf->PDFA && !$this->mpdf->PDFAauto) {
$PDFAXwarnings[] = "Spot color specified '" . $this->mpdf->spotColorIDs[$c[1]] . "' (converted to process color)";
}
- if ($this->mode != 3) {
+ if ($this->mpdf->restrictColorSpace != 3) {
$sp = $this->mpdf->spotColors[$this->mpdf->spotColorIDs[$c[1]]];
$c = $this->colorModeConverter->cmyk2rgb([4, $sp['c'], $sp['m'], $sp['y'], $sp['k']]);
}
- } elseif ($this->mode == 1) {
+ } elseif ($this->mpdf->restrictColorSpace == 1) {
$sp = $this->mpdf->spotColors[$this->mpdf->spotColorIDs[$c[1]]];
$c = $this->colorModeConverter->cmyk2gray([4, $sp['c'], $sp['m'], $sp['y'], $sp['k']]);
}
@@ -114,14 +113,14 @@ private function restrictSpotColorSpace($c, &$PDFAXwarnings = [])
*/
private function restrictRgbColorSpace($c, $color, &$PDFAXwarnings = [])
{
- if ($this->mpdf->PDFX || ($this->mpdf->PDFA && $this->mode == 3)) {
+ if ($this->mpdf->PDFX || ($this->mpdf->PDFA && $this->mpdf->restrictColorSpace == 3)) {
if (($this->mpdf->PDFA && !$this->mpdf->PDFAauto) || ($this->mpdf->PDFX && !$this->mpdf->PDFXauto)) {
$PDFAXwarnings[] = "RGB color specified '" . $color . "' (converted to CMYK)";
}
$c = $this->colorModeConverter->rgb2cmyk($c);
- } elseif ($this->mode == 1) {
+ } elseif ($this->mpdf->restrictColorSpace == 1) {
$c = $this->colorModeConverter->rgb2gray($c);
- } elseif ($this->mode == 3) {
+ } elseif ($this->mpdf->restrictColorSpace == 3) {
$c = $this->colorModeConverter->rgb2cmyk($c);
}
@@ -137,14 +136,14 @@ private function restrictRgbColorSpace($c, $color, &$PDFAXwarnings = [])
*/
private function restrictCmykColorSpace($c, $color, &$PDFAXwarnings = [])
{
- if ($this->mpdf->PDFA && $this->mode != 3) {
+ if ($this->mpdf->PDFA && $this->mpdf->restrictColorSpace != 3) {
if ($this->mpdf->PDFA && !$this->mpdf->PDFAauto) {
$PDFAXwarnings[] = "CMYK color specified '" . $color . "' (converted to RGB)";
}
$c = $this->colorModeConverter->cmyk2rgb($c);
- } elseif ($this->mode == 1) {
+ } elseif ($this->mpdf->restrictColorSpace == 1) {
$c = $this->colorModeConverter->cmyk2gray($c);
- } elseif ($this->mode == 2) {
+ } elseif ($this->mpdf->restrictColorSpace == 2) {
$c = $this->colorModeConverter->cmyk2rgb($c);
}
@@ -160,21 +159,21 @@ private function restrictCmykColorSpace($c, $color, &$PDFAXwarnings = [])
*/
private function restrictRgbaColorSpace($c, $color, &$PDFAXwarnings = [])
{
- if ($this->mpdf->PDFX || ($this->mpdf->PDFA && $this->mode == 3)) {
+ if ($this->mpdf->PDFX || ($this->mpdf->PDFA && $this->mpdf->restrictColorSpace == 3)) {
if (($this->mpdf->PDFA && !$this->mpdf->PDFAauto) || ($this->mpdf->PDFX && !$this->mpdf->PDFXauto)) {
$PDFAXwarnings[] = "RGB color with transparency specified '" . $color . "' (converted to CMYK without transparency)";
}
$c = $this->colorModeConverter->rgb2cmyk($c);
$c = [4, $c[1], $c[2], $c[3], $c[4]];
- } elseif ($this->mpdf->PDFA && $this->mode != 3) {
+ } elseif ($this->mpdf->PDFA && $this->mpdf->restrictColorSpace != 3) {
if (!$this->mpdf->PDFAauto) {
$PDFAXwarnings[] = "RGB color with transparency specified '" . $color . "' (converted to RGB without transparency)";
}
$c = $this->colorModeConverter->rgb2cmyk($c);
$c = [4, $c[1], $c[2], $c[3], $c[4]];
- } elseif ($this->mode == 1) {
+ } elseif ($this->mpdf->restrictColorSpace == 1) {
$c = $this->colorModeConverter->rgb2gray($c);
- } elseif ($this->mode == 3) {
+ } elseif ($this->mpdf->restrictColorSpace == 3) {
$c = $this->colorModeConverter->rgb2cmyk($c);
}
@@ -190,21 +189,21 @@ private function restrictRgbaColorSpace($c, $color, &$PDFAXwarnings = [])
*/
private function restrictCmykaColorSpace($c, $color, &$PDFAXwarnings = [])
{
- if ($this->mpdf->PDFA && $this->mode != 3) {
+ if ($this->mpdf->PDFA && $this->mpdf->restrictColorSpace != 3) {
if (($this->mpdf->PDFA && !$this->mpdf->PDFAauto) || ($this->mpdf->PDFX && !$this->mpdf->PDFXauto)) {
$PDFAXwarnings[] = "CMYK color with transparency specified '" . $color . "' (converted to RGB without transparency)";
}
$c = $this->colorModeConverter->cmyk2rgb($c);
$c = [3, $c[1], $c[2], $c[3]];
- } elseif ($this->mpdf->PDFX || ($this->mpdf->PDFA && $this->mode == 3)) {
+ } elseif ($this->mpdf->PDFX || ($this->mpdf->PDFA && $this->mpdf->restrictColorSpace == 3)) {
if (($this->mpdf->PDFA && !$this->mpdf->PDFAauto) || ($this->mpdf->PDFX && !$this->mpdf->PDFXauto)) {
$PDFAXwarnings[] = "CMYK color with transparency specified '" . $color . "' (converted to CMYK without transparency)";
}
$c = $this->colorModeConverter->cmyk2rgb($c);
$c = [3, $c[1], $c[2], $c[3]];
- } elseif ($this->mode == 1) {
+ } elseif ($this->mpdf->restrictColorSpace == 1) {
$c = $this->colorModeConverter->cmyk2gray($c);
- } elseif ($this->mode == 2) {
+ } elseif ($this->mpdf->restrictColorSpace == 2) {
$c = $this->colorModeConverter->cmyk2rgb($c);
}
diff --git a/vendor/mpdf/mpdf/src/Config/ConfigVariables.php b/vendor/mpdf/mpdf/src/Config/ConfigVariables.php
index 8eef5a80..fcbfc055 100644
--- a/vendor/mpdf/mpdf/src/Config/ConfigVariables.php
+++ b/vendor/mpdf/mpdf/src/Config/ConfigVariables.php
@@ -152,6 +152,7 @@ public function __construct()
'PDFA' => false,
// Overrides warnings making changes when possible to force PDFA1-b compliance
'PDFAauto' => false,
+ 'PDFAversion' => '1-B',
// Colour profile OutputIntent
// sRGB_IEC61966-2-1 (=default if blank and PDFA), or other added .icc profile
@@ -515,7 +516,7 @@ public function __construct()
'curlExecutionTimeout' => null,
'curlProxy' => null,
'curlProxyAuth' => null,
- 'curlUserAgent' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:13.0) Gecko/20100101 Firefox/13.0.1',
+ 'curlUserAgent' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:108.0) Gecko/20100101 Firefox/108.0',
'exposeVersion' => true,
];
diff --git a/vendor/mpdf/mpdf/src/Config/FontVariables.php b/vendor/mpdf/mpdf/src/Config/FontVariables.php
index 777ebf8a..a3694735 100644
--- a/vendor/mpdf/mpdf/src/Config/FontVariables.php
+++ b/vendor/mpdf/mpdf/src/Config/FontVariables.php
@@ -198,7 +198,7 @@ public function __construct()
'useOTL' => 0xFF,
],
"eeyekunicode" => [/* Meetei Mayek */
- 'R' => "Eeyek.ttf",
+ 'R' => "Eeyek-Regular.ttf",
],
"lannaalif" => [/* Tai Tham */
'R' => "lannaalif-v1-03.ttf",
diff --git a/vendor/mpdf/mpdf/src/Container/ContainerInterface.php b/vendor/mpdf/mpdf/src/Container/ContainerInterface.php
new file mode 100644
index 00000000..e0fb0d5c
--- /dev/null
+++ b/vendor/mpdf/mpdf/src/Container/ContainerInterface.php
@@ -0,0 +1,12 @@
+services = $services;
+ }
+
+ public function get($id)
+ {
+ if (!$this->has($id)) {
+ throw new \Mpdf\Container\NotFoundException(sprintf('Unable to find service of key "%s"', $id));
+ }
+
+ return $this->services[$id];
+ }
+
+ public function has($id)
+ {
+ return isset($this->services[$id]);
+ }
+
+ public function getServices()
+ {
+ return $this->services;
+ }
+
+}
diff --git a/vendor/mpdf/mpdf/src/CssManager.php b/vendor/mpdf/mpdf/src/CssManager.php
index 1ab5c14b..aeaf177a 100644
--- a/vendor/mpdf/mpdf/src/CssManager.php
+++ b/vendor/mpdf/mpdf/src/CssManager.php
@@ -5,6 +5,8 @@
use Mpdf\Color\ColorConverter;
use Mpdf\Css\TextVars;
use Mpdf\File\StreamWrapperChecker;
+use Mpdf\Http\ClientInterface;
+use Mpdf\PsrHttpMessageShim\Request;
use Mpdf\Utils\Arrays;
use Mpdf\Utils\UtfString;
@@ -31,6 +33,11 @@ class CssManager
*/
private $colorConverter;
+ /**
+ * @var \Mpdf\AssetFetcher
+ */
+ private $assetFetcher;
+
var $tablecascadeCSS;
var $cascadeCSS;
@@ -47,26 +54,21 @@ class CssManager
var $cell_border_dominance_T;
- /**
- * @var \Mpdf\RemoteContentFetcher
- */
- private $remoteContentFetcher;
-
- public function __construct(Mpdf $mpdf, Cache $cache, SizeConverter $sizeConverter, ColorConverter $colorConverter, RemoteContentFetcher $remoteContentFetcher)
+ public function __construct(Mpdf $mpdf, Cache $cache, SizeConverter $sizeConverter, ColorConverter $colorConverter, AssetFetcher $assetFetcher)
{
$this->mpdf = $mpdf;
$this->cache = $cache;
$this->sizeConverter = $sizeConverter;
+ $this->assetFetcher = $assetFetcher;
$this->tablecascadeCSS = [];
$this->CSS = [];
$this->cascadeCSS = [];
$this->tbCSSlvl = 0;
$this->colorConverter = $colorConverter;
- $this->remoteContentFetcher = $remoteContentFetcher;
}
- function ReadCSS($html)
+ public function ReadCSS($html)
{
preg_match_all('/