Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.75% covered (success)
93.75%
135 / 144
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
Pdf
93.75% covered (success)
93.75%
135 / 144
33.33% covered (danger)
33.33%
1 / 3
16.06
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 generate
91.43% covered (success)
91.43%
64 / 70
0.00% covered (danger)
0.00%
0 / 1
7.03
 generateFile
95.16% covered (success)
95.16%
59 / 62
0.00% covered (danger)
0.00%
0 / 1
8
1<?php
2
3/**
4 * PDF Export class for phpMyFAQ.
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2009-2026 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2009-10-07
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Export;
21
22use League\CommonMark\CommonMarkConverter;
23use League\CommonMark\Exception\CommonMarkException;
24use phpMyFAQ\Category;
25use phpMyFAQ\Configuration;
26use phpMyFAQ\Core\Exception;
27use phpMyFAQ\Date;
28use phpMyFAQ\Export;
29use phpMyFAQ\Export\Pdf\Wrapper;
30use phpMyFAQ\Faq;
31use phpMyFAQ\System;
32use phpMyFAQ\Tags;
33use phpMyFAQ\Translation;
34use phpMyFAQ\User\CurrentUser;
35
36/**
37 * Class PDF
38 *
39 * @package phpMyFAQ\Export
40 */
41class Pdf extends Export
42{
43    private readonly Wrapper $wrapper;
44
45    private Tags $tags;
46
47    private readonly CommonMarkConverter $commonMarkConverter;
48
49    /**
50     * Constructor.
51     *
52     * @param  Faq           $faq           FaqHelper object
53     * @param  Category      $category      Entity object
54     * @param  Configuration $configuration Configuration
55     */
56    public function __construct(Faq $faq, Category $category, Configuration $configuration)
57    {
58        $this->faq = $faq;
59        $this->category = $category;
60        $this->config = $configuration;
61
62        $this->tags = new Tags($this->config);
63
64        $this->wrapper = new Wrapper();
65        $this->wrapper->setConfig($this->config);
66
67        // Set PDF options
68        $this->wrapper->Open();
69        $this->wrapper->SetDisplayMode(zoom: 'real');
70
71        $this->commonMarkConverter = new CommonMarkConverter([
72            'html_input' => 'strip',
73            'allow_unsafe_links' => false,
74        ]);
75    }
76
77    /**
78     * Generates the export.
79     *
80     * @param int    $categoryId CategoryHelper Id
81     * @param bool   $downwards If true, downwards, otherwise upward ordering
82     * @param string $language Language
83     * @throws CommonMarkException
84     */
85    public function generate(int $categoryId = 0, bool $downwards = true, string $language = ''): string
86    {
87        // Set PDF options
88        $this->wrapper->enableBookmarks = true;
89        $this->wrapper->isFullExport = true;
90
91        $filename = 'FAQs.pdf';
92
93        // Initialize categories
94        $this->category->transform($categoryId);
95
96        $this->wrapper->setCategory($categoryId);
97        $this->wrapper->setCategories($this->category->getAllCategories());
98        $this->wrapper->SetCreator($this->config->getTitle() . ' - ' . System::getPoweredByPlainString());
99
100        $faqData = $this->faq->get(
101            queryType: 'faq_export_pdf',
102            categoryId: $categoryId,
103            downwards: $downwards,
104            lang: $language,
105        );
106
107        $currentCategory = 0;
108
109        foreach ($faqData as $faq) {
110            if (!is_array($faq)) {
111                continue;
112            }
113
114            $faqId = (int) ($faq['id'] ?? 0);
115            $faqCategoryId = (int) ($faq['category_id'] ?? 0);
116            $topic = (string) ($faq['topic'] ?? '');
117
118            $this->wrapper->AddPage();
119
120            // Bookmark for categories
121            if ($currentCategory !== $faqCategoryId) {
122                $this->wrapper->Bookmark(
123                    txt: html_entity_decode(
124                        $this->category->getCategoryName($faqCategoryId),
125                        ENT_QUOTES,
126                        encoding: 'utf-8',
127                    ),
128                    level: $this->category->getLevelOf($faqCategoryId) - 1,
129                    y: 0,
130                );
131            }
132
133            // Bookmark for FAQs
134            $this->wrapper->Bookmark(
135                txt: html_entity_decode($topic, ENT_QUOTES, encoding: 'utf-8'),
136                level: $this->category->getLevelOf($faqCategoryId),
137                y: 0,
138            );
139
140            $tags = $this->tags->getAllTagsById($faqId);
141
142            $this->wrapper->SetFont($this->wrapper->getCurrentFont(), style: 'b', size: 12);
143            $this->wrapper->WriteHTML('<h1>' . $this->category->getCategoryName($faqCategoryId) . '</h1>');
144            $this->wrapper->WriteHTML('<h2>' . $topic . '</h2>');
145            $this->wrapper->Ln(h: 10);
146
147            $this->wrapper->SetFont($this->wrapper->getCurrentFont(), style: '', size: 10);
148
149            $content = $this->config->get(item: 'main.enableMarkdownEditor')
150                ? trim($this->commonMarkConverter->convert((string) ($faq['content'] ?? ''))->getContent())
151                : trim((string) ($faq['content'] ?? ''));
152            $this->wrapper->WriteHTML($content);
153
154            $this->wrapper->Ln(h: 10);
155
156            if (array_key_exists('keywords', $faq)) {
157                $this->wrapper->Ln();
158                $this->wrapper->Write(
159                    h: 5,
160                    txt: Translation::getString(key: 'msgNewContentKeywords') . ' ' . (string) $faq['keywords'],
161                );
162            }
163
164            if ($tags !== []) {
165                $this->wrapper->Ln();
166                $this->wrapper->Write(
167                    h: 5,
168                    txt: Translation::getString(key: 'msgTags') . ': ' . implode(separator: ', ', array: $tags),
169                );
170            }
171
172            $this->wrapper->Ln();
173            $this->wrapper->Ln();
174            $this->wrapper->Write(
175                h: 5,
176                txt: Translation::getString(key: 'msgLastUpdateArticle')
177                    . Date::createIsoDate((string) ($faq['lastmodified'] ?? '')),
178            );
179
180            $currentCategory = $faqCategoryId;
181        }
182
183        // remove default header/footer
184        $this->wrapper->setPrintHeader(val: false);
185        $this->wrapper->addFaqToc();
186
187        return $this->wrapper->Output($filename, 'S');
188    }
189
190    /**
191     * Builds the PDF delivery for the given FAQ.
192     *
193     * @param array<string, mixed> $faqData
194     * @throws CommonMarkException
195     * @throws Exception
196     */
197    public function generateFile(array $faqData, ?string $filename = null): string
198    {
199        $title = (string) ($faqData['title'] ?? '');
200
201        if ($filename === null || $filename === '') {
202            // Default filename: FAQ-<id>-<language>.pdf
203            $name = 'FAQ-%s-%s.pdf';
204            $filename = sprintf($name, (string) ($faqData['id'] ?? ''), (string) ($faqData['lang'] ?? ''));
205        }
206
207        $date = new Date($this->config);
208
209        $this->wrapper->setFaq($faqData);
210        $this->wrapper->setCategory((int) ($faqData['category_id'] ?? 0));
211        $this->wrapper->setQuestion($title);
212        $this->wrapper->setCategories($this->category->getAllCategories());
213
214        // Set any item
215        $this->wrapper->SetTitle($title);
216        $this->wrapper->SetCreator($this->config->getTitle() . ' - ' . System::getPoweredByPlainString());
217
218        $this->wrapper->AddPage();
219        $this->wrapper->SetFont($this->wrapper->getCurrentFont(), style: '', size: 10);
220        $this->wrapper->SetDisplayMode(zoom: 'real');
221        $this->wrapper->Ln();
222        $this->wrapper->Ln();
223        $this->wrapper->WriteHTML('<h3>' . $title . '</h3>');
224        $this->wrapper->Ln(h: 5);
225        $this->wrapper->Ln();
226
227        $content = $this->config->get(item: 'main.enableMarkdownEditor')
228            ? $this->commonMarkConverter->convert((string) ($faqData['content'] ?? ''))->getContent()
229            : (string) ($faqData['content'] ?? '');
230        $this->wrapper->WriteHTML($content);
231
232        $attachmentList = $faqData['attachmentList'] ?? null;
233        if (is_array($attachmentList)) {
234            $this->wrapper->Ln(h: 10);
235            $this->wrapper->Ln();
236            $this->wrapper->Write(h: 5, txt: Translation::getString(key: 'msgAttachedFiles') . ':');
237            $this->wrapper->Ln(h: 5);
238            $this->wrapper->Ln();
239            $listItems = '<ul class="pb-4 mb-4 border-bottom">';
240            foreach ($attachmentList as $attachment) {
241                if (!is_array($attachment)) {
242                    continue;
243                }
244
245                $list = '<li><a href="%s">%s</a></li>';
246                $listItems .= sprintf(
247                    $list,
248                    (string) ($attachment['url'] ?? ''),
249                    (string) ($attachment['filename'] ?? ''),
250                );
251            }
252
253            $listItems .= '</ul>';
254            $this->wrapper->WriteHTML($listItems);
255        }
256
257        $this->wrapper->Ln(h: 10);
258        $this->wrapper->Ln();
259        $this->wrapper->SetFont($this->wrapper->getCurrentFont(), style: '', size: 9);
260        $this->wrapper->Write(
261            h: 5,
262            txt: Translation::getString(key: 'ad_entry_solution_id') . ': #' . (string) ($faqData['solution_id'] ?? ''),
263        );
264
265        // Check if the author name should be visible, according to the GDPR option
266        $currentUser = new CurrentUser($this->config);
267        $author = $currentUser->getUserVisibilityByEmail((string) ($faqData['email'] ?? ''))
268            ? (string) ($faqData['author'] ?? '')
269            : 'n/a';
270
271        $this->wrapper->SetAuthor($author);
272        $this->wrapper->Ln();
273        $this->wrapper->Write(h: 5, txt: Translation::getString(key: 'msgAuthor') . ': ' . $author);
274        $this->wrapper->Ln();
275        $this->wrapper->Write(
276            h: 5,
277            txt: Translation::getString(key: 'msgLastUpdateArticle') . $date->format((string) ($faqData['date'] ?? '')),
278        );
279
280        return $this->wrapper->Output($filename, 'S');
281    }
282}