Dan Brown

Added book export and created export tests to cover

In reference to #177
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
3 use Activity; 3 use Activity;
4 use BookStack\Repos\EntityRepo; 4 use BookStack\Repos\EntityRepo;
5 use BookStack\Repos\UserRepo; 5 use BookStack\Repos\UserRepo;
6 +use BookStack\Services\ExportService;
6 use Illuminate\Http\Request; 7 use Illuminate\Http\Request;
7 use Illuminate\Http\Response; 8 use Illuminate\Http\Response;
8 use Views; 9 use Views;
...@@ -12,16 +13,19 @@ class BookController extends Controller ...@@ -12,16 +13,19 @@ class BookController extends Controller
12 13
13 protected $entityRepo; 14 protected $entityRepo;
14 protected $userRepo; 15 protected $userRepo;
16 + protected $exportService;
15 17
16 /** 18 /**
17 * BookController constructor. 19 * BookController constructor.
18 * @param EntityRepo $entityRepo 20 * @param EntityRepo $entityRepo
19 * @param UserRepo $userRepo 21 * @param UserRepo $userRepo
22 + * @param ExportService $exportService
20 */ 23 */
21 - public function __construct(EntityRepo $entityRepo, UserRepo $userRepo) 24 + public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
22 { 25 {
23 $this->entityRepo = $entityRepo; 26 $this->entityRepo = $entityRepo;
24 $this->userRepo = $userRepo; 27 $this->userRepo = $userRepo;
28 + $this->exportService = $exportService;
25 parent::__construct(); 29 parent::__construct();
26 } 30 }
27 31
...@@ -258,4 +262,49 @@ class BookController extends Controller ...@@ -258,4 +262,49 @@ class BookController extends Controller
258 session()->flash('success', trans('entities.books_permissions_updated')); 262 session()->flash('success', trans('entities.books_permissions_updated'));
259 return redirect($book->getUrl()); 263 return redirect($book->getUrl());
260 } 264 }
265 +
266 + /**
267 + * Export a book as a PDF file.
268 + * @param string $bookSlug
269 + * @return mixed
270 + */
271 + public function exportPdf($bookSlug)
272 + {
273 + $book = $this->entityRepo->getBySlug('book', $bookSlug);
274 + $pdfContent = $this->exportService->bookToPdf($book);
275 + return response()->make($pdfContent, 200, [
276 + 'Content-Type' => 'application/octet-stream',
277 + 'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.pdf'
278 + ]);
279 + }
280 +
281 + /**
282 + * Export a book as a contained HTML file.
283 + * @param string $bookSlug
284 + * @return mixed
285 + */
286 + public function exportHtml($bookSlug)
287 + {
288 + $book = $this->entityRepo->getBySlug('book', $bookSlug);
289 + $htmlContent = $this->exportService->bookToContainedHtml($book);
290 + return response()->make($htmlContent, 200, [
291 + 'Content-Type' => 'application/octet-stream',
292 + 'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.html'
293 + ]);
294 + }
295 +
296 + /**
297 + * Export a book as a plain text file.
298 + * @param $bookSlug
299 + * @return mixed
300 + */
301 + public function exportPlainText($bookSlug)
302 + {
303 + $book = $this->entityRepo->getBySlug('book', $bookSlug);
304 + $htmlContent = $this->exportService->bookToPlainText($book);
305 + return response()->make($htmlContent, 200, [
306 + 'Content-Type' => 'application/octet-stream',
307 + 'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.txt'
308 + ]);
309 + }
261 } 310 }
......
...@@ -439,7 +439,6 @@ class PageController extends Controller ...@@ -439,7 +439,6 @@ class PageController extends Controller
439 { 439 {
440 $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); 440 $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
441 $pdfContent = $this->exportService->pageToPdf($page); 441 $pdfContent = $this->exportService->pageToPdf($page);
442 -// return $pdfContent;
443 return response()->make($pdfContent, 200, [ 442 return response()->make($pdfContent, 200, [
444 'Content-Type' => 'application/octet-stream', 443 'Content-Type' => 'application/octet-stream',
445 'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf' 444 'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf'
......
...@@ -313,11 +313,12 @@ class EntityRepo ...@@ -313,11 +313,12 @@ class EntityRepo
313 * Loads the book slug onto child elements to prevent access database access for getting the slug. 313 * Loads the book slug onto child elements to prevent access database access for getting the slug.
314 * @param Book $book 314 * @param Book $book
315 * @param bool $filterDrafts 315 * @param bool $filterDrafts
316 + * @param bool $renderPages
316 * @return mixed 317 * @return mixed
317 */ 318 */
318 - public function getBookChildren(Book $book, $filterDrafts = false) 319 + public function getBookChildren(Book $book, $filterDrafts = false, $renderPages = false)
319 { 320 {
320 - $q = $this->permissionService->bookChildrenQuery($book->id, $filterDrafts)->get(); 321 + $q = $this->permissionService->bookChildrenQuery($book->id, $filterDrafts, $renderPages)->get();
321 $entities = []; 322 $entities = [];
322 $parents = []; 323 $parents = [];
323 $tree = []; 324 $tree = [];
...@@ -325,6 +326,10 @@ class EntityRepo ...@@ -325,6 +326,10 @@ class EntityRepo
325 foreach ($q as $index => $rawEntity) { 326 foreach ($q as $index => $rawEntity) {
326 if ($rawEntity->entity_type === 'BookStack\\Page') { 327 if ($rawEntity->entity_type === 'BookStack\\Page') {
327 $entities[$index] = $this->page->newFromBuilder($rawEntity); 328 $entities[$index] = $this->page->newFromBuilder($rawEntity);
329 + if ($renderPages) {
330 + $entities[$index]->html = $rawEntity->description;
331 + $entities[$index]->html = $this->renderPage($entities[$index]);
332 + };
328 } else if ($rawEntity->entity_type === 'BookStack\\Chapter') { 333 } else if ($rawEntity->entity_type === 'BookStack\\Chapter') {
329 $entities[$index] = $this->chapter->newFromBuilder($rawEntity); 334 $entities[$index] = $this->chapter->newFromBuilder($rawEntity);
330 $key = $entities[$index]->entity_type . ':' . $entities[$index]->id; 335 $key = $entities[$index]->entity_type . ':' . $entities[$index]->id;
......
1 <?php namespace BookStack\Services; 1 <?php namespace BookStack\Services;
2 2
3 +use BookStack\Book;
3 use BookStack\Page; 4 use BookStack\Page;
4 use BookStack\Repos\EntityRepo; 5 use BookStack\Repos\EntityRepo;
5 6
...@@ -25,24 +26,69 @@ class ExportService ...@@ -25,24 +26,69 @@ class ExportService
25 */ 26 */
26 public function pageToContainedHtml(Page $page) 27 public function pageToContainedHtml(Page $page)
27 { 28 {
28 - $cssContent = file_get_contents(public_path('/css/export-styles.css')); 29 + $pageHtml = view('pages/export', [
29 - $pageHtml = view('pages/export', ['page' => $page, 'pageContent' => $this->entityRepo->renderPage($page), 'css' => $cssContent])->render(); 30 + 'page' => $page,
31 + 'pageContent' => $this->entityRepo->renderPage($page)
32 + ])->render();
30 return $this->containHtml($pageHtml); 33 return $this->containHtml($pageHtml);
31 } 34 }
32 35
33 /** 36 /**
34 - * Convert a page to a pdf file. 37 + * Convert a book to a self-contained HTML file.
38 + * @param Book $book
39 + * @return mixed|string
40 + */
41 + public function bookToContainedHtml(Book $book)
42 + {
43 + $bookTree = $this->entityRepo->getBookChildren($book, true, true);
44 + $html = view('books/export', [
45 + 'book' => $book,
46 + 'bookChildren' => $bookTree
47 + ])->render();
48 + return $this->containHtml($html);
49 + }
50 +
51 + /**
52 + * Convert a page to a PDF file.
35 * @param Page $page 53 * @param Page $page
36 * @return mixed|string 54 * @return mixed|string
37 */ 55 */
38 public function pageToPdf(Page $page) 56 public function pageToPdf(Page $page)
39 { 57 {
40 - $cssContent = file_get_contents(public_path('/css/export-styles.css')); 58 + $html = view('pages/pdf', [
41 - $pageHtml = view('pages/pdf', ['page' => $page, 'pageContent' => $this->entityRepo->renderPage($page), 'css' => $cssContent])->render(); 59 + 'page' => $page,
60 + 'pageContent' => $this->entityRepo->renderPage($page)
61 + ])->render();
62 + return $this->htmlToPdf($html);
63 + }
64 +
65 + /**
66 + * Convert a book to a PDF file
67 + * @param Book $book
68 + * @return string
69 + */
70 + public function bookToPdf(Book $book)
71 + {
72 + $bookTree = $this->entityRepo->getBookChildren($book, true, true);
73 + $html = view('books/export', [
74 + 'book' => $book,
75 + 'bookChildren' => $bookTree
76 + ])->render();
77 + return $this->htmlToPdf($html);
78 + }
79 +
80 + /**
81 + * Convert normal webpage HTML to a PDF.
82 + * @param $html
83 + * @return string
84 + */
85 + protected function htmlToPdf($html)
86 + {
87 + $containedHtml = $this->containHtml($html);
42 $useWKHTML = config('snappy.pdf.binary') !== false; 88 $useWKHTML = config('snappy.pdf.binary') !== false;
43 - $containedHtml = $this->containHtml($pageHtml);
44 if ($useWKHTML) { 89 if ($useWKHTML) {
45 $pdf = \SnappyPDF::loadHTML($containedHtml); 90 $pdf = \SnappyPDF::loadHTML($containedHtml);
91 + $pdf->setOption('print-media-type', true);
46 } else { 92 } else {
47 $pdf = \PDF::loadHTML($containedHtml); 93 $pdf = \PDF::loadHTML($containedHtml);
48 } 94 }
...@@ -122,6 +168,29 @@ class ExportService ...@@ -122,6 +168,29 @@ class ExportService
122 return $text; 168 return $text;
123 } 169 }
124 170
171 + /**
172 + * Convert a book into a plain text string.
173 + * @param Book $book
174 + * @return string
175 + */
176 + public function bookToPlainText(Book $book)
177 + {
178 + $bookTree = $this->entityRepo->getBookChildren($book, true, true);
179 + $text = $book->name . "\n\n";
180 + foreach ($bookTree as $bookChild) {
181 + if ($bookChild->isA('chapter')) {
182 + $text .= $bookChild->name . "\n\n";
183 + $text .= $bookChild->description . "\n\n";
184 + foreach ($bookChild->pages as $page) {
185 + $text .= $this->pageToPlainText($page);
186 + }
187 + } else {
188 + $text .= $this->pageToPlainText($bookChild);
189 + }
190 + }
191 + return $text;
192 + }
193 +
125 } 194 }
126 195
127 196
......
...@@ -474,11 +474,13 @@ class PermissionService ...@@ -474,11 +474,13 @@ class PermissionService
474 /** 474 /**
475 * Get the children of a book in an efficient single query, Filtered by the permission system. 475 * Get the children of a book in an efficient single query, Filtered by the permission system.
476 * @param integer $book_id 476 * @param integer $book_id
477 - * @param bool $filterDrafts 477 + * @param bool $filterDrafts
478 + * @param bool $fetchPageContent
478 * @return \Illuminate\Database\Query\Builder 479 * @return \Illuminate\Database\Query\Builder
479 */ 480 */
480 - public function bookChildrenQuery($book_id, $filterDrafts = false) { 481 + public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) {
481 - $pageSelect = $this->db->table('pages')->selectRaw("'BookStack\\\\Page' as entity_type, id, slug, name, text, '' as description, book_id, priority, chapter_id, draft")->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) { 482 + $pageContentSelect = $fetchPageContent ? 'html' : "''";
483 + $pageSelect = $this->db->table('pages')->selectRaw("'BookStack\\\\Page' as entity_type, id, slug, name, text, {$pageContentSelect} as description, book_id, priority, chapter_id, draft")->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
482 $query->where('draft', '=', 0); 484 $query->where('draft', '=', 0);
483 if (!$filterDrafts) { 485 if (!$filterDrafts) {
484 $query->orWhere(function($query) { 486 $query->orWhere(function($query) {
......
...@@ -143,7 +143,7 @@ return [ ...@@ -143,7 +143,7 @@ return [
143 * the desired content might be different (e.g. screen or projection view of html file). 143 * the desired content might be different (e.g. screen or projection view of html file).
144 * Therefore allow specification of content here. 144 * Therefore allow specification of content here.
145 */ 145 */
146 - "DOMPDF_DEFAULT_MEDIA_TYPE" => "screen", 146 + "DOMPDF_DEFAULT_MEDIA_TYPE" => "print",
147 147
148 /** 148 /**
149 * The default paper size. 149 * The default paper size.
......
1 +<!doctype html>
2 +<html lang="en">
3 +<head>
4 + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
5 + <title>{{ $book->name }}</title>
6 +
7 + <style>
8 + {!! file_get_contents(public_path('/css/export-styles.css')) !!}
9 + .page-break {
10 + page-break-after: always;
11 + }
12 + .chapter-hint {
13 + color: #888;
14 + margin-top: 32px;
15 + }
16 + .chapter-hint + h1 {
17 + margin-top: 0;
18 + }
19 + ul.contents ul li {
20 + list-style: circle;
21 + }
22 + @media screen {
23 + .page-break {
24 + border-top: 1px solid #DDD;
25 + }
26 + }
27 + </style>
28 + @yield('head')
29 +</head>
30 +<body>
31 +<div class="container">
32 + <div class="row">
33 + <div class="col-md-8 col-md-offset-2">
34 + <div class="page-content">
35 +
36 + <h1 style="font-size: 4.8em">{{$book->name}}</h1>
37 +
38 + <p>{{ $book->description }}</p>
39 +
40 + @if(count($bookChildren) > 0)
41 + <ul class="contents">
42 + @foreach($bookChildren as $bookChild)
43 + <li><a href="#{{$bookChild->getType()}}-{{$bookChild->id}}">{{ $bookChild->name }}</a></li>
44 + @if($bookChild->isA('chapter') && count($bookChild->pages) > 0)
45 + <ul>
46 + @foreach($bookChild->pages as $page)
47 + <li><a href="#page-{{$page->id}}">{{ $page->name }}</a></li>
48 + @endforeach
49 + </ul>
50 + @endif
51 + @endforeach
52 + </ul>
53 + @endif
54 +
55 + @foreach($bookChildren as $bookChild)
56 + <div class="page-break"></div>
57 + <h1 id="{{$bookChild->getType()}}-{{$bookChild->id}}">{{ $bookChild->name }}</h1>
58 + @if($bookChild->isA('chapter'))
59 + <p>{{ $bookChild->description }}</p>
60 + @if(count($bookChild->pages) > 0)
61 + @foreach($bookChild->pages as $page)
62 + <div class="page-break"></div>
63 + <div class="chapter-hint">{{$bookChild->name}}</div>
64 + <h1 id="page-{{$page->id}}">{{ $page->name }}</h1>
65 + {!! $page->html !!}
66 + @endforeach
67 + @endif
68 + @else
69 + {!! $bookChild->html !!}
70 + @endif
71 + @endforeach
72 +
73 + </div>
74 + </div>
75 + </div>
76 +</div>
77 +</body>
78 +</html>
...@@ -10,6 +10,14 @@ ...@@ -10,6 +10,14 @@
10 </div> 10 </div>
11 <div class="col-sm-6"> 11 <div class="col-sm-6">
12 <div class="action-buttons faded"> 12 <div class="action-buttons faded">
13 + <span dropdown class="dropdown-container">
14 + <div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>{{ trans('entities.pages_export') }}</div>
15 + <ul class="wide">
16 + <li><a href="{{ $book->getUrl('/export/html') }}" target="_blank">{{ trans('entities.pages_export_html') }} <span class="text-muted float right">.html</span></a></li>
17 + <li><a href="{{ $book->getUrl('/export/pdf') }}" target="_blank">{{ trans('entities.pages_export_pdf') }} <span class="text-muted float right">.pdf</span></a></li>
18 + <li><a href="{{ $book->getUrl('/export/plaintext') }}" target="_blank">{{ trans('entities.pages_export_text') }} <span class="text-muted float right">.txt</span></a></li>
19 + </ul>
20 + </span>
13 @if(userCan('page-create', $book)) 21 @if(userCan('page-create', $book))
14 <a href="{{ $book->getUrl('/page/create') }}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>{{ trans('entities.pages_new') }}</a> 22 <a href="{{ $book->getUrl('/page/create') }}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>{{ trans('entities.pages_new') }}</a>
15 @endif 23 @endif
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
5 <title>{{ $page->name }}</title> 5 <title>{{ $page->name }}</title>
6 6
7 <style> 7 <style>
8 - {!! $css !!} 8 + {!! file_get_contents(public_path('/css/export-styles.css')) !!}
9 </style> 9 </style>
10 @yield('head') 10 @yield('head')
11 </head> 11 </head>
......
...@@ -26,6 +26,9 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -26,6 +26,9 @@ Route::group(['middleware' => 'auth'], function () {
26 Route::get('/{slug}/delete', 'BookController@showDelete'); 26 Route::get('/{slug}/delete', 'BookController@showDelete');
27 Route::get('/{bookSlug}/sort', 'BookController@sort'); 27 Route::get('/{bookSlug}/sort', 'BookController@sort');
28 Route::put('/{bookSlug}/sort', 'BookController@saveSort'); 28 Route::put('/{bookSlug}/sort', 'BookController@saveSort');
29 + Route::get('/{bookSlug}/export/html', 'BookController@exportHtml');
30 + Route::get('/{bookSlug}/export/pdf', 'BookController@exportPdf');
31 + Route::get('/{bookSlug}/export/plaintext', 'BookController@exportPlainText');
29 32
30 // Pages 33 // Pages
31 Route::get('/{bookSlug}/page/create', 'PageController@create'); 34 Route::get('/{bookSlug}/page/create', 'PageController@create');
......
1 +<?php namespace Tests;
2 +
3 +
4 +use BookStack\Page;
5 +
6 +class ExportTest extends TestCase
7 +{
8 +
9 + public function test_page_text_export()
10 + {
11 + $page = Page::first();
12 + $this->asEditor();
13 +
14 + $resp = $this->get($page->getUrl('/export/plaintext'));
15 + $resp->assertStatus(200);
16 + $resp->assertSee($page->name);
17 + $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.txt');
18 + }
19 +
20 + public function test_page_pdf_export()
21 + {
22 + $page = Page::first();
23 + $this->asEditor();
24 +
25 + $resp = $this->get($page->getUrl('/export/pdf'));
26 + $resp->assertStatus(200);
27 + $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.pdf');
28 + }
29 +
30 + public function test_page_html_export()
31 + {
32 + $page = Page::first();
33 + $this->asEditor();
34 +
35 + $resp = $this->get($page->getUrl('/export/html'));
36 + $resp->assertStatus(200);
37 + $resp->assertSee($page->name);
38 + $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.html');
39 + }
40 +
41 + public function test_book_text_export()
42 + {
43 + $page = Page::first();
44 + $book = $page->book;
45 + $this->asEditor();
46 +
47 + $resp = $this->get($book->getUrl('/export/plaintext'));
48 + $resp->assertStatus(200);
49 + $resp->assertSee($book->name);
50 + $resp->assertSee($page->name);
51 + $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.txt');
52 + }
53 +
54 + public function test_book_pdf_export()
55 + {
56 + $page = Page::first();
57 + $book = $page->book;
58 + $this->asEditor();
59 +
60 + $resp = $this->get($book->getUrl('/export/pdf'));
61 + $resp->assertStatus(200);
62 + $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.pdf');
63 + }
64 +
65 + public function test_book_html_export()
66 + {
67 + $page = Page::first();
68 + $book = $page->book;
69 + $this->asEditor();
70 +
71 + $resp = $this->get($book->getUrl('/export/html'));
72 + $resp->assertStatus(200);
73 + $resp->assertSee($book->name);
74 + $resp->assertSee($page->name);
75 + $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.html');
76 + }
77 +
78 +}
...\ No newline at end of file ...\ No newline at end of file