Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
01d4b86
updated mpdf to 8.2 (requires php 8.0 now)
splitbrain Sep 2, 2025
c5c184f
first start at refactoring
splitbrain Nov 17, 2025
b883aec
add toc handling
splitbrain Nov 17, 2025
1d334a8
handle debug writing
splitbrain Nov 17, 2025
8c4b141
remove a few now unused methods
splitbrain Nov 17, 2025
081fc33
Handle styles
splitbrain Nov 17, 2025
2bef96e
move page collecting into separate classes
splitbrain Nov 20, 2025
ac65d8f
remove some moved methods
splitbrain Nov 20, 2025
6afd336
remove obsolete template event
splitbrain Nov 20, 2025
293d84e
more refactoring
splitbrain Nov 24, 2025
0eefd8f
decouple renderer
splitbrain Nov 25, 2025
ab12f14
removed more obsolete methods from action plugin
splitbrain Nov 25, 2025
f77f381
fixed issues from first real run
splitbrain Nov 26, 2025
decfd8d
extract $INPUT dependencies out of Collector classes
splitbrain Nov 26, 2025
4c3a95e
reimplement ImageProcessor differently
splitbrain Nov 26, 2025
38ca8b2
updated tests for Media resolving
splitbrain Nov 26, 2025
c617724
update general test
splitbrain Nov 26, 2025
8f1c137
removed unneeded custom exception
splitbrain Nov 26, 2025
bbcdb3f
rewrite internal links. implements #526
splitbrain Nov 26, 2025
4526935
address some of the remaining FIXMEs
splitbrain Nov 26, 2025
5eabda9
fix additional blank page at the end
splitbrain Nov 26, 2025
b706c93
fixed remaining sort test
splitbrain Nov 26, 2025
32d393a
have collectors check for page existance
splitbrain Nov 26, 2025
b23d7b8
for exporting single pages do not rely on $ID
splitbrain Nov 26, 2025
9220075
Added tests
splitbrain Nov 26, 2025
5340eaf
rector and codesnifffer fixes
splitbrain Nov 26, 2025
fbe4e9d
remove obsolete book chapter handling in action
splitbrain Nov 26, 2025
7002812
extracted PDF generation and sending out of action
splitbrain Nov 26, 2025
7542e5b
some minor fixes based on PR feedback
splitbrain Nov 27, 2025
01a7083
Use attributes in config class to denote how to initialize them
splitbrain Nov 27, 2025
f200011
make PdfExportService reusable in debug mode
splitbrain Nov 27, 2025
a9e9f33
introduce a first test for rendering - testing numbered headlines
splitbrain Nov 27, 2025
bee95f0
Overhaul chapter/header counting in renderer
splitbrain Nov 27, 2025
b08d547
pass the Config object into the renderer
splitbrain Nov 27, 2025
c124fc5
Some more tests testing rendering functionality
splitbrain Nov 28, 2025
212a1ea
fix config attrbutes and extend tests
splitbrain Nov 28, 2025
837fcad
update MPDF downgrade log/psr
splitbrain Dec 2, 2025
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
239 changes: 28 additions & 211 deletions action.php
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
<?php

use dokuwiki\Cache\Cache;
use dokuwiki\Extension\ActionPlugin;
use dokuwiki\Extension\Event;
use dokuwiki\Extension\EventHandler;
use dokuwiki\plugin\dw2pdf\MenuItem;
use dokuwiki\plugin\dw2pdf\src\AbstractCollector;
use dokuwiki\plugin\dw2pdf\src\Cache;
use dokuwiki\plugin\dw2pdf\src\CollectorFactory;
use dokuwiki\plugin\dw2pdf\src\Config;
use dokuwiki\plugin\dw2pdf\src\DokuPdf;
use dokuwiki\plugin\dw2pdf\src\Styles;
use dokuwiki\plugin\dw2pdf\src\Template;
use dokuwiki\plugin\dw2pdf\src\Writer;
use dokuwiki\StyleUtils;
use Mpdf\HTMLParserMode;
use Mpdf\MpdfException;

/**
Expand All @@ -27,41 +26,8 @@
*/
class action_plugin_dw2pdf extends ActionPlugin
{
/**
* Settings for current export, collected from url param, plugin config, global config
*
* @var array
*/
protected $exportConfig;
/** @var string template name, to use templates from dw2pdf/tpl/<template name> */
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);
}
}
protected $currentBookChapter = 0;

/**
* Return the value of currentBookChapter, which is the order of the file to be added in a book generation
Expand Down Expand Up @@ -90,153 +56,38 @@ public function register(EventHandler $controller)
public function convert(Event $event)
{
global $REV, $DATE_AT;
global $conf, $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 $REV, $DATE_AT;
global $INPUT;

$this->loadConfig();
$config = new Config($this->conf);
$collector = CollectorFactory::create($event->data, $REV, $DATE_AT);
$list = $collector->getPages();
$title = $collector->getTitle();
$cache = new Cache($config, $collector);


$list = array_map('cleanID', $list);
if (!$cache->useCache()) {
// generating the pdf may take a long time for larger wikis / namespaces with many pages
set_time_limit(0);

$skippedpages = [];
foreach ($list as $index => $pageid) {
if (auth_quickaclcheck($pageid) < AUTH_READ) {
$skippedpages[] = $pageid;
unset($list[$index]);
try {
$this->generatePDF($config, $collector, $cache->cache, $event);
} catch (Exception $e) {
// FIXME should we log here?
// FIXME there was special handling for BookCreator with $INPUT->has('selection') before
nice_die($e->getMessage());
}
}
$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));
}
$event->preventDefault(); // after prevent, $event->data cannot be changed

return [$title, $list];
// deliver the file
$this->sendPDFFile($cache->cache); //exits
}

/**
* 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
Expand Down Expand Up @@ -279,41 +130,28 @@ protected function wikiToDW2PDF($id, $rev = '', $date_at = '')
* @param Event $event
* @throws MpdfException
*/
protected function generatePDF($cachefile, $event)
protected function generatePDF(Config $config, AbstractCollector $collector, $cachefile, $event)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it logical to use an interface here for the collector?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... good question. Currently I need collectors to be children of AbstractCollector, because I use methods from it on the Collector. Using an Interface would make that more difficult, but I might be wrong.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not fluent with this kind of patterns, so for in depth ideas others better step in ;-)

{
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 = '';
}

$config = new Config($this->conf, $this->getDocumentLanguage($this->list[0]));
$mpdf = new DokuPDF($config);
$mpdf = new DokuPDF($config, $collector->getLanguage());
$styles = new Styles($config);
$template = new Template($this->getConf('template'), $this->getConf('qrcodescale'));
$template = new Template($config);
$writer = new Writer($mpdf, $template, $styles, $config->isDebugEnabled());

$writer->startDocument($this->title);
$writer->startDocument($collector->getTitle());
$writer->cover();

if($config->hasToC()) {
if ($config->hasToC()) {
$writer->toc($this->getLang('tocheader'));
}

// loop over all pages
$counter = 0;
foreach ($this->list as $page) {
$template->setContext($page, $this->title, $rev, $date_at, $INPUT->server->str('REMOTE_USER', '', true));

$this->currentBookChapter = $counter;
$counter++;
$pagehtml = $this->wikiToDW2PDF($page, $rev, $date_at);
$writer->wikiPage($pagehtml);
foreach ($collector->getPages() as $page) {
$template->setContext($collector, $page, $INPUT->server->str('REMOTE_USER', '', true));
$this->currentBookChapter = $counter++; //FIXME I don't like this
$writer->renderWikiPage($collector, $page);
}

// insert the back page
Expand Down Expand Up @@ -494,26 +332,5 @@ 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;
}
}
64 changes: 63 additions & 1 deletion src/AbstractCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ public function __construct(?int $rev = null, ?int $at = null)
$this->rev = $rev;
$this->at = $at;
$this->title = $INPUT->str('book_title');
$this->pages = $this->collect();

// 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
);
Copy link
Collaborator

@Klap-in Klap-in Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

book_skipforbiddenpages is not used anymore? Needs doc update and bookcreator fix.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe book_skipforbiddenpages is part of the mentioned changed exception handling...

Collectors will always obey ACLs thanks to above code. I think before a book export that contained ACL protected pages did return to the book creator page with an error unless skipforbiddenpages was set. Now it basically behaves like skipforbiddenpages is always set, because above code will skip those protected pages by default.

}

/**
Expand Down Expand Up @@ -55,6 +60,50 @@ public function getTitle(): string
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
*
Expand All @@ -64,4 +113,17 @@ 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);
}
}
Loading