]> BookStack Code Mirror - bookstack/blob - app/Entities/ExportService.php
move zip export into exportservice
[bookstack] / app / Entities / ExportService.php
1 <?php namespace BookStack\Entities;
2
3 use BookStack\Entities\Managers\BookContents;
4 use BookStack\Entities\Managers\PageContent;
5 use BookStack\Uploads\ImageService;
6 use DomPDF;
7 use Exception;
8 use SnappyPDF;
9 use League\HTMLToMarkdown\HtmlConverter;
10 use Throwable;
11 use ZipArchive;
12
13 class ExportService
14 {
15
16     protected $imageService;
17
18     /**
19      * ExportService constructor.
20      */
21     public function __construct(ImageService $imageService)
22     {
23         $this->imageService = $imageService;
24     }
25
26     /**
27      * Convert a page to a self-contained HTML file.
28      * Includes required CSS & image content. Images are base64 encoded into the HTML.
29      * @throws Throwable
30      */
31     public function pageToContainedHtml(Page $page)
32     {
33         $page->html = (new PageContent($page))->render();
34         $pageHtml = view('pages.export', [
35             'page' => $page,
36             'format' => 'html',
37         ])->render();
38         return $this->containHtml($pageHtml);
39     }
40
41     /**
42      * Convert a chapter to a self-contained HTML file.
43      * @throws Throwable
44      */
45     public function chapterToContainedHtml(Chapter $chapter)
46     {
47         $pages = $chapter->getVisiblePages();
48         $pages->each(function ($page) {
49             $page->html = (new PageContent($page))->render();
50         });
51         $html = view('chapters.export', [
52             'chapter' => $chapter,
53             'pages' => $pages,
54             'format' => 'html',
55         ])->render();
56         return $this->containHtml($html);
57     }
58
59     /**
60      * Convert a book to a self-contained HTML file.
61      * @throws Throwable
62      */
63     public function bookToContainedHtml(Book $book)
64     {
65         $bookTree = (new BookContents($book))->getTree(false, true);
66         $html = view('books.export', [
67             'book' => $book,
68             'bookChildren' => $bookTree,
69             'format' => 'html',
70         ])->render();
71         return $this->containHtml($html);
72     }
73
74     /**
75      * Convert a page to a PDF file.
76      * @throws Throwable
77      */
78     public function pageToPdf(Page $page)
79     {
80         $page->html = (new PageContent($page))->render();
81         $html = view('pages.export', [
82             'page' => $page,
83             'format' => 'pdf',
84         ])->render();
85         return $this->htmlToPdf($html);
86     }
87
88     /**
89      * Convert a chapter to a PDF file.
90      * @throws Throwable
91      */
92     public function chapterToPdf(Chapter $chapter)
93     {
94         $pages = $chapter->getVisiblePages();
95         $pages->each(function ($page) {
96             $page->html = (new PageContent($page))->render();
97         });
98
99         $html = view('chapters.export', [
100             'chapter' => $chapter,
101             'pages' => $pages,
102             'format' => 'pdf',
103         ])->render();
104
105         return $this->htmlToPdf($html);
106     }
107
108     /**
109      * Convert a book to a PDF file.
110      * @throws Throwable
111      */
112     public function bookToPdf(Book $book)
113     {
114         $bookTree = (new BookContents($book))->getTree(false, true);
115         $html = view('books.export', [
116             'book' => $book,
117             'bookChildren' => $bookTree,
118             'format' => 'pdf',
119         ])->render();
120         return $this->htmlToPdf($html);
121     }
122
123     /**
124      * Convert normal web-page HTML to a PDF.
125      * @throws Exception
126      */
127     protected function htmlToPdf(string $html): string
128     {
129         $containedHtml = $this->containHtml($html);
130         $useWKHTML = config('snappy.pdf.binary') !== false;
131         if ($useWKHTML) {
132             $pdf = SnappyPDF::loadHTML($containedHtml);
133             $pdf->setOption('print-media-type', true);
134         } else {
135             $pdf = DomPDF::loadHTML($containedHtml);
136         }
137         return $pdf->output();
138     }
139
140     /**
141      * Bundle of the contents of a html file to be self-contained.
142      * @throws Exception
143      */
144     protected function containHtml(string $htmlContent): string
145     {
146         $imageTagsOutput = [];
147         preg_match_all("/\<img.*src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
148
149         // Replace image src with base64 encoded image strings
150         if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
151             foreach ($imageTagsOutput[0] as $index => $imgMatch) {
152                 $oldImgTagString = $imgMatch;
153                 $srcString = $imageTagsOutput[2][$index];
154                 $imageEncoded = $this->imageService->imageUriToBase64($srcString);
155                 if ($imageEncoded === null) {
156                     $imageEncoded = $srcString;
157                 }
158                 $newImgTagString = str_replace($srcString, $imageEncoded, $oldImgTagString);
159                 $htmlContent = str_replace($oldImgTagString, $newImgTagString, $htmlContent);
160             }
161         }
162
163         $linksOutput = [];
164         preg_match_all("/\<a.*href\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $linksOutput);
165
166         // Replace image src with base64 encoded image strings
167         if (isset($linksOutput[0]) && count($linksOutput[0]) > 0) {
168             foreach ($linksOutput[0] as $index => $linkMatch) {
169                 $oldLinkString = $linkMatch;
170                 $srcString = $linksOutput[2][$index];
171                 if (strpos(trim($srcString), 'http') !== 0) {
172                     $newSrcString = url($srcString);
173                     $newLinkString = str_replace($srcString, $newSrcString, $oldLinkString);
174                     $htmlContent = str_replace($oldLinkString, $newLinkString, $htmlContent);
175                 }
176             }
177         }
178
179         // Replace any relative links with system domain
180         return $htmlContent;
181     }
182
183     /**
184      * Converts the page contents into simple plain text.
185      * This method filters any bad looking content to provide a nice final output.
186      */
187     public function pageToPlainText(Page $page): string
188     {
189         $html = (new PageContent($page))->render();
190         $text = strip_tags($html);
191         // Replace multiple spaces with single spaces
192         $text = preg_replace('/\ {2,}/', ' ', $text);
193         // Reduce multiple horrid whitespace characters.
194         $text = preg_replace('/(\x0A|\xA0|\x0A|\r|\n){2,}/su', "\n\n", $text);
195         $text = html_entity_decode($text);
196         // Add title
197         $text = $page->name . "\n\n" . $text;
198         return $text;
199     }
200
201     /**
202      * Convert a chapter into a plain text string.
203      */
204     public function chapterToPlainText(Chapter $chapter): string
205     {
206         $text = $chapter->name . "\n\n";
207         $text .= $chapter->description . "\n\n";
208         foreach ($chapter->pages as $page) {
209             $text .= $this->pageToPlainText($page);
210         }
211         return $text;
212     }
213
214     /**
215      * Convert a book into a plain text string.
216      */
217     public function bookToPlainText(Book $book): string
218     {
219         $bookTree = (new BookContents($book))->getTree(false, true);
220         $text = $book->name . "\n\n";
221         foreach ($bookTree as $bookChild) {
222             if ($bookChild->isA('chapter')) {
223                 $text .= $this->chapterToPlainText($bookChild);
224             } else {
225                 $text .= $this->pageToPlainText($bookChild);
226             }
227         }
228         return $text;
229     }
230
231     /**
232      * Convert a page to a Markdown file.
233      * @throws Throwable
234      */
235     public function pageToMarkdown(Page $page)
236     {
237         if (property_exists($page, 'markdown') && $page->markdown != '') {
238             return "# " . $page->name . "\n\n" . $page->markdown;
239         } else {
240             $converter = new HtmlConverter();
241             return "# " . $page->name . "\n\n" . $converter->convert($page->html);
242         }
243     }
244
245     /**
246      * Convert a chapter to a Markdown file.
247      * @throws Throwable
248      */
249     public function chapterToMarkdown(Chapter $chapter)
250     {
251         $text = "# " . $chapter->name . "\n\n";
252         $text .= $chapter->description . "\n\n";
253         foreach ($chapter->pages as $page) {
254             $text .= $this->pageToMarkdown($page);
255         }
256         return $text;
257     }
258
259     /**
260      * Convert a book into a plain text string.
261      */
262     public function bookToMarkdown(Book $book): string
263     {
264         $bookTree = (new BookContents($book))->getTree(false, true);
265         $text = "# " . $book->name . "\n\n";
266         foreach ($bookTree as $bookChild) {
267             if ($bookChild->isA('chapter')) {
268                 $text .= $this->chapterToMarkdown($bookChild);
269             } else {
270                 $text .= $this->pageToMarkdown($bookChild);
271             }
272         }
273         return $text;
274     }
275
276     /**
277      * Convert a book into a zip file.
278      */
279     public function bookToZip(Book $book): string
280     {
281         // TODO: Is not unlinking the file a security risk?
282         $z = new ZipArchive();
283         $z->open("book.zip", \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
284         $bookTree = (new BookContents($book))->getTree(false, true);
285         foreach ($bookTree as $bookChild) {
286             if ($bookChild->isA('chapter')) {
287                 $z->addEmptyDir($bookChild->name);
288                 foreach ($bookChild->pages as $page) {
289                     $filename = $bookChild->name . "/" . $page->name . ".md";
290                     $z->addFromString($filename, $this->pageToMarkdown($page));
291                 }
292             } else {
293                 $z->addFromString($bookChild->name . ".md", $this->pageToMarkdown($bookChild));
294             }
295         }
296         return "book.zip";
297     }
298 }