From a34a07c610390746e1160c6b57b0b190a3c772d5 Mon Sep 17 00:00:00 2001
From: Nikhil Jha <hi@nikhiljha.com>
Date: Tue, 12 May 2020 21:12:26 -0700
Subject: [PATCH 1/4] basic markdown export

---
 app/Entities/ExportService.php                | 45 +++++++++++++++++++
 app/Http/Controllers/BookExportController.php | 11 +++++
 .../Controllers/ChapterExportController.php   | 12 +++++
 app/Http/Controllers/PageExportController.php | 11 +++++
 resources/lang/en/entities.php                |  1 +
 .../partials/entity-export-menu.blade.php     |  1 +
 routes/web.php                                |  3 ++
 7 files changed, 84 insertions(+)

diff --git a/app/Entities/ExportService.php b/app/Entities/ExportService.php
index f945dfbe4..29df1e82d 100644
--- a/app/Entities/ExportService.php
+++ b/app/Entities/ExportService.php
@@ -225,4 +225,49 @@ class ExportService
         }
         return $text;
     }
+
+    /**
+     * Convert a page to a Markdown file.
+     * @throws Throwable
+     */
+    public function pageToMarkdown(Page $page)
+    {
+        if (property_exists($page, 'markdown') || $page->markdown != '') {
+            return "#" . $page->name . "\n\n" . $page->markdown;
+        } else {
+            // TODO: Implement this feature.
+            return "# Unimplemented Feature\nidk how to turn html into markdown";
+        }
+    }
+
+    /**
+     * Convert a chapter to a Markdown file.
+     * @throws Throwable
+     */
+    public function chapterToMarkdown(Chapter $chapter)
+    {
+        $text = "#" . $chapter->name . "\n\n";
+        $text .= $chapter->description . "\n\n";
+        foreach ($chapter->pages as $page) {
+            $text .= $this->pageToMarkdown($page);
+        }
+        return $text;
+    }
+
+    /**
+     * Convert a book into a plain text string.
+     */
+    public function bookToMarkdown(Book $book): string
+    {
+        $bookTree = (new BookContents($book))->getTree(false, true);
+        $text = "#" . $book->name . "\n\n";
+        foreach ($bookTree as $bookChild) {
+            if ($bookChild->isA('chapter')) {
+                $text .= $this->chapterToMarkdown($bookChild);
+            } else {
+                $text .= $this->pageToMarkdown($bookChild);
+            }
+        }
+        return $text;
+    }
 }
diff --git a/app/Http/Controllers/BookExportController.php b/app/Http/Controllers/BookExportController.php
index cfa3d6a3a..40eec69fe 100644
--- a/app/Http/Controllers/BookExportController.php
+++ b/app/Http/Controllers/BookExportController.php
@@ -53,4 +53,15 @@ class BookExportController extends Controller
         $textContent = $this->exportService->bookToPlainText($book);
         return $this->downloadResponse($textContent, $bookSlug . '.txt');
     }
+
+    /**
+     * Export a book as a markdown file.
+     */
+    public function markdown(string $bookSlug)
+    {
+        // TODO: This should probably export to a zip file.
+        $book = $this->bookRepo->getBySlug($bookSlug);
+        $textContent = $this->exportService->bookToMarkdown($book);
+        return $this->downloadResponse($textContent, $bookSlug . '.md');
+    }
 }
diff --git a/app/Http/Controllers/ChapterExportController.php b/app/Http/Controllers/ChapterExportController.php
index 0c86f8548..c0fa9fad9 100644
--- a/app/Http/Controllers/ChapterExportController.php
+++ b/app/Http/Controllers/ChapterExportController.php
@@ -55,4 +55,16 @@ class ChapterExportController extends Controller
         $chapterText = $this->exportService->chapterToPlainText($chapter);
         return $this->downloadResponse($chapterText, $chapterSlug . '.txt');
     }
+
+    /**
+     * Export a chapter to a simple markdown file.
+     * @throws NotFoundException
+     */
+    public function markdown(string $bookSlug, string $chapterSlug)
+    {
+        // TODO: This should probably export to a zip file.
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapterText = $this->exportService->chapterToMarkdown($chapter);
+        return $this->downloadResponse($chapterText, $chapterSlug . '.md');
+    }
 }
diff --git a/app/Http/Controllers/PageExportController.php b/app/Http/Controllers/PageExportController.php
index 3b02ea224..037f84e3b 100644
--- a/app/Http/Controllers/PageExportController.php
+++ b/app/Http/Controllers/PageExportController.php
@@ -63,4 +63,15 @@ class PageExportController extends Controller
         $pageText = $this->exportService->pageToPlainText($page);
         return $this->downloadResponse($pageText, $pageSlug . '.txt');
     }
+
+    /**
+     * Export a page to a simple markdown .md file.
+     * @throws NotFoundException
+     */
+    public function markdown(string $bookSlug, string $pageSlug)
+    {
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $pageText = $this->exportService->pageToMarkdown($page);
+        return $this->downloadResponse($pageText, $pageSlug . '.md');
+    }
 }
diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php
index 6bbc723b0..b459c3d4b 100644
--- a/resources/lang/en/entities.php
+++ b/resources/lang/en/entities.php
@@ -33,6 +33,7 @@ return [
     'export_html' => 'Contained Web File',
     'export_pdf' => 'PDF File',
     'export_text' => 'Plain Text File',
+    'export_md' => 'Markdown File',
 
     // Permissions and restrictions
     'permissions' => 'Permissions',
diff --git a/resources/views/partials/entity-export-menu.blade.php b/resources/views/partials/entity-export-menu.blade.php
index 630d640bf..42c2eb79a 100644
--- a/resources/views/partials/entity-export-menu.blade.php
+++ b/resources/views/partials/entity-export-menu.blade.php
@@ -8,5 +8,6 @@
         <li><a href="{{ $entity->getUrl('/export/html') }}" target="_blank">{{ trans('entities.export_html') }} <span class="text-muted float right">.html</span></a></li>
         <li><a href="{{ $entity->getUrl('/export/pdf') }}" target="_blank">{{ trans('entities.export_pdf') }} <span class="text-muted float right">.pdf</span></a></li>
         <li><a href="{{ $entity->getUrl('/export/plaintext') }}" target="_blank">{{ trans('entities.export_text') }} <span class="text-muted float right">.txt</span></a></li>
+        <li><a href="{{ $entity->getUrl('/export/markdown') }}" target="_blank">{{ trans('entities.export_md') }} <span class="text-muted float right">.md</span></a></li>
     </ul>
 </div>
\ No newline at end of file
diff --git a/routes/web.php b/routes/web.php
index 3e05e394d..f2c4f432e 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -47,6 +47,7 @@ Route::group(['middleware' => 'auth'], function () {
         Route::put('/{bookSlug}/sort', 'BookSortController@update');
         Route::get('/{bookSlug}/export/html', 'BookExportController@html');
         Route::get('/{bookSlug}/export/pdf', 'BookExportController@pdf');
+        Route::get('/{bookSlug}/export/markdown', 'BookExportController@markdown');
         Route::get('/{bookSlug}/export/plaintext', 'BookExportController@plainText');
 
         // Pages
@@ -57,6 +58,7 @@ Route::group(['middleware' => 'auth'], function () {
         Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show');
         Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageExportController@pdf');
         Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageExportController@html');
+        Route::get('/{bookSlug}/page/{pageSlug}/export/markdown', 'PageExportController@markdown');
         Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageExportController@plainText');
         Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit');
         Route::get('/{bookSlug}/page/{pageSlug}/move', 'PageController@showMove');
@@ -91,6 +93,7 @@ Route::group(['middleware' => 'auth'], function () {
         Route::get('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@showPermissions');
         Route::get('/{bookSlug}/chapter/{chapterSlug}/export/pdf', 'ChapterExportController@pdf');
         Route::get('/{bookSlug}/chapter/{chapterSlug}/export/html', 'ChapterExportController@html');
+        Route::get('/{bookSlug}/chapter/{chapterSlug}/export/markdown', 'ChapterExportController@markdown');
         Route::get('/{bookSlug}/chapter/{chapterSlug}/export/plaintext', 'ChapterExportController@plainText');
         Route::put('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@permissions');
         Route::get('/{bookSlug}/chapter/{chapterSlug}/delete', 'ChapterController@showDelete');

From a7d9646b19cd4549849e56bec1afedb1c5987f3d Mon Sep 17 00:00:00 2001
From: Nikhil Jha <hi@nikhiljha.com>
Date: Wed, 13 May 2020 18:34:22 -0700
Subject: [PATCH 2/4] support exporting WYSIWYG pages as Markdown

---
 app/Entities/ExportService.php | 13 +++----
 composer.json                  |  1 +
 composer.lock                  | 66 +++++++++++++++++++++++++++++++++-
 3 files changed, 73 insertions(+), 7 deletions(-)

diff --git a/app/Entities/ExportService.php b/app/Entities/ExportService.php
index 29df1e82d..1b294d8b1 100644
--- a/app/Entities/ExportService.php
+++ b/app/Entities/ExportService.php
@@ -6,6 +6,7 @@ use BookStack\Uploads\ImageService;
 use DomPDF;
 use Exception;
 use SnappyPDF;
+use League\HTMLToMarkdown\HtmlConverter;
 use Throwable;
 
 class ExportService
@@ -232,11 +233,11 @@ class ExportService
      */
     public function pageToMarkdown(Page $page)
     {
-        if (property_exists($page, 'markdown') || $page->markdown != '') {
-            return "#" . $page->name . "\n\n" . $page->markdown;
+        if (property_exists($page, 'markdown') && $page->markdown != '') {
+            return "# " . $page->name . "\n\n" . $page->markdown;
         } else {
-            // TODO: Implement this feature.
-            return "# Unimplemented Feature\nidk how to turn html into markdown";
+            $converter = new HtmlConverter();
+            return "# " . $page->name . "\n\n" . $converter->convert($page->html);
         }
     }
 
@@ -246,7 +247,7 @@ class ExportService
      */
     public function chapterToMarkdown(Chapter $chapter)
     {
-        $text = "#" . $chapter->name . "\n\n";
+        $text = "# " . $chapter->name . "\n\n";
         $text .= $chapter->description . "\n\n";
         foreach ($chapter->pages as $page) {
             $text .= $this->pageToMarkdown($page);
@@ -260,7 +261,7 @@ class ExportService
     public function bookToMarkdown(Book $book): string
     {
         $bookTree = (new BookContents($book))->getTree(false, true);
-        $text = "#" . $book->name . "\n\n";
+        $text = "# " . $book->name . "\n\n";
         foreach ($bookTree as $bookChild) {
             if ($bookChild->isA('chapter')) {
                 $text .= $this->chapterToMarkdown($bookChild);
diff --git a/composer.json b/composer.json
index 80e8b0a61..68802e935 100644
--- a/composer.json
+++ b/composer.json
@@ -24,6 +24,7 @@
         "laravel/socialite": "^4.3.2",
         "league/commonmark": "^1.4",
         "league/flysystem-aws-s3-v3": "^1.0",
+        "league/html-to-markdown": "^4.9",
         "nunomaduro/collision": "^3.0",
         "onelogin/php-saml": "^3.3",
         "predis/predis": "^1.1",
diff --git a/composer.lock b/composer.lock
index 3ddd28e5a..7f58ffaee 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "bbe47cff4f167fd6ce7047dff4602a78",
+    "content-hash": "280a7e2fe2a6f65812594d73df2ccc0f",
     "packages": [
         {
             "name": "aws/aws-sdk-php",
@@ -2004,6 +2004,70 @@
             "description": "Flysystem adapter for the AWS S3 SDK v3.x",
             "time": "2020-02-23T13:31:58+00:00"
         },
+        {
+            "name": "league/html-to-markdown",
+            "version": "4.9.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thephpleague/html-to-markdown.git",
+                "reference": "1dcd0f85de786f46a7f224a27cc3d709ddd2a68c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/1dcd0f85de786f46a7f224a27cc3d709ddd2a68c",
+                "reference": "1dcd0f85de786f46a7f224a27cc3d709ddd2a68c",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-xml": "*",
+                "php": ">=5.3.3"
+            },
+            "require-dev": {
+                "mikehaertl/php-shellcommand": "~1.1.0",
+                "phpunit/phpunit": "^4.8|^5.7",
+                "scrutinizer/ocular": "~1.1"
+            },
+            "bin": [
+                "bin/html-to-markdown"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.10-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "League\\HTMLToMarkdown\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Colin O'Dell",
+                    "email": "colinodell@gmail.com",
+                    "homepage": "https://www.colinodell.com",
+                    "role": "Lead Developer"
+                },
+                {
+                    "name": "Nick Cernis",
+                    "email": "nick@cern.is",
+                    "homepage": "http://modernnerd.net",
+                    "role": "Original Author"
+                }
+            ],
+            "description": "An HTML-to-markdown conversion helper for PHP",
+            "homepage": "https://github.com/thephpleague/html-to-markdown",
+            "keywords": [
+                "html",
+                "markdown"
+            ],
+            "time": "2019-12-28T01:32:28+00:00"
+        },
         {
             "name": "league/oauth1-client",
             "version": "1.7.0",

From ea82c2f61b00231cdbcffd0463361c5b41832062 Mon Sep 17 00:00:00 2001
From: Nikhil Jha <hi@nikhiljha.com>
Date: Wed, 13 May 2020 19:57:59 -0700
Subject: [PATCH 3/4] support exporting books as zip files

---
 app/Http/Controllers/BookExportController.php | 26 ++++++++++++++++++-
 dev/docker/Dockerfile                         |  4 +--
 routes/web.php                                |  1 +
 3 files changed, 28 insertions(+), 3 deletions(-)

diff --git a/app/Http/Controllers/BookExportController.php b/app/Http/Controllers/BookExportController.php
index 40eec69fe..0414b7250 100644
--- a/app/Http/Controllers/BookExportController.php
+++ b/app/Http/Controllers/BookExportController.php
@@ -2,9 +2,11 @@
 
 namespace BookStack\Http\Controllers;
 
+use BookStack\Entities\Managers\BookContents;
 use BookStack\Entities\ExportService;
 use BookStack\Entities\Repos\BookRepo;
 use Throwable;
+use ZipArchive;
 
 class BookExportController extends Controller
 {
@@ -59,9 +61,31 @@ class BookExportController extends Controller
      */
     public function markdown(string $bookSlug)
     {
-        // TODO: This should probably export to a zip file.
         $book = $this->bookRepo->getBySlug($bookSlug);
         $textContent = $this->exportService->bookToMarkdown($book);
         return $this->downloadResponse($textContent, $bookSlug . '.md');
     }
+
+    /**
+     * Export a book as a zip file, made of markdown files.
+     */
+    public function zip(string $bookSlug)
+    {
+        $book = $this->bookRepo->getBySlug($bookSlug);
+        $z = new ZipArchive();
+        $z->open("book.zip", \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
+        $bookTree = (new BookContents($book))->getTree(false, true);
+        foreach ($bookTree as $bookChild) {
+            if ($bookChild->isA('chapter')) {
+                $z->addEmptyDir($bookChild->name);
+                foreach ($bookChild->pages as $page) {
+                    $z->addFromString($bookChild->name . "/" . $page->name . ".md", $this->exportService->pageToMarkdown($page));
+                }
+            } else {
+                $z->addFromString($bookChild->name . ".md", $this->exportService->pageToMarkdown($bookChild));
+            }
+        }
+        return response()->download('book.zip');
+        // TODO: Is not unlinking it a security issue?
+    }
 }
diff --git a/dev/docker/Dockerfile b/dev/docker/Dockerfile
index 8816615cf..be5af9ed9 100644
--- a/dev/docker/Dockerfile
+++ b/dev/docker/Dockerfile
@@ -4,9 +4,9 @@ ENV APACHE_DOCUMENT_ROOT /app/public
 WORKDIR /app
 
 RUN apt-get update -y \
-    && apt-get install -y git zip unzip libtidy-dev libpng-dev libldap2-dev libxml++2.6-dev wait-for-it \
+    && apt-get install -y git zip unzip libtidy-dev libpng-dev libldap2-dev libxml++2.6-dev wait-for-it libzip-dev \
     && docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu \
-    && docker-php-ext-install pdo pdo_mysql tidy dom xml mbstring gd ldap \
+    && docker-php-ext-install pdo pdo_mysql tidy dom xml mbstring gd ldap zip \
     && a2enmod rewrite \
     && sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
     && sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf \
diff --git a/routes/web.php b/routes/web.php
index f2c4f432e..4d00b5ff6 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -48,6 +48,7 @@ Route::group(['middleware' => 'auth'], function () {
         Route::get('/{bookSlug}/export/html', 'BookExportController@html');
         Route::get('/{bookSlug}/export/pdf', 'BookExportController@pdf');
         Route::get('/{bookSlug}/export/markdown', 'BookExportController@markdown');
+        Route::get('/{bookSlug}/export/zip', 'BookExportController@zip');
         Route::get('/{bookSlug}/export/plaintext', 'BookExportController@plainText');
 
         // Pages

From e287d965f5ed6d72bb5e83fa655207f96e3257df Mon Sep 17 00:00:00 2001
From: Nikhil Jha <hi@nikhiljha.com>
Date: Wed, 13 May 2020 20:07:19 -0700
Subject: [PATCH 4/4] move zip export into exportservice

---
 app/Entities/ExportService.php                | 24 +++++++++++++++++++
 app/Http/Controllers/BookExportController.php | 18 ++------------
 2 files changed, 26 insertions(+), 16 deletions(-)

diff --git a/app/Entities/ExportService.php b/app/Entities/ExportService.php
index 1b294d8b1..b0e88b18b 100644
--- a/app/Entities/ExportService.php
+++ b/app/Entities/ExportService.php
@@ -8,6 +8,7 @@ use Exception;
 use SnappyPDF;
 use League\HTMLToMarkdown\HtmlConverter;
 use Throwable;
+use ZipArchive;
 
 class ExportService
 {
@@ -271,4 +272,27 @@ class ExportService
         }
         return $text;
     }
+
+    /**
+     * Convert a book into a zip file.
+     */
+    public function bookToZip(Book $book): string
+    {
+        // TODO: Is not unlinking the file a security risk?
+        $z = new ZipArchive();
+        $z->open("book.zip", \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
+        $bookTree = (new BookContents($book))->getTree(false, true);
+        foreach ($bookTree as $bookChild) {
+            if ($bookChild->isA('chapter')) {
+                $z->addEmptyDir($bookChild->name);
+                foreach ($bookChild->pages as $page) {
+                    $filename = $bookChild->name . "/" . $page->name . ".md";
+                    $z->addFromString($filename, $this->pageToMarkdown($page));
+                }
+            } else {
+                $z->addFromString($bookChild->name . ".md", $this->pageToMarkdown($bookChild));
+            }
+        }
+        return "book.zip";
+    }
 }
diff --git a/app/Http/Controllers/BookExportController.php b/app/Http/Controllers/BookExportController.php
index 0414b7250..a92d94cc9 100644
--- a/app/Http/Controllers/BookExportController.php
+++ b/app/Http/Controllers/BookExportController.php
@@ -6,7 +6,6 @@ use BookStack\Entities\Managers\BookContents;
 use BookStack\Entities\ExportService;
 use BookStack\Entities\Repos\BookRepo;
 use Throwable;
-use ZipArchive;
 
 class BookExportController extends Controller
 {
@@ -72,20 +71,7 @@ class BookExportController extends Controller
     public function zip(string $bookSlug)
     {
         $book = $this->bookRepo->getBySlug($bookSlug);
-        $z = new ZipArchive();
-        $z->open("book.zip", \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
-        $bookTree = (new BookContents($book))->getTree(false, true);
-        foreach ($bookTree as $bookChild) {
-            if ($bookChild->isA('chapter')) {
-                $z->addEmptyDir($bookChild->name);
-                foreach ($bookChild->pages as $page) {
-                    $z->addFromString($bookChild->name . "/" . $page->name . ".md", $this->exportService->pageToMarkdown($page));
-                }
-            } else {
-                $z->addFromString($bookChild->name . ".md", $this->exportService->pageToMarkdown($bookChild));
-            }
-        }
-        return response()->download('book.zip');
-        // TODO: Is not unlinking it a security issue?
+        $filename = $this->exportService->bookToZip($book);
+        return response()->download($filename);
     }
 }