Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions _test/ConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public function testDefaults(): void
$this->assertNull($config->getSavedSelection(), 'default savedselection');
$this->assertSame('', $config->getExportId(), 'default exportid');
$this->assertTrue($config->useCache(), 'default usecache');
$this->assertSame(2 * 1024 * 1024, $config->getFetchSize(), 'default fetchsize');

$this->assertSame('A4', $mpdfConfig['format'], 'default pagesize/orientation');
$this->assertSame(11, $mpdfConfig['default_font_size'], 'default font-size');
Expand Down Expand Up @@ -86,6 +87,7 @@ public function testloadPluginConfig(): void
'template' => 'modern',
'output' => 'inline',
'usestyles' => 'wrap,foo ',
'fetchsize' => 1024,
'watermark' => 'CONFIDENTIAL',
'qrcodescale' => '2.5',
'debug' => 1,
Expand All @@ -111,6 +113,7 @@ public function testloadPluginConfig(): void
$this->assertSame('modern', $config->getTemplateName(), 'from template');
$this->assertSame('inline', $config->getOutputTarget(), 'from output');
$this->assertSame(['wrap', 'foo'], $config->getStyledExtensions(), 'from usestyles');
$this->assertSame(1024, $config->getFetchSize(), 'from fetchsize');
$this->assertSame('CONFIDENTIAL', $config->getWatermarkText(), 'from watermark');
$this->assertTrue($mpdfConfig['showWatermarkText'], 'from watermark');
$this->assertSame(2.5, $config->getQRScale(), 'from qrcodescale');
Expand Down
123 changes: 123 additions & 0 deletions _test/DokuAssetFetcherTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

namespace dokuwiki\plugin\dw2pdf\test;

use dokuwiki\plugin\dw2pdf\src\Config;
use dokuwiki\plugin\dw2pdf\src\DokuMpdf;
use DokuWikiTest;
use Mpdf\AssetFetcherInterface;
use Mpdf\Mpdf;
use ReflectionProperty;

/**
* Tests for handing resolved media files over to mpdf.
*
* @group plugin_dw2pdf
* @group plugins
*/
class DokuAssetFetcherTest extends DokuWikiTest
{
/**
* Get the asset fetcher of a fresh DokuMpdf instance with the given basepath setting.
*
* Depending on basepathIsLocal mpdf uses either the first or the second argument of
* fetchDataFromPath(), so both cases need to be tested. The flag is false on any install
* where the baseurl host differs from the request host, a non-default port being enough.
*
* @param bool $basepathIsLocal Value to force for the mpdf flag
* @return AssetFetcherInterface
*/
protected function getFetcher(bool $basepathIsLocal): AssetFetcherInterface
{
$mpdf = new DokuMpdf(new Config(), 'en');
$mpdf->basepathIsLocal = $basepathIsLocal;

return (new ReflectionProperty(Mpdf::class, 'assetFetcher'))->getValue($mpdf);
}

/**
* @return array<string, array{0:string,1:string,2:bool}>
*/
public static function fetchProvider(): array
{
global $conf;

$media = DOKU_URL . 'lib/exe/fetch.php?media=wiki:dokuwiki-128.png';
$mediaFile = $conf['mediadir'] . '/wiki/dokuwiki-128.png';
$static = DOKU_URL . 'lib/images/throbber.gif';
$staticFile = DOKU_INC . 'lib/images/throbber.gif';

return [
'fetch url, local basepath' => [$media, $mediaFile, true],
'fetch url, remote basepath' => [$media, $mediaFile, false],
'static file, local basepath' => [$static, $staticFile, true],
'static file, remote basepath' => [$static, $staticFile, false],
];
}

/**
* The local file has to be read regardless of the basepath setting.
*
* The second argument is the unmodified source as mpdf passes it for every image.
*
* @dataProvider fetchProvider
*/
public function testFetchDataFromPathReadsLocalFile(string $source, string $expected, bool $basepathIsLocal): void
{
$data = $this->getFetcher($basepathIsLocal)->fetchDataFromPath($source, $source);

$this->assertSame(file_get_contents($expected), $data);
}

/**
* The resolved file has to win over the source mpdf was originally given.
*
* With a local basepath mpdf prefers the second argument whenever it can open it. For a
* media URL that means an anonymous HTTP request back into the wiki instead of our local
* copy. A readable file stands in for such a loadable source here.
*/
public function testFetchDataFromPathIgnoresOriginalSource(): void
{
global $conf;
$file = $conf['mediadir'] . '/wiki/dokuwiki-128.png';
$source = DOKU_URL . 'lib/exe/fetch.php?media=wiki:dokuwiki-128.png';
$loadable = DOKU_INC . 'lib/images/throbber.gif';

$data = $this->getFetcher(true)->fetchDataFromPath($source, $loadable);

$this->assertSame(file_get_contents($file), $data);
}

/**
* Our pseudo scheme has no stream wrapper, so it can only ever be read locally.
*
* @testWith [true]
* [false]
*/
public function testFetchDataFromPathReadsDw2pdfScheme(bool $basepathIsLocal): void
{
global $conf;
$file = $conf['mediadir'] . '/wiki/dokuwiki-128.png';
$source = 'dw2pdf://' . $file;

$data = $this->getFetcher($basepathIsLocal)->fetchDataFromPath($source, $source);

$this->assertSame(file_get_contents($file), $data);
}

/**
* Media this wiki serves is never requested back over HTTP when it cannot be resolved.
*
* @testWith [true]
* [false]
*/
public function testFetchDataFromPathRefusesUnresolvableMedia(bool $basepathIsLocal): void
{
$source = DOKU_URL . 'lib/exe/fetch.php?media=wiki:no-such-image.png';
$this->expectLogMessage('Media not available for PDF export');

$data = $this->getFetcher($basepathIsLocal)->fetchDataFromPath($source, $source);

$this->assertSame('', $data);
}
}
30 changes: 30 additions & 0 deletions _test/HttpClientTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace dokuwiki\plugin\dw2pdf\test;

use dokuwiki\plugin\dw2pdf\src\Config;
use dokuwiki\plugin\dw2pdf\src\HttpClient;
use DokuWikiTest;
use Mpdf\PsrHttpMessageShim\Request;

/**
* Tests for the HTTP client mpdf loads remote assets through.
*
* @group plugin_dw2pdf
* @group plugins
*/
class HttpClientTest extends DokuWikiTest
{
/**
* No remote host may be contacted when downloading is switched off.
*/
public function testSendRequestRefusesWhenDownloadingIsDisabled(): void
{
$this->expectLogMessage('Remote asset not downloaded for PDF export');

$request = new Request('GET', 'https://example.org/does-not-matter.png');
$response = (new HttpClient(new Config(['fetchsize' => 0])))->sendRequest($request);

$this->assertSame(403, $response->getStatusCode());
}
}
89 changes: 86 additions & 3 deletions _test/MediaLinkResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace dokuwiki\plugin\dw2pdf\test;

use dokuwiki\plugin\dw2pdf\src\Config;
use dokuwiki\plugin\dw2pdf\src\MediaLinkResolver;
use DokuWikiTest;

Expand All @@ -18,7 +19,7 @@ class MediaLinkResolverTest extends DokuWikiTest
public function setUp(): void
{
parent::setUp();
$this->resolver = new MediaLinkResolver();
$this->resolver = new MediaLinkResolver(new Config());
}

/**
Expand Down Expand Up @@ -80,16 +81,19 @@ public function testResolveDw2pdfScheme(): void
}

/**
* External media is downloaded even when the wiki does not allow fetch.php to do so.
*
* @group internet
*/
public function testResolveFetchesExternalMedia(): void
{
global $conf;
$conf['fetchsize'] = 512 * 1024; // 512 KB
$conf['fetchsize'] = 0;
$resolver = new MediaLinkResolver(new Config(['fetchsize' => 512 * 1024]));

$external = 'https://php.net/images/php.gif';
$input = DOKU_URL . 'lib/exe/fetch.php?media=' . rawurlencode($external);
$resolved = $this->resolver->resolve($input);
$resolved = $resolver->resolve($input);

if ($resolved === null) {
$this->markTestSkipped('External media fetching is not available in this environment.');
Expand All @@ -101,6 +105,85 @@ public function testResolveFetchesExternalMedia(): void
$this->assertSame(2523, filesize($resolved['path']));
}

/**
* Downloading external media can be switched off in the plugin configuration.
*/
public function testResolveSkipsExternalMediaWhenDownloadingIsDisabled(): void
{
$resolver = new MediaLinkResolver(new Config(['fetchsize' => 0]));

$external = 'https://php.net/images/php.gif';
$input = DOKU_URL . 'lib/exe/fetch.php?media=' . rawurlencode($external);

$this->assertNull($resolver->resolve($input));
}

/**
* Media marked nocache is still downloaded.
*
* @group internet
*/
public function testResolveFetchesNocacheExternalMedia(): void
{
$resolver = new MediaLinkResolver(new Config(['fetchsize' => 512 * 1024]));

$external = 'https://php.net/images/php.gif';
$input = DOKU_URL . 'lib/exe/fetch.php?media=' . rawurlencode($external) . '&cache=nocache';
$resolved = $resolver->resolve($input);

if ($resolved === null) {
$this->markTestSkipped('External media fetching is not available in this environment.');
}

$this->assertSame(2523, filesize($resolved['path']));
}

/**
* Without a cache parameter an existing copy is used no matter how old it is.
*/
public function testResolveKeepsCachedExternalMediaByDefault(): void
{
$external = 'http://127.0.0.1:1/unreachable.png';
$cacheFile = $this->seedOutdatedCache($external);

$resolver = new MediaLinkResolver(new Config(['fetchsize' => 1024 * 1024]));
$input = DOKU_URL . 'lib/exe/fetch.php?media=' . rawurlencode($external);

$resolved = $resolver->resolve($input);

$this->assertNotNull($resolved);
$this->assertSame($cacheFile, $resolved['path']);
}

/**
* Media marked recache expires, so an outdated copy is fetched again.
*/
public function testResolveExpiresCachedExternalMediaOnRecache(): void
{
$external = 'http://127.0.0.1:1/unreachable.png';
$this->seedOutdatedCache($external);

$resolver = new MediaLinkResolver(new Config(['fetchsize' => 1024 * 1024]));
$input = DOKU_URL . 'lib/exe/fetch.php?media=' . rawurlencode($external) . '&cache=recache';

$this->assertNull($resolver->resolve($input));
}

/**
* Write a week old cache file for the given external media URL.
*
* @param string $url The external media URL.
* @return string Absolute path to the cache file.
*/
protected function seedOutdatedCache(string $url): string
{
$cacheFile = getCacheName(strtolower($url), '.media.png');
io_saveFile($cacheFile, 'outdated bytes');
touch($cacheFile, time() - 7 * 86400);

return $cacheFile;
}

/**
* Non-image payloads should never be returned to the PDF generator.
*/
Expand Down
1 change: 1 addition & 0 deletions conf/default.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
$conf['template'] = 'default';
$conf['output'] = 'browser';
$conf['usecache'] = 1;
$conf['fetchsize'] = 2 * 1024 * 1024;
$conf['usestyles'] = 'wrap,';
$conf['qrcodescale'] = '1';
$conf['showexportbutton'] = 1;
1 change: 1 addition & 0 deletions conf/metadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
$meta['template'] = array('dirchoice', '_dir' => DOKU_PLUGIN . 'dw2pdf/tpl/');
$meta['output'] = array('multichoice', '_choices' => array('browser', 'file'));
$meta['usecache'] = array('onoff');
$meta['fetchsize'] = array('numeric');
$meta['usestyles'] = array('string');
$meta['qrcodescale'] = array('string', '_pattern' => '/^(|\d+(\.\d+)?)$/');
$meta['showexportbutton'] = array('onoff');
1 change: 1 addition & 0 deletions lang/en/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
$lang['output_o_browser'] = 'Show in browser';
$lang['output_o_file'] = 'Download the PDF';
$lang['usecache'] = 'Should PDFs be cached? Embedded images won\'t be ACL checked then, disable if that\'s a security concern for you.';
$lang['fetchsize'] = 'Maximum size (bytes) an export may download per remote file, such as an external image or font. <code>0</code> disables downloading, those files are then missing from the PDF.';
$lang['usestyles'] = 'You can give a comma separated list of plugins of which the <code>style.css</code> or <code>screen.css</code> should be used for PDF generation. By default only <code>print.css</code> and <code>pdf.css</code> are used.';
$lang['qrcodescale'] = 'Size scaling of the embedded QR code. Empty or <code>0</code> to disable.';
$lang['showexportbutton'] = 'Show PDF export button (only when supported by your template)';
12 changes: 12 additions & 0 deletions src/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ class Config
#[FromConfig]
protected bool $usecache = true;
#[FromConfig]
protected int $fetchSize = 2 * 1024 * 1024;
#[FromConfig]
protected array $useStyles = [];
#[FromConfig]
protected float $qrCodeScale = 0.0;
Expand Down Expand Up @@ -196,6 +198,16 @@ public function useCache(): bool
return $this->usecache;
}

/**
* Get the maximum number of bytes to download per remote file
*
* @return int
*/
public function getFetchSize(): int
{
return $this->fetchSize;
}

/**
* Get a list of extensions whose screen styles should be applied
*
Expand Down
Loading