]> BookStack Code Mirror - api-scripts/blob - php-book-to-static-site/book-to-static.php
Added bsfs to community project list
[api-scripts] / php-book-to-static-site / book-to-static.php
1 #!/usr/bin/env php
2 <?php
3
4 // API Credentials
5 // You can either provide them as environment variables
6 // or hard-code them in the empty strings below.
7 $apiUrl = getenv('BS_URL') ?: '';
8 $clientId = getenv('BS_TOKEN_ID') ?: '';
9 $clientSecret = getenv('BS_TOKEN_SECRET') ?: '';
10
11 // Output Folder
12 // Can be provided as a arguments when calling the script
13 // or be hard-coded as strings below.
14 $bookSlug = $argv[1] ?? '';
15 $outFolder = $argv[2] ?? './out';
16
17 // Script logic
18 ////////////////
19
20 // Check we have required options
21 if (empty($bookSlug) || empty($outFolder)) {
22     errorOut("Both a book slug and output folder must be provided");
23 }
24
25 // Create the output folder if it does not exist
26 if (!is_dir($outFolder)) {
27     mkdir($outFolder, 0777, true);
28 }
29
30 // Get full output directory and book details
31 $outDir = realpath($outFolder);
32 $book = getBookBySlug($bookSlug);
33
34 // Error out if we don't have a book
35 if (is_null($book)) {
36     errorOut("Could not find book with the URL slug: {$bookSlug}");
37 }
38
39 // Get all chapters and pages within the book
40 $chapters = getAllOfAtListEndpoint("api/chapters", ['filter[book_id]' => $book['id']]);
41 $pages = getAllOfAtListEndpoint("api/pages", ['filter[book_id]' => $book['id']]);
42
43 // Get the full content for each page
44 foreach ($pages as $index => $page) {
45     $pages[$index] = apiGetJson("api/pages/{$page['id']}");
46 }
47
48 // Create the image output directory
49 if (!is_dir($outDir . "/images")) {
50     mkdir($outDir . "/images", 0777, true);
51 }
52
53 // Find the pages that are not within a chapter
54 $directBookPages = array_filter($pages, function($page) {
55     return empty($page['chapter_id']);
56 });
57
58 // Create book index file
59 $bookIndex = getBookHtmlOutput($book, $chapters, $directBookPages);
60 file_put_contents($outDir . "/index.html", $bookIndex);
61
62 // Create a HTML file for each chapter
63 // in addition to each page within those chapters
64 foreach ($chapters as $chapter) {
65     $childPages = array_filter($pages, function($page) use ($chapter) {
66         return $page['chapter_id'] == $chapter['id'];
67     });
68     $chapterPage = getChapterHtmlOutput($chapter, $childPages);
69     file_put_contents($outDir . "/chapter-{$chapter['slug']}.html", $chapterPage);
70
71     foreach ($childPages as $childPage) {
72         $childPageContent = getPageHtmlOutput($childPage, $chapter);
73         $childPageContent = extractImagesFromHtml($childPageContent);
74         file_put_contents($outDir . "/page-{$childPage['slug']}.html", $childPageContent);
75     }
76 }
77
78 // Create a file for each direct child book page
79 foreach ($directBookPages as $directPage) {
80     $directPageContent = getPageHtmlOutput($directPage, null);
81     $directPageContent = extractImagesFromHtml($directPageContent);
82     file_put_contents($outDir . "/page-{$directPage['slug']}.html", $directPageContent);
83 }
84
85 /**
86  * Scan the given HTML for image URL's and extract those images
87  * to save them locally and update the HTML references to point
88  * to the local files.
89  */
90 function extractImagesFromHtml(string $html): string {
91     global $outDir;
92     static $savedImages = [];
93     $matches = [];
94     preg_match_all('/<img.*?src=["\'](.*?)[\'"].*?>/i', $html, $matches);
95     foreach (array_unique($matches[1] ?? []) as $url) {
96         $image = getImageFile($url);
97         if ($image === false) {
98             continue;
99         }
100
101         $name = basename($url);
102         $fileName = $name;
103         $count = 1;
104         while (isset($savedImages[$fileName])) {
105             $fileName = $count . '-' . $name;
106             $count++;
107         }
108
109         $savedImages[$fileName] = true;
110         file_put_contents($outDir . "/images/" . $fileName, $image);
111         $html = str_replace($url, "./images/" . $fileName, $html);
112     }
113     return $html;
114 }
115
116 /**
117  * Get an image file from the given URL.
118  * Checks if it's hosted on the same instance as the API we're
119  * using so that auth details can be provided for BookStack images
120  * in case local_secure images are in use.
121  */
122 function getImageFile(string $url): string {
123     global $apiUrl;
124     if (strpos(strtolower($url), strtolower($apiUrl)) === 0) {
125         $url = substr($url, strlen($apiUrl));
126         return apiGet($url);
127     }
128     return @file_get_contents($url);
129 }
130
131 /**
132  * Get the HTML representation of a book.
133  */
134 function getBookHtmlOutput(array $book, array $chapters, array $pages): string {
135     $content = "<h1>{$book['name']}</h1>";
136     $content .= "<p>{$book['description']}</p>";
137     $content .= "<hr>";
138     if (count($chapters) > 0) {
139         $content .= "<h3>Chapters</h3><ul>";
140         foreach ($chapters as $chapter) {
141             $content .= "<li><a href='./chapter-{$chapter['slug']}.html'>{$chapter['name']}</a></li>";
142         }
143         $content .= "</ul>";
144     }
145     if (count($pages) > 0) {
146         $content .= "<h3>Pages</h3><ul>";
147         foreach ($pages as $page) {
148             $content .= "<li><a href='./page-{$page['slug']}.html'>{$page['name']}</a></li>";
149         }
150         $content .= "</ul>";
151     }
152     return $content;
153 }
154
155 /**
156  * Get the HTML representation of a chapter.
157  */
158 function getChapterHtmlOutput(array $chapter, array $pages): string {
159     $content = "<p><a href='./index.html'>Back to book</a></p>";
160     $content .= "<h1>{$chapter['name']}</h1>";
161     $content .= "<p>{$chapter['description']}</p>";
162     $content .= "<hr>";
163     if (count($pages) > 0) {
164         $content .= "<h3>Pages</h3><ul>";
165         foreach ($pages as $page) {
166             $content .= "<li><a href='./page-{$page['slug']}.html'>{$page['name']}</a></li>";
167         }
168         $content .= "</ul>";
169     }
170     return $content;
171 }
172
173 /**
174  * Get the HTML representation of a page.
175  */
176 function getPageHtmlOutput(array $page, ?array $parentChapter): string {
177     if (is_null($parentChapter)) {
178         $content = "<p><a href='./index.html'>Back to book</a></p>";
179     } else {
180         $content = "<p><a href='./chapter-{$parentChapter['slug']}.html'>Back to chapter</a></p>";
181     }
182     $content .= "<h1>{$page['name']}</h1>";
183     $content .= "<div>{$page['html']}</div>";
184     return $content;
185 }
186
187 /**
188  * Get a single book by the slug or return null if not exists.
189  */
190 function getBookBySlug(string $slug): ?array {
191     $endpoint = 'api/books?' . http_build_query(['filter[slug]' => $slug]);
192     $resp = apiGetJson($endpoint);
193     $book = $resp['data'][0] ?? null;
194
195     if (!is_null($book)) {
196         $book = apiGetJson("api/books/{$book['id']}") ?? null;
197     }
198     return $book;
199 }
200
201 /**
202  * Get all books from the system API.
203  */
204 function getAllOfAtListEndpoint(string $endpoint, array $params): array {
205     $count = 100;
206     $offset = 0;
207     $total = 0;
208     $all = [];
209
210     do {
211         $endpoint = $endpoint . '?' . http_build_query(array_merge($params, ['count' => $count, 'offset' => $offset]));
212         $resp = apiGetJson($endpoint);
213
214         $total = $resp['total'] ?? 0;
215         $new = $resp['data'] ?? [];
216         array_push($all, ...$new);
217         $offset += $count;
218     } while ($offset < $total);
219
220     return $all;
221 }
222
223 /**
224  * Make a simple GET HTTP request to the API.
225  */
226 function apiGet(string $endpoint): string {
227     global $apiUrl, $clientId, $clientSecret;
228     $url = rtrim($apiUrl, '/') . '/' . ltrim($endpoint, '/');
229     $opts = ['http' => ['header' => "Authorization: Token {$clientId}:{$clientSecret}"]];
230     $context = stream_context_create($opts);
231     return @file_get_contents($url, false, $context);
232 }
233
234 /**
235  * Make a simple GET HTTP request to the API &
236  * decode the JSON response to an array.
237  */
238 function apiGetJson(string $endpoint): array {
239     $data = apiGet($endpoint);
240     return json_decode($data, true);
241 }
242
243 /**
244  * DEBUG: Dump out the given variables and exit.
245  */
246 function dd(...$args) {
247     foreach ($args as $arg) {
248         var_dump($arg);
249     }
250     exit(1);
251 }
252
253 /**
254  * Alert of an error then exit the script.
255  */
256 function errorOut(string $text) {
257     echo "ERROR: " .  $text;
258     exit(1);
259 }