Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
e66fd6b
Remove ReactPHP
TheSyscall Aug 26, 2026
8847aeb
Convert the HeadlessChrome class into a PdfPrintDriver
TheSyscall Mar 4, 2026
f1dbfe3
Add types to printable html document
TheSyscall Mar 4, 2026
5ec93d6
Use str_starts_with whereever possible
TheSyscall Mar 4, 2026
cd9e76c
Add method to create parameters for printing with the webdriver
TheSyscall Mar 4, 2026
3d95368
Add webdrivers for chrome and gecko (firefox)
TheSyscall Mar 4, 2026
3839aba
Use destructor instead of close method
TheSyscall Mar 4, 2026
4ff5e6d
Rewrite Pdfexport hook
TheSyscall Mar 4, 2026
ad35915
Move and rename HeadlessChrome to HeadlessChromeDriver
TheSyscall Mar 4, 2026
f775c83
Rename $targetId -> $frameId
TheSyscall Mar 5, 2026
0993c5d
Always waitForPageLoad after setting content
TheSyscall Mar 5, 2026
7ae353d
Implement basic driver selection
TheSyscall Mar 5, 2026
e3ce68a
Fix form connection validation
TheSyscall Mar 5, 2026
db21b0c
Use chrome Page.printToPDF
TheSyscall Mar 5, 2026
607d8be
Fix coverpage creation
TheSyscall Mar 5, 2026
39928f6
FIXUP: remove comment
TheSyscall Mar 5, 2026
1f73866
Use try catch
TheSyscall Mar 5, 2026
fa8a428
Format changes
TheSyscall Mar 5, 2026
e7fe006
Rename Driver to Backend
TheSyscall Mar 5, 2026
0e0e18b
Allow for filesystem based html transfer for HeadlesChromeBackend
TheSyscall Mar 5, 2026
06a8ada
Change ConfigController to CompatController
TheSyscall Mar 6, 2026
93ae7e1
Move BackendConfigForm away from ZendForms
TheSyscall Mar 6, 2026
5091bca
Change element order to reflect precedence
TheSyscall Mar 6, 2026
00a44a3
Add headers and short descriptions
TheSyscall Mar 6, 2026
eb7932a
Require two underscores to divide section and key
TheSyscall Mar 6, 2026
5a939eb
Move initialization of the backend to the backend locator
TheSyscall Mar 6, 2026
b149749
Add license headers
TheSyscall Mar 6, 2026
7def1f2
Use outerHTML instead of innerHTML to preseve class names on the body…
TheSyscall Mar 9, 2026
75b8b5d
Remove unused methods
TheSyscall Mar 9, 2026
6f4b2d6
Explicitly call close on the backend
TheSyscall Mar 9, 2026
25d8c49
Fixup: clear header & set transfer mode
TheSyscall Mar 10, 2026
97e1d35
Manually close filestorage
TheSyscall Mar 10, 2026
fa49b5b
Wait for chrome to close and force close it after a timeout
TheSyscall Mar 10, 2026
8cd0543
Use constants instead of magic numbers
TheSyscall Mar 10, 2026
dbc1e40
Relicense to GPL-3.0-only and add SPDX license headers
TheSyscall Mar 13, 2026
dd78b54
Code style changes
TheSyscall Mar 13, 2026
409c8ca
fixup! Code style changes
TheSyscall Mar 13, 2026
f672452
Use Callout instead of building something custom
TheSyscall Mar 13, 2026
1072953
Implement WebDriver Protocol with GuzzlePHP
TheSyscall Mar 18, 2026
d0bb9ab
Use new WebDriver implementation
TheSyscall Mar 18, 2026
6db0adb
Add support for layout plugins to ChromeWebdriver
TheSyscall Mar 19, 2026
c38387d
Code style changes
TheSyscall Mar 19, 2026
e7a8c7e
Add missing license headers and remove unused files
TheSyscall Mar 20, 2026
546d044
Code review suggestions
TheSyscall Mar 20, 2026
c4035f2
Use ConfigForm in IW2
TheSyscall Mar 23, 2026
4b482f9
Use 0/1 instead of n/y
TheSyscall Mar 23, 2026
9e9e37a
Move script blocks into separate JavaScript files
TheSyscall Mar 23, 2026
e78609b
Update config form to allow for an arbitrary number of backends
TheSyscall Mar 26, 2026
c14f7f1
Document webdriver installation
TheSyscall Mar 26, 2026
8bd4c45
Add docstrings
TheSyscall Apr 7, 2026
93c6069
Use ShellCommand abstraction
TheSyscall Apr 7, 2026
a0c563f
Add a function to check if adding a cover page is supported
TheSyscall Apr 8, 2026
fa9709e
Fix a deprecation with unset member access
TheSyscall Apr 8, 2026
61b8a99
fixme! Docstrings for ShellCommand
TheSyscall Apr 8, 2026
0f849fd
Bring back htmlToPdf function
TheSyscall Apr 8, 2026
ab1b456
Store the last working backend instance
TheSyscall Apr 10, 2026
3b17973
Remove types to stay backward compatible
TheSyscall Apr 10, 2026
650c3a0
Change `first` method to mimic the behaviour of the base class
TheSyscall Apr 10, 2026
5569fc2
Add deprecation warning for the old first method
TheSyscall Apr 16, 2026
c8ee3eb
Use new ConfigSectionForm
TheSyscall Apr 16, 2026
e37e127
Code review changes
TheSyscall Apr 16, 2026
6964ef9
Show a different name on rename
TheSyscall Apr 16, 2026
9c945d8
Update `first` method to mirror parent
TheSyscall Apr 16, 2026
c6fae99
Add option to disable a backend
TheSyscall May 11, 2026
2ecaf1d
Update to new ConfigSectionForm constructor
TheSyscall May 20, 2026
324a698
fixup! Properly log error and remove Hook::has guard
TheSyscall May 21, 2026
d7f3243
fixup! Remove extra log message
TheSyscall May 21, 2026
73f5e33
Update to new ConfigSectionForm
TheSyscall May 28, 2026
4586699
Rework wait-for-layout
TheSyscall May 22, 2026
fc9aedc
Add docstrings
TheSyscall Jun 23, 2026
47fa6f7
f! code format
TheSyscall Jul 1, 2026
114906e
f! Remove duplicate `closeBrowser()` call
TheSyscall Aug 25, 2026
b8e5d52
f! captured full regex instead of matched parts
TheSyscall Aug 25, 2026
cf7750c
Fix Chrome IO thread deadlock caused by distro wrapper scripts
TheSyscall Aug 25, 2026
65986f2
f! Make compatible with new config section form implementation
TheSyscall Aug 26, 2026
b4923de
Fix Chrome startup on Linux distros with stderr wrapper scripts
TheSyscall Aug 25, 2026
f9a2d00
Allow null on document title
TheSyscall Aug 28, 2026
c25f348
Add CSRF counter-measure to backend config form
TheSyscall Aug 31, 2026
e2b7bbe
Document systemd MemoryDenyWriteExecute workaround for Ubuntu
TheSyscall Aug 31, 2026
6c96603
Don't set a timeout to immediately clear
TheSyscall Sep 2, 2026
1e7da88
Remove centOS from the documentation
TheSyscall Sep 2, 2026
2dd01ea
f! fix typo
TheSyscall Sep 2, 2026
c0b9766
Remove troubleshooting steps
TheSyscall Sep 2, 2026
bd6a94e
f! Use constructor property promotion
TheSyscall Sep 2, 2026
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
112 changes: 103 additions & 9 deletions application/controllers/ConfigController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,21 @@
namespace Icinga\Module\Pdfexport\Controllers;

use Icinga\Application\Config;
use Icinga\Module\Pdfexport\Forms\ChromeBinaryForm;
use Icinga\Web\Controller;
use Icinga\Module\Pdfexport\Forms\BackendConfigForm;
use Icinga\Web\Form\ConfigSectionForm;
use Icinga\Web\Notification;
use Icinga\Web\Session;
use ipl\Html\Attributes;
use ipl\Html\Contract\Form;
use ipl\Html\HtmlString;
use ipl\Html\Table;
use ipl\Web\Compat\CompatController;
use Icinga\Web\Widget\Tabs;
use ipl\Web\Widget\ButtonLink;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\Link;

class ConfigController extends Controller
class ConfigController extends CompatController
{
public function init()
{
Expand All @@ -18,14 +29,97 @@ public function init()
parent::init();
}

public function chromeAction()
public function backendsAction(): void
{
$form = (new ChromeBinaryForm())
->setIniConfig(Config::module('pdfexport'));
$button = new ButtonLink(
$this->translate('Create a New Backend'),
'pdfexport/config/createbackend',
'plus',
['title' => $this->translate('Create a New Backend')],
);
$button->setBaseTarget('_next');
$this->addContent($button);

$form->handleRequest();
$table = new Table();
$table->setAttributes(Attributes::create([
'class' => 'table-row-selectable common-table',
'data-base-target' => '_next',
]));
$table->add(Table::tr([
Table::th($this->translate('Backend')),
Table::th($this->translate('Priority')),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

FWIW, /icingaweb2/config/userbackend displays vertical arrows next to multiple user backends and uses the order in the .ini file as prio. So the user doesn't have to manage such numbers.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I was thinking about adding the drag-and-drop behavior like in kubernetes-web.
I would want that to be part of icingaweb or ipl-web first.

]));

$this->view->tabs = $this->Module()->getConfigTabs()->activate('chrome');
$this->view->form = $form;
$config = Config::module('pdfexport');

$sections = [];
foreach ($config as $name => $data) {
$sections[] = [$name, $data, (int) $data->get('priority')];
}

usort($sections, function ($a, $b) {
return $a[2] <=> $b[2];
});

foreach ($sections as [$name, $data]) {
$table->add(Table::tr([
Table::td([
new Icon('print'),
new Link($name, 'pdfexport/config/backend?backend=' . $name),
]),
Table::td($data->get('priority')),
], [
'class' => 'clickable',
]));
}

$this->mergeTabs($this->Module()->getConfigTabs()->activate('backends'));
$this->addContent($table);
}

public function backendAction(): void

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Being able to rename a backend would be nice. See Icinga/icinga-sso-web#3

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I would leave that up to Icinga/icingaweb2#5480

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't be necessary if you use random IDs as section keys like Icinga/icinga-sso-web#3.

@TheSyscall TheSyscall May 11, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I don't think randomize IDs are necessary here. They make the config file hard to read and probably cause problems with configuration management. The INI section name is usually the name if the resource, so I implemented this exact behaviour in Icinga/icingaweb2#5480 Icinga/icingaweb2#5567.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Being able to deactivate a backend temporarily for testing would be nice, but I guess changing priorities will do it as well. You decide.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Implemented

{
$name = $this->params->shiftRequired('backend');
$this->addTitleTab($this->translate(sprintf('Edit %s', $name)));

$form = (new BackendConfigForm(Config::module('pdfexport'), $name))
->setCsrfCounterMeasureId(Session::getSession()->getId())
->on(Form::ON_SUBMIT, function () {
Notification::success($this->translate('Updated print backend'));
$this->redirectNow('__CLOSE__');
})
->on(ConfigSectionForm::ON_DELETE, function () {
Notification::success($this->translate('Print backend deleted'));
$this->redirectNow('__CLOSE__');
})
->on(ConfigSectionForm::ON_RENAME, function () {
Notification::success($this->translate('Print backend renamed'));
$this->redirectNow('__CLOSE__');
})
->handleRequest($this->getServerRequest());

$this->addContent(HtmlString::create($form->render()));
}

public function createbackendAction(): void
{
$this->addTitleTab($this->translate('Create Print Backend'));

$form = (new BackendConfigForm(Config::module('pdfexport'), null))
->setCsrfCounterMeasureId(Session::getSession()->getId())
->on(Form::ON_SUBMIT, function () {
Notification::success($this->translate('Created new print backend'));
$this->redirectNow('__CLOSE__');
})
->handleRequest($this->getServerRequest());

$this->addContent($form);
}

protected function mergeTabs(Tabs $tabs): void
{
foreach ($tabs->getTabs() as $tab) {
$this->tabs->add($tab->getName(), $tab);
}
}
}
181 changes: 181 additions & 0 deletions application/forms/BackendConfigForm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
<?php

// SPDX-FileCopyrightText: 2019 Icinga GmbH <https://icinga.com>
// SPDX-License-Identifier: GPL-3.0-or-later

namespace Icinga\Module\Pdfexport\Forms;

use Exception;
use Icinga\Module\Pdfexport\Backend\Chromedriver;
use Icinga\Module\Pdfexport\Backend\Geckodriver;
use Icinga\Module\Pdfexport\Backend\HeadlessChromeBackend;
use Icinga\Web\Form\ConfigSectionForm;
use ipl\Validator\CallbackValidator;
use ipl\Web\Common\CsrfCounterMeasure;

class BackendConfigForm extends ConfigSectionForm
{
use CsrfCounterMeasure;

public function assemble(): void
{
$this->addElement('number', 'priority', [
'label' => $this->translate('Priority'),
'required' => true,
'placeholder' => 100,
'min' => 0,
'description' => $this->translate('The priority of the backend. A lower priority will be used first.'),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This ordering seems counter-intuitive to me at first sight.

@flourish86 What do you think?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is the same order as menu items internally.

To be fair afaik. it is the first time that a user would have to interact with the number directly.

]);

$this->addElement('checkbox', 'enabled', [
'label' => $this->translate('Enabled'),
'value' => true,
'description' => $this->translate('If the backend is not enabled, it will be ignored for PDF generation.'),
]);

$this->addElement('select', 'type', [
'label' => $this->translate('Type'),
'multiOptions' => [
'' => sprintf(' - %s - ', t('Please choose')),
'chrome_webdriver' => t('Chrome WebDriver'),
'firefox_webdriver' => t('Firefox WebDriver'),
'remote_chrome' => t('Headless Chrome (Remote)'),
'local_chrome' => t('Headless Chrome (Local)'),
],
'required' => true,
'class' => 'autosubmit',
'value' => 'local_chrome',
]);

$type = $this->getValue('type');
Comment thread
Al2Klimov marked this conversation as resolved.

switch ($type) {
case 'remote_chrome':
$this->addElement('text', 'host', [

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So, you conditionally add elements, but no hidden ones? This can lose values on toggling type.

'label' => $this->translate('Host'),
'description' => $this->translate('Host address of the server with the running web browser.'),
'required' => true,
'validators' => [
new CallbackValidator(function ($value, CallbackValidator $validator) {
$port = $this->getValue('port') ?: 9222;

try {
$chrome = HeadlessChromeBackend::createRemote($value, $port);
$version = $chrome->getVersion();
} catch (Exception $e) {
$validator->addMessage($e->getMessage());
return false;
}

if ($version < HeadlessChromeBackend::MIN_SUPPORTED_CHROME_VERSION) {
$validator->addMessage(t(
'Chrome/Chromium supporting headless mode required'
. ' which is provided since version %s. Version detected: %s',
));
return false;
}

return true;
}),
],
]);

$this->addElement('number', 'port', [
'label' => $this->translate('Port'),
'description' => $this->translate('Port of the chrome developer tools. (Default: 9222)'),
'placeholder' => 9222,
'min' => 1,
'max' => 65535,
]);

break;

case 'local_chrome':
$this->addElement('text', 'binary', [
'label' => $this->translate('Binary'),
'placeholder' => '/usr/bin/google-chrome',
Comment thread
Al2Klimov marked this conversation as resolved.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(I can't evaluate it myself, as) both browsers run into a Gateway Timeout.

'description' => $this->translate('Path to the binary of the web browser.'),
'validators' => [
new CallbackValidator(function ($value, CallbackValidator $validator) {
if (empty($value)) {
return true;
}

try {
$chrome = (HeadlessChromeBackend::createLocal($value));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
$chrome = (HeadlessChromeBackend::createLocal($value));
$chrome = HeadlessChromeBackend::createLocal($value);

$version = $chrome->getVersion();
} catch (Exception $e) {
$validator->addMessage($e->getMessage());
return false;
}

if ($version < HeadlessChromeBackend::MIN_SUPPORTED_CHROME_VERSION) {
$validator->addMessage(t(
'Chrome/Chromium supporting headless mode required'
. ' which is provided since version %s. Version detected: %s',
));
}

return true;
}),
],
]);

$this->addElement('checkbox', 'force_temp_storage', [
'label' => $this->translate('Use temp storage'),
'description' => $this->translate(
'Use temp storage to transfer the html to the local chrome instance.'
),
'checkedValue' => '1',
'uncheckedValue' => '0',
]);

break;

case 'firefox_webdriver':
case 'chrome_webdriver':
$this->addElement('text', 'host', [
'label' => $this->translate('Host'),
'description' => $this->translate('Host address of the webdriver server'),
'required' => true,
'validators' => [
new CallbackValidator(function ($value, CallbackValidator $validator) use ($type) {
$port = $this->getValue('port') ?: 4444;

try {
$url = "$value:$port";
$backend = match ($type) {
'chrome_webdriver' => new Chromedriver($url),
'firefox_webdriver' => new Geckodriver($url),
default => throw new Exception("Invalid webdriver type $type"),
};

if (! $backend->isSupported()) {
$validator->addMessage(
t('The webdriver server reports that it is unable to generate PDFs'),
);
return false;
}
} catch (Exception $e) {
$validator->addMessage($e->getMessage());
return false;
}
return true;
}),
],
]);

$this->addElement('number', 'port', [
'label' => $this->translate('Port'),
'description' => $this->translate('Port of the webdriver instance. (Default: 4444)'),
'placeholder' => 4444,
'min' => 1,
'max' => 65535,
]);

break;
}

$this->addCsrfCounterMeasure();
}
}
Loading
Loading