From 7b8fe5fbc69a6f49c2acff938f1da48c28e19ecd Mon Sep 17 00:00:00 2001
From: Dan Brown <ssddanbrown@googlemail.com>
Date: Fri, 10 Apr 2020 16:05:17 +0100
Subject: [PATCH] Added book-export endpoints to the API

---
 app/Api/ApiDocsGenerator.php                  |  2 +
 .../Api/BooksExportApiController.php          | 55 +++++++++++++++++++
 resources/views/api-docs/index.blade.php      |  4 +-
 routes/api.php                                |  4 ++
 tests/Api/BooksApiTest.php                    | 32 +++++++++++
 5 files changed, 95 insertions(+), 2 deletions(-)
 create mode 100644 app/Http/Controllers/Api/BooksExportApiController.php

diff --git a/app/Api/ApiDocsGenerator.php b/app/Api/ApiDocsGenerator.php
index a0c45608a..ddba24bdb 100644
--- a/app/Api/ApiDocsGenerator.php
+++ b/app/Api/ApiDocsGenerator.php
@@ -3,6 +3,7 @@
 use BookStack\Http\Controllers\Api\ApiController;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Facades\Route;
+use Illuminate\Support\Str;
 use ReflectionClass;
 use ReflectionException;
 use ReflectionMethod;
@@ -117,6 +118,7 @@ class ApiDocsGenerator
                 'method' => $route->methods[0],
                 'controller' => $controller,
                 'controller_method' => $controllerMethod,
+                'controller_method_kebab' => Str::kebab($controllerMethod),
                 'base_model' => $baseModelName,
             ];
         });
diff --git a/app/Http/Controllers/Api/BooksExportApiController.php b/app/Http/Controllers/Api/BooksExportApiController.php
new file mode 100644
index 000000000..605f8f408
--- /dev/null
+++ b/app/Http/Controllers/Api/BooksExportApiController.php
@@ -0,0 +1,55 @@
+<?php namespace BookStack\Http\Controllers\Api;
+
+use BookStack\Entities\Book;
+use BookStack\Entities\ExportService;
+use BookStack\Entities\Repos\BookRepo;
+use Throwable;
+
+class BooksExportApiController extends ApiController
+{
+
+    protected $bookRepo;
+    protected $exportService;
+
+    /**
+     * BookExportController constructor.
+     */
+    public function __construct(BookRepo $bookRepo, ExportService $exportService)
+    {
+        $this->bookRepo = $bookRepo;
+        $this->exportService = $exportService;
+        parent::__construct();
+    }
+
+    /**
+     * Export a book as a PDF file.
+     * @throws Throwable
+     */
+    public function exportPdf(int $id)
+    {
+        $book = Book::visible()->findOrFail($id);
+        $pdfContent = $this->exportService->bookToPdf($book);
+        return $this->downloadResponse($pdfContent, $book->slug . '.pdf');
+    }
+
+    /**
+     * Export a book as a contained HTML file.
+     * @throws Throwable
+     */
+    public function exportHtml(int $id)
+    {
+        $book = Book::visible()->findOrFail($id);
+        $htmlContent = $this->exportService->bookToContainedHtml($book);
+        return $this->downloadResponse($htmlContent, $book->slug . '.html');
+    }
+
+    /**
+     * Export a book as a plain text file.
+     */
+    public function exportPlainText(int $id)
+    {
+        $book = Book::visible()->findOrFail($id);
+        $textContent = $this->exportService->bookToPlainText($book);
+        return $this->downloadResponse($textContent, $book->slug . '.txt');
+    }
+}
diff --git a/resources/views/api-docs/index.blade.php b/resources/views/api-docs/index.blade.php
index ab4db89e8..e92b505cf 100644
--- a/resources/views/api-docs/index.blade.php
+++ b/resources/views/api-docs/index.blade.php
@@ -26,7 +26,7 @@
                                     <span class="api-method" data-method="{{ $endpoint['method'] }}">{{ $endpoint['method'] }}</span>
                                 </a>
                                 <a href="#{{ $endpoint['name'] }}" class="text-mono">
-                                    {{ $endpoint['controller_method'] }}
+                                    {{ $endpoint['controller_method_kebab'] }}
                                 </a>
                             </div>
                         @endforeach
@@ -186,7 +186,7 @@
                         <h1 class="list-heading text-capitals">{{ $model }}</h1>
 
                         @foreach($endpoints as $endpoint)
-                            <h6 class="text-uppercase text-muted float right">{{ $endpoint['controller_method'] }}</h6>
+                            <h6 class="text-uppercase text-muted float right">{{ $endpoint['controller_method_kebab'] }}</h6>
                             <h5 id="{{ $endpoint['name'] }}" class="text-mono mb-m">
                                 <span class="api-method" data-method="{{ $endpoint['method'] }}">{{ $endpoint['method'] }}</span>
                                 {{ url($endpoint['uri']) }}
diff --git a/routes/api.php b/routes/api.php
index 7ca5e66fc..f9c27b62f 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -15,6 +15,10 @@ Route::get('books/{id}', 'BooksApiController@read');
 Route::put('books/{id}', 'BooksApiController@update');
 Route::delete('books/{id}', 'BooksApiController@delete');
 
+Route::get('books/{id}/export/html', 'BooksExportApiController@exportHtml');
+Route::get('books/{id}/export/pdf', 'BooksExportApiController@exportPdf');
+Route::get('books/{id}/export/plaintext', 'BooksExportApiController@exportPlainText');
+
 Route::get('shelves', 'BookshelfApiController@list');
 Route::post('shelves', 'BookshelfApiController@create');
 Route::get('shelves/{id}', 'BookshelfApiController@read');
diff --git a/tests/Api/BooksApiTest.php b/tests/Api/BooksApiTest.php
index 6f8753fb0..3fd763ec6 100644
--- a/tests/Api/BooksApiTest.php
+++ b/tests/Api/BooksApiTest.php
@@ -105,4 +105,36 @@ class BooksApiTest extends TestCase
         $resp->assertStatus(204);
         $this->assertActivityExists('book_delete');
     }
+
+    public function test_export_html_endpoint()
+    {
+        $this->actingAsApiEditor();
+        $book = Book::visible()->first();
+
+        $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/html");
+        $resp->assertStatus(200);
+        $resp->assertSee($book->name);
+        $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.html"');
+    }
+
+    public function test_export_plain_text_endpoint()
+    {
+        $this->actingAsApiEditor();
+        $book = Book::visible()->first();
+
+        $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/plaintext");
+        $resp->assertStatus(200);
+        $resp->assertSee($book->name);
+        $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.txt"');
+    }
+
+    public function test_export_pdf_endpoint()
+    {
+        $this->actingAsApiEditor();
+        $book = Book::visible()->first();
+
+        $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/pdf");
+        $resp->assertStatus(200);
+        $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.pdf"');
+    }
 }
\ No newline at end of file