Skip to content

Commit 40f45aa

Browse files
committed
Add disableTimeout() method
1 parent b06c424 commit 40f45aa

File tree

6 files changed

+153
-34
lines changed

6 files changed

+153
-34
lines changed

CHANGELOG.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,21 @@ The format is based on [Keep a Changelog(https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning(https://semver.org/spec/v2.0.0.html).
66

77
## Unreleased
8+
### Added
9+
- Add `--timeout` option to the WeasyPrint command-line call by default. This improves consistency with the internal process timeout already applied by Symfony Process. If you're running WeasyPrint inside a worker, queue, or other timeout-managed environment, you can disable it using `$pdf->disableTimeout()` or `$pdf->setTimeout(null)`. (#15)
10+
- Add `disableTimeout()` method to easily disable the new CLI timeout behavior
11+
12+
### Security
13+
- Update `symfony/process` minimal version to mitigate [CVE-2024-51736](https://github.com/advisories/GHSA-qq5c-677p-737q)
814

915
## 1.5.0 - 2024-11-04
1016
### Added
1117
- Support WeasyPrint 63.0 new `srgb` option
12-
- Added support for PHP 8.4
18+
- Add support for PHP 8.4
1319

1420
## 1.4.0 - 2023-11-20
1521
### Changed
16-
- Added support for Symfony 7.0 and PHP 8.3
22+
- Add support for Symfony 7.0 and PHP 8.3
1723

1824
## 1.3.0 - 2023-10-07
1925
### Added
@@ -39,7 +45,7 @@ and this project adheres to [Semantic Versioning(https://semver.org/spec/v2.0.0.
3945

4046
## 1.0.0 - 2023-01-16
4147
### Fixed
42-
- Fixed handling of repeatable options (attachment and stylesheet)
48+
- Fix handling of repeatable options (attachment and stylesheet)
4349

4450
### Changed
4551
- Bump symfony/process up to ^6.2

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,40 @@ $pdf->setOption('media-type', 'screen');
8181
$pdf->resetOptions();
8282
```
8383

84+
### Timeouts
85+
86+
A default timeout of 10 seconds is set for the WeasyPrint process to prevent orphaned or hanging processes.
87+
This is a defensive measure that applies in most cases and helps ensure system stability.
88+
89+
You can change the timeout with the `setTimeout()` method:
90+
91+
```php
92+
$pdf = new Pdf('/usr/local/bin/weasyprint');
93+
$pdf->setTimeout(30); // 30 seconds
94+
```
95+
96+
The timeout can be disabled entirely using either of the following:
97+
98+
```php
99+
$pdf->setTimeout(null);
100+
// or
101+
$pdf->disableTimeout();
102+
```
103+
104+
This is especially useful if you're running inside a *queue worker*, *job runner*, or other environments that already handle timeouts (e.g. Symfony Messenger, Laravel Queue, Supervisor).
105+
Disabling the internal timeout in those cases avoids conflicts with higher-level timeout strategies.
106+
107+
> **Note:**
108+
> The `setTimeout()` method affects **both**:
109+
> - how long the process is allowed to run before being forcibly stopped, and
110+
> - the `--timeout` option passed to the WeasyPrint command-line tool.
111+
>
112+
> If you only want to disable WeasyPrint's own timeout (while keeping the execution time limit), use:
113+
>
114+
> ```php
115+
> $pdf->setOption('timeout', null);
116+
> ```
117+
84118
## Differences with Snappy
85119
86120
Although PhpWeasyPrint and Snappy are interchangeable, there are a couple of differences between the two, due to WeasyPrint CLI API:

src/AbstractGenerator.php

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,21 @@ abstract class AbstractGenerator implements GeneratorInterface, LoggerAwareInter
3838
/**
3939
* @param array<string, bool|int|string|array|null> $options
4040
* @param array<string, mixed>|null $env
41+
*
42+
* @note
43+
* This class sets a default timeout on the process to prevent
44+
* orphaned or hanging processes. This is a defensive measure that applies
45+
* in most cases.
46+
*
47+
* If you run this inside a queue worker, job runner, or any environment
48+
* that already handles timeouts (e.g. Symfony Messenger, Laravel Queue),
49+
* you can disable the internal timeout using:
50+
*
51+
* $generator->disableTimeout();
52+
* or
53+
* $generator->setTimeout(null);
54+
*
55+
* This ensures no conflicts with higher-level timeout strategies.
4156
*/
4257
public function __construct(?string $binary = null, array $options = [], ?array $env = null)
4358
{
@@ -171,18 +186,7 @@ protected function buildCommand(string $binary, string $input, string $output, a
171186
$command .= ' --' . $key . ' ' . \escapeshellarg($v);
172187
}
173188
} else {
174-
switch ($key) {
175-
case 'format':
176-
$command .= ' --' . $key . ' ' . $option;
177-
break;
178-
case 'resolution':
179-
case 'timeout':
180-
$command .= ' --' . $key . ' ' . (int)$option;
181-
break;
182-
default:
183-
$command .= ' --' . $key . ' ' . \escapeshellarg((string)$option);
184-
break;
185-
}
189+
$command .= ' --' . $key . ' ' . \escapeshellarg((string)$option);
186190
}
187191
}
188192

@@ -259,6 +263,11 @@ public function setTimeout(?int $timeout): self
259263
return $this;
260264
}
261265

266+
public function disableTimeout(): self
267+
{
268+
return $this->setTimeout(null);
269+
}
270+
262271
/**
263272
* Defines the binary.
264273
*

src/Pdf.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ public function setTimeout(?int $timeout): self
4242
return $this;
4343
}
4444

45+
public function disableTimeout(): self
46+
{
47+
parent::disableTimeout();
48+
$this->setOption('timeout', null);
49+
50+
return $this;
51+
}
52+
4553
/**
4654
* @param array<string, bool|int|string|array|null> $options
4755
*
@@ -137,6 +145,54 @@ protected function configure(): void
137145
]);
138146
}
139147

148+
/**
149+
* Builds the command string.
150+
*
151+
* @param string $binary The binary path/name
152+
* @param string $input Url or file location of the page to process
153+
* @param string $output File location to the pdf-or-image-to-be
154+
* @param array<string, bool|int|string|array|null> $options An array of options
155+
*/
156+
protected function buildCommand(string $binary, string $input, string $output, array $options = []): string
157+
{
158+
$escapedBinary = \escapeshellarg($binary);
159+
$command = \is_executable($escapedBinary) ? $escapedBinary : $binary;
160+
161+
foreach ($options as $key => $option) {
162+
if (null === $option || false === $option) {
163+
continue;
164+
}
165+
166+
if (true === $option) {
167+
$command .= ' --' . $key;
168+
continue;
169+
}
170+
171+
if (\is_array($option)) {
172+
foreach ($option as $v) {
173+
$command .= ' --' . $key . ' ' . \escapeshellarg($v);
174+
}
175+
} else {
176+
switch ($key) {
177+
case 'format':
178+
$command .= ' --' . $key . ' ' . $option;
179+
break;
180+
case 'dpi':
181+
case 'jpeg-quality':
182+
case 'resolution':
183+
case 'timeout':
184+
$command .= ' --' . $key . ' ' . (int)$option;
185+
break;
186+
default:
187+
$command .= ' --' . $key . ' ' . \escapeshellarg((string)$option);
188+
break;
189+
}
190+
}
191+
}
192+
193+
return $command . (' ' . \escapeshellarg($input) . ' ' . \escapeshellarg($output));
194+
}
195+
140196
private function setOptionsWithContentCheck(): void
141197
{
142198
$this->optionsWithContentCheck = [

tests/Unit/AbstractGeneratorTest.php

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -587,15 +587,6 @@ public function dataForBuildCommand(): array
587587
],
588588
$theBinary . ' --attachment ' . \escapeshellarg('/path1') . ' --attachment ' . \escapeshellarg('/path2') . ' ' . \escapeshellarg('https://the.url/') . ' ' . \escapeshellarg('/the/path'),
589589
],
590-
[
591-
$theBinary,
592-
'https://the.url/',
593-
'/the/path',
594-
[
595-
'resolution' => 100,
596-
],
597-
$theBinary . ' --resolution 100 ' . \escapeshellarg('https://the.url/') . ' ' . \escapeshellarg('/the/path'),
598-
],
599590
];
600591
}
601592

@@ -915,7 +906,7 @@ private function getPHPExecutableFromPath(): ?string
915906
public function testFailingGenerateWithOutputContainingPharPrefix(): void
916907
{
917908
$media = $this->getMockBuilder(AbstractGenerator::class)
918-
->setMethods([
909+
->onlyMethods([
919910
'configure',
920911
'prepareOutput',
921912
])

tests/Unit/PdfTest.php

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public function testThatSomethingUsingTmpFolder(): void
6262
$testObject->setTemporaryFolder(__DIR__);
6363

6464
$testObject->getOutputFromHtml('<html></html>', ['stylesheet' => 'html {font-size: 16px;}']);
65-
$this->assertMatchesRegularExpression('/emptyBinary --stylesheet ' . $q . '.*' . $q . ' ' . $q . '.*' . $q . ' ' . $q . '.*' . $q . '/', $testObject->getLastCommand());
65+
$this->assertMatchesRegularExpression('/emptyBinary --stylesheet ' . $q . '.*' . $q . ' --timeout \d* ' . $q . '.*' . $q . ' ' . $q . '.*' . $q . '/', $testObject->getLastCommand());
6666
}
6767

6868
/**
@@ -114,48 +114,71 @@ public function dataOptions(): array
114114
return [
115115
'0 - no options' => [
116116
[],
117-
'/emptyBinary ' . $q . '.*\.html' . $q . ' ' . $q . '.*\.pdf' . $q . '/',
117+
'/emptyBinary --timeout \d* ' . $q . '.*\.html' . $q . ' ' . $q . '.*\.pdf' . $q . '/',
118118
],
119119

120120
'1 - pass a single stylesheet URL' => [
121121
['stylesheet' => 'https://google.com'],
122-
'/emptyBinary --stylesheet ' . $q . 'https:\/\/google\.com' . $q . ' ' . $q . '.*\.html' . $q . ' ' . $q . '.*\.pdf' . $q . '/',
122+
'/emptyBinary --stylesheet ' . $q . 'https:\/\/google\.com' . $q . ' --timeout \d* ' . $q . '.*\.html' . $q . ' ' . $q . '.*\.pdf' . $q . '/',
123123
],
124124

125125
'2 - pass a single stylesheet file' => [
126126
['stylesheet' => __DIR__ . '/../Fixture/style1.css'],
127-
'/emptyBinary --stylesheet ' . $q . \preg_quote(__DIR__ . '/../Fixture/style1.css', '/') . $q . ' ' . $q . '.*\.html' . $q . ' ' . $q . '.*\.pdf' . $q . '/',
127+
'/emptyBinary --stylesheet ' . $q . \preg_quote(__DIR__ . '/../Fixture/style1.css', '/') . $q . ' --timeout \d* ' . $q . '.*\.html' . $q . ' ' . $q . '.*\.pdf' . $q . '/',
128128
],
129129

130130
'3 - pass two stylesheet files' => [
131131
['stylesheet' => [__DIR__ . '/../Fixture/style1.css', __DIR__ . '/../Fixture/style2.css']],
132132
'/emptyBinary --stylesheet ' . $q . \preg_quote(__DIR__ . '/../Fixture/style1.css', '/') . $q . ' '
133-
. '--stylesheet ' . $q . \preg_quote(__DIR__ . '/../Fixture/style2.css', '/') . $q . ' ' . $q . '.*\.html' . $q . ' ' . $q . '.*\.pdf' . $q . '/',
133+
. '--stylesheet ' . $q . \preg_quote(__DIR__ . '/../Fixture/style2.css', '/') . $q . ' --timeout \d* ' . $q . '.*\.html' . $q . ' ' . $q . '.*\.pdf' . $q . '/',
134134
],
135135

136136
'4 - pass one stylesheet file and one inline css' => [
137137
['stylesheet' => [__DIR__ . '/../Fixture/style1.css', 'html {font-size: 24px;}']],
138138
'/emptyBinary --stylesheet ' . $q . \preg_quote(__DIR__ . '/../Fixture/style1.css', '/') . $q . ' '
139-
. '--stylesheet ' . $q . '.*php_weasyprint.*\.css' . $q . ' ' . $q . '.*\.html' . $q . ' ' . $q . '.*\.pdf' . $q . '/',
139+
. '--stylesheet ' . $q . '.*php_weasyprint.*\.css' . $q . ' --timeout \d* ' . $q . '.*\.html' . $q . ' ' . $q . '.*\.pdf' . $q . '/',
140140
],
141141

142142
'5 - save the given stylesheet CSS string into a temporary file and pass that filename' => [
143143
['stylesheet' => 'html {font-size: 16px;}'],
144-
'/emptyBinary --stylesheet ' . $q . '.*\.css' . $q . ' ' . $q . '.*\.html' . $q . ' ' . $q . '.*\.pdf' . $q . '/',
144+
'/emptyBinary --stylesheet ' . $q . '.*\.css' . $q . ' --timeout \d* ' . $q . '.*\.html' . $q . ' ' . $q . '.*\.pdf' . $q . '/',
145145
],
146146

147147
'6 - save the content of the given attachment URL to a file and pass that filename' => [
148148
['attachment' => 'https://www.google.com/favicon.ico'],
149-
'/emptyBinary --attachment ' . $q . '.*php_weasyprint.*\.temp' . $q . ' ' . $q . '.*\.html' . $q . ' ' . $q . '.*\.pdf' . $q . '/',
149+
'/emptyBinary --attachment ' . $q . '.*php_weasyprint.*\.temp' . $q . ' --timeout \d* ' . $q . '.*\.html' . $q . ' ' . $q . '.*\.pdf' . $q . '/',
150150
],
151151

152152
'7 - save the content of multiple attachments URL to files and pass those filenames' => [
153153
['attachment' => ['https://www.google.com/favicon.ico', 'https://github.githubassets.com/favicons/favicon.svg']],
154-
'/emptyBinary --attachment ' . $q . '.*php_weasyprint.*\.temp' . $q . ' --attachment ' . $q . '.*php_weasyprint.*\.temp' . $q . ' ' . $q . '.*\.html' . $q . ' ' . $q . '.*\.pdf' . $q . '/',
154+
'/emptyBinary --attachment ' . $q . '.*php_weasyprint.*\.temp' . $q . ' --attachment ' . $q . '.*php_weasyprint.*\.temp' . $q . ' --timeout \d* ' . $q . '.*\.html' . $q . ' ' . $q . '.*\.pdf' . $q . '/',
155+
],
156+
157+
'8 - set integer, string, and boolean options' => [
158+
['pdf-variant' => 'pdf/ua-1', 'dpi' => 300, 'timeout' => 60, 'srgb' => true, 'resolution' => 100],
159+
"/emptyBinary --pdf-variant 'pdf\/ua-1' --dpi 300 --timeout 60 --srgb --resolution 100 " . $q . '.*\.html' . $q . ' ' . $q . '.*\.pdf' . $q . '/',
155160
],
156161
];
157162
}
158163

164+
public function testDisableTimeout(): void
165+
{
166+
$testObject = new PdfSpy();
167+
$testObject->disableTimeout();
168+
$testObject->getOutputFromHtml('<html></html>');
169+
170+
$q = self::SHELL_ARG_QUOTE_REGEX;
171+
$expectedRegex = '/emptyBinary ' . $q . '.*\.html' . $q . ' ' . $q . '.*\.pdf' . $q . '/';
172+
173+
$this->assertMatchesRegularExpression($expectedRegex, $testObject->getLastCommand());
174+
175+
$testObject2 = new PdfSpy();
176+
$testObject2->setOption('timeout', null);
177+
$testObject2->getOutputFromHtml('<html></html>');
178+
179+
$this->assertMatchesRegularExpression($expectedRegex, $testObject2->getLastCommand());
180+
}
181+
159182
/**
160183
* @covers \Pontedilana\PhpWeasyPrint\Pdf::createTemporaryFile
161184
* @covers \Pontedilana\PhpWeasyPrint\Pdf::__destruct

0 commit comments

Comments
 (0)