diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php index 408192ff9..fe9ece5b2 100644 --- a/app/Http/Controllers/BookController.php +++ b/app/Http/Controllers/BookController.php @@ -3,6 +3,7 @@ use Activity; use BookStack\Repos\EntityRepo; use BookStack\Repos\UserRepo; +use BookStack\Services\ExportService; use Illuminate\Http\Request; use Illuminate\Http\Response; use Views; @@ -12,16 +13,19 @@ class BookController extends Controller protected $entityRepo; protected $userRepo; + protected $exportService; /** * BookController constructor. * @param EntityRepo $entityRepo * @param UserRepo $userRepo + * @param ExportService $exportService */ - public function __construct(EntityRepo $entityRepo, UserRepo $userRepo) + public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService) { $this->entityRepo = $entityRepo; $this->userRepo = $userRepo; + $this->exportService = $exportService; parent::__construct(); } @@ -258,4 +262,49 @@ class BookController extends Controller session()->flash('success', trans('entities.books_permissions_updated')); return redirect($book->getUrl()); } + + /** + * Export a book as a PDF file. + * @param string $bookSlug + * @return mixed + */ + public function exportPdf($bookSlug) + { + $book = $this->entityRepo->getBySlug('book', $bookSlug); + $pdfContent = $this->exportService->bookToPdf($book); + return response()->make($pdfContent, 200, [ + 'Content-Type' => 'application/octet-stream', + 'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.pdf' + ]); + } + + /** + * Export a book as a contained HTML file. + * @param string $bookSlug + * @return mixed + */ + public function exportHtml($bookSlug) + { + $book = $this->entityRepo->getBySlug('book', $bookSlug); + $htmlContent = $this->exportService->bookToContainedHtml($book); + return response()->make($htmlContent, 200, [ + 'Content-Type' => 'application/octet-stream', + 'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.html' + ]); + } + + /** + * Export a book as a plain text file. + * @param $bookSlug + * @return mixed + */ + public function exportPlainText($bookSlug) + { + $book = $this->entityRepo->getBySlug('book', $bookSlug); + $htmlContent = $this->exportService->bookToPlainText($book); + return response()->make($htmlContent, 200, [ + 'Content-Type' => 'application/octet-stream', + 'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.txt' + ]); + } } diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 623cb9c4d..4a29c20d6 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -439,7 +439,6 @@ class PageController extends Controller { $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $pdfContent = $this->exportService->pageToPdf($page); -// return $pdfContent; return response()->make($pdfContent, 200, [ 'Content-Type' => 'application/octet-stream', 'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf' diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php index 7b262c3de..4db69137f 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -313,11 +313,12 @@ class EntityRepo * Loads the book slug onto child elements to prevent access database access for getting the slug. * @param Book $book * @param bool $filterDrafts + * @param bool $renderPages * @return mixed */ - public function getBookChildren(Book $book, $filterDrafts = false) + public function getBookChildren(Book $book, $filterDrafts = false, $renderPages = false) { - $q = $this->permissionService->bookChildrenQuery($book->id, $filterDrafts)->get(); + $q = $this->permissionService->bookChildrenQuery($book->id, $filterDrafts, $renderPages)->get(); $entities = []; $parents = []; $tree = []; @@ -325,6 +326,10 @@ class EntityRepo foreach ($q as $index => $rawEntity) { if ($rawEntity->entity_type === 'BookStack\\Page') { $entities[$index] = $this->page->newFromBuilder($rawEntity); + if ($renderPages) { + $entities[$index]->html = $rawEntity->description; + $entities[$index]->html = $this->renderPage($entities[$index]); + }; } else if ($rawEntity->entity_type === 'BookStack\\Chapter') { $entities[$index] = $this->chapter->newFromBuilder($rawEntity); $key = $entities[$index]->entity_type . ':' . $entities[$index]->id; diff --git a/app/Services/ExportService.php b/app/Services/ExportService.php index e51577a22..3ac698718 100644 --- a/app/Services/ExportService.php +++ b/app/Services/ExportService.php @@ -1,5 +1,6 @@ $page, 'pageContent' => $this->entityRepo->renderPage($page), 'css' => $cssContent])->render(); + $pageHtml = view('pages/export', [ + 'page' => $page, + 'pageContent' => $this->entityRepo->renderPage($page) + ])->render(); return $this->containHtml($pageHtml); } /** - * Convert a page to a pdf file. + * Convert a book to a self-contained HTML file. + * @param Book $book + * @return mixed|string + */ + public function bookToContainedHtml(Book $book) + { + $bookTree = $this->entityRepo->getBookChildren($book, true, true); + $html = view('books/export', [ + 'book' => $book, + 'bookChildren' => $bookTree + ])->render(); + return $this->containHtml($html); + } + + /** + * Convert a page to a PDF file. * @param Page $page * @return mixed|string */ public function pageToPdf(Page $page) { - $cssContent = file_get_contents(public_path('/css/export-styles.css')); - $pageHtml = view('pages/pdf', ['page' => $page, 'pageContent' => $this->entityRepo->renderPage($page), 'css' => $cssContent])->render(); + $html = view('pages/pdf', [ + 'page' => $page, + 'pageContent' => $this->entityRepo->renderPage($page) + ])->render(); + return $this->htmlToPdf($html); + } + + /** + * Convert a book to a PDF file + * @param Book $book + * @return string + */ + public function bookToPdf(Book $book) + { + $bookTree = $this->entityRepo->getBookChildren($book, true, true); + $html = view('books/export', [ + 'book' => $book, + 'bookChildren' => $bookTree + ])->render(); + return $this->htmlToPdf($html); + } + + /** + * Convert normal webpage HTML to a PDF. + * @param $html + * @return string + */ + protected function htmlToPdf($html) + { + $containedHtml = $this->containHtml($html); $useWKHTML = config('snappy.pdf.binary') !== false; - $containedHtml = $this->containHtml($pageHtml); if ($useWKHTML) { $pdf = \SnappyPDF::loadHTML($containedHtml); + $pdf->setOption('print-media-type', true); } else { $pdf = \PDF::loadHTML($containedHtml); } @@ -122,6 +168,29 @@ class ExportService return $text; } + /** + * Convert a book into a plain text string. + * @param Book $book + * @return string + */ + public function bookToPlainText(Book $book) + { + $bookTree = $this->entityRepo->getBookChildren($book, true, true); + $text = $book->name . "\n\n"; + foreach ($bookTree as $bookChild) { + if ($bookChild->isA('chapter')) { + $text .= $bookChild->name . "\n\n"; + $text .= $bookChild->description . "\n\n"; + foreach ($bookChild->pages as $page) { + $text .= $this->pageToPlainText($page); + } + } else { + $text .= $this->pageToPlainText($bookChild); + } + } + return $text; + } + } diff --git a/app/Services/PermissionService.php b/app/Services/PermissionService.php index 72a810b6b..8b47e1246 100644 --- a/app/Services/PermissionService.php +++ b/app/Services/PermissionService.php @@ -474,11 +474,13 @@ class PermissionService /** * Get the children of a book in an efficient single query, Filtered by the permission system. * @param integer $book_id - * @param bool $filterDrafts + * @param bool $filterDrafts + * @param bool $fetchPageContent * @return \Illuminate\Database\Query\Builder */ - public function bookChildrenQuery($book_id, $filterDrafts = false) { - $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) { + public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) { + $pageContentSelect = $fetchPageContent ? 'html' : "''"; + $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) { $query->where('draft', '=', 0); if (!$filterDrafts) { $query->orWhere(function($query) { diff --git a/config/dompdf.php b/config/dompdf.php index 1eb1d9782..036e1bb3c 100644 --- a/config/dompdf.php +++ b/config/dompdf.php @@ -143,7 +143,7 @@ return [ * the desired content might be different (e.g. screen or projection view of html file). * Therefore allow specification of content here. */ - "DOMPDF_DEFAULT_MEDIA_TYPE" => "screen", + "DOMPDF_DEFAULT_MEDIA_TYPE" => "print", /** * The default paper size. diff --git a/resources/views/books/export.blade.php b/resources/views/books/export.blade.php new file mode 100644 index 000000000..e5fbada44 --- /dev/null +++ b/resources/views/books/export.blade.php @@ -0,0 +1,78 @@ + + + + + {{ $book->name }} + + + @yield('head') + + +
+
+
+
+ +

{{$book->name}}

+ +

{{ $book->description }}

+ + @if(count($bookChildren) > 0) +
    + @foreach($bookChildren as $bookChild) +
  • {{ $bookChild->name }}
  • + @if($bookChild->isA('chapter') && count($bookChild->pages) > 0) + + @endif + @endforeach +
+ @endif + + @foreach($bookChildren as $bookChild) +
+

{{ $bookChild->name }}

+ @if($bookChild->isA('chapter')) +

{{ $bookChild->description }}

+ @if(count($bookChild->pages) > 0) + @foreach($bookChild->pages as $page) +
+
{{$bookChild->name}}
+

{{ $page->name }}

+ {!! $page->html !!} + @endforeach + @endif + @else + {!! $bookChild->html !!} + @endif + @endforeach + +
+
+
+
+ + diff --git a/resources/views/books/show.blade.php b/resources/views/books/show.blade.php index 6b4e7f88a..99ffe80e1 100644 --- a/resources/views/books/show.blade.php +++ b/resources/views/books/show.blade.php @@ -10,6 +10,14 @@
+ +
{{ trans('entities.pages_export') }}
+ +
@if(userCan('page-create', $book)) {{ trans('entities.pages_new') }} @endif diff --git a/resources/views/pages/export.blade.php b/resources/views/pages/export.blade.php index 19a635563..e0813e468 100644 --- a/resources/views/pages/export.blade.php +++ b/resources/views/pages/export.blade.php @@ -5,7 +5,7 @@ {{ $page->name }} @yield('head') diff --git a/routes/web.php b/routes/web.php index 8d166f1d6..670439a66 100644 --- a/routes/web.php +++ b/routes/web.php @@ -26,6 +26,9 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/{slug}/delete', 'BookController@showDelete'); Route::get('/{bookSlug}/sort', 'BookController@sort'); Route::put('/{bookSlug}/sort', 'BookController@saveSort'); + Route::get('/{bookSlug}/export/html', 'BookController@exportHtml'); + Route::get('/{bookSlug}/export/pdf', 'BookController@exportPdf'); + Route::get('/{bookSlug}/export/plaintext', 'BookController@exportPlainText'); // Pages Route::get('/{bookSlug}/page/create', 'PageController@create'); diff --git a/tests/Entity/ExportTest.php b/tests/Entity/ExportTest.php new file mode 100644 index 000000000..b1dab094f --- /dev/null +++ b/tests/Entity/ExportTest.php @@ -0,0 +1,78 @@ +asEditor(); + + $resp = $this->get($page->getUrl('/export/plaintext')); + $resp->assertStatus(200); + $resp->assertSee($page->name); + $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.txt'); + } + + public function test_page_pdf_export() + { + $page = Page::first(); + $this->asEditor(); + + $resp = $this->get($page->getUrl('/export/pdf')); + $resp->assertStatus(200); + $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.pdf'); + } + + public function test_page_html_export() + { + $page = Page::first(); + $this->asEditor(); + + $resp = $this->get($page->getUrl('/export/html')); + $resp->assertStatus(200); + $resp->assertSee($page->name); + $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.html'); + } + + public function test_book_text_export() + { + $page = Page::first(); + $book = $page->book; + $this->asEditor(); + + $resp = $this->get($book->getUrl('/export/plaintext')); + $resp->assertStatus(200); + $resp->assertSee($book->name); + $resp->assertSee($page->name); + $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.txt'); + } + + public function test_book_pdf_export() + { + $page = Page::first(); + $book = $page->book; + $this->asEditor(); + + $resp = $this->get($book->getUrl('/export/pdf')); + $resp->assertStatus(200); + $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.pdf'); + } + + public function test_book_html_export() + { + $page = Page::first(); + $book = $page->book; + $this->asEditor(); + + $resp = $this->get($book->getUrl('/export/html')); + $resp->assertStatus(200); + $resp->assertSee($book->name); + $resp->assertSee($page->name); + $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.html'); + } + +} \ No newline at end of file