diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 499796314..970d0dc03 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -3,6 +3,7 @@ namespace BookStack\Http\Controllers; use Activity; +use BookStack\Services\ExportService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -18,18 +19,21 @@ class PageController extends Controller protected $pageRepo; protected $bookRepo; protected $chapterRepo; + protected $exportService; /** * PageController constructor. - * @param PageRepo $pageRepo - * @param BookRepo $bookRepo - * @param ChapterRepo $chapterRepo + * @param PageRepo $pageRepo + * @param BookRepo $bookRepo + * @param ChapterRepo $chapterRepo + * @param ExportService $exportService */ - public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo) + public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService) { $this->pageRepo = $pageRepo; $this->bookRepo = $bookRepo; $this->chapterRepo = $chapterRepo; + $this->exportService = $exportService; parent::__construct(); } @@ -221,4 +225,30 @@ class PageController extends Controller Activity::add($page, 'page_restore', $book->id); return redirect($page->getUrl()); } + + public function exportPdf($bookSlug, $pageSlug) + { + $book = $this->bookRepo->getBySlug($bookSlug); + $page = $this->pageRepo->getBySlug($pageSlug, $book->id); + $cssContent = file_get_contents(public_path('/css/styles.css')); + + return $pdf->download($pageSlug . '.pdf'); + } + + /** + * Export a page to a self-contained HTML file. + * @param $bookSlug + * @param $pageSlug + * @return \Illuminate\Http\Response + */ + public function exportHtml($bookSlug, $pageSlug) + { + $book = $this->bookRepo->getBySlug($bookSlug); + $page = $this->pageRepo->getBySlug($pageSlug, $book->id); + $containedHtml = $this->exportService->pageToContainedHtml($page); + return response()->make($containedHtml, 200, [ + 'Content-Type' => 'application/octet-stream', + 'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.html' + ]); + } } diff --git a/app/Http/routes.php b/app/Http/routes.php index 23d4c33ab..7c6a256a5 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -18,17 +18,18 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/{bookSlug}/sort', 'BookController@sort'); Route::put('/{bookSlug}/sort', 'BookController@saveSort'); - // Pages Route::get('/{bookSlug}/page/create', 'PageController@create'); Route::post('/{bookSlug}/page', 'PageController@store'); Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show'); + Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageController@exportPdf'); + Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml'); Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit'); Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete'); Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update'); Route::delete('/{bookSlug}/page/{pageSlug}', 'PageController@destroy'); - //Revisions + // Revisions Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageController@showRevisions'); Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}', 'PageController@showRevision'); Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', 'PageController@restoreRevision'); diff --git a/app/Services/ExportService.php b/app/Services/ExportService.php new file mode 100644 index 000000000..05ba85dd1 --- /dev/null +++ b/app/Services/ExportService.php @@ -0,0 +1,61 @@ + $page, 'css' => $cssContent])->render(); + + $imageTagsOutput = []; + preg_match_all("/\/i", $pageHtml, $imageTagsOutput); + + // Replace image src with base64 encoded image strings + if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) { + foreach ($imageTagsOutput[0] as $index => $imgMatch) { + $oldImgString = $imgMatch; + $srcString = $imageTagsOutput[2][$index]; + if (strpos(trim($srcString), 'http') !== 0) { + $pathString = public_path($srcString); + } else { + $pathString = $srcString; + } + $imageContent = file_get_contents($pathString); + $imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent); + $newImageString = str_replace($srcString, $imageEncoded, $oldImgString); + $pageHtml = str_replace($oldImgString, $newImageString, $pageHtml); + } + } + + $linksOutput = []; + preg_match_all("/\/i", $pageHtml, $linksOutput); + + // Replace image src with base64 encoded image strings + if (isset($linksOutput[0]) && count($linksOutput[0]) > 0) { + foreach ($linksOutput[0] as $index => $linkMatch) { + $oldLinkString = $linkMatch; + $srcString = $linksOutput[2][$index]; + if (strpos(trim($srcString), 'http') !== 0) { + $newSrcString = url($srcString); + $newLinkString = str_replace($srcString, $newSrcString, $oldLinkString); + $pageHtml = str_replace($oldLinkString, $newLinkString, $pageHtml); + } + } + } + + // Replace any relative links with system domain + return $pageHtml; + } + +} \ No newline at end of file diff --git a/composer.lock b/composer.lock index 364e7092e..d64c48e29 100644 --- a/composer.lock +++ b/composer.lock @@ -9,16 +9,16 @@ "packages": [ { "name": "aws/aws-sdk-php", - "version": "3.12.1", + "version": "3.13.1", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "5ee0f33fafe47740c03ff38ddb73ae4f52b4da5b" + "reference": "cc1796d1c21146cdcbfb7628aee816acb7b85e09" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5ee0f33fafe47740c03ff38ddb73ae4f52b4da5b", - "reference": "5ee0f33fafe47740c03ff38ddb73ae4f52b4da5b", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/cc1796d1c21146cdcbfb7628aee816acb7b85e09", + "reference": "cc1796d1c21146cdcbfb7628aee816acb7b85e09", "shasum": "" }, "require": { @@ -40,7 +40,7 @@ "ext-simplexml": "*", "ext-spl": "*", "nette/neon": "^2.3", - "phpunit/phpunit": "~4.0" + "phpunit/phpunit": "~4.0|~5.0" }, "suggest": { "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", @@ -84,7 +84,7 @@ "s3", "sdk" ], - "time": "2016-01-06 22:50:48" + "time": "2016-01-19 22:46:22" }, { "name": "barryvdh/laravel-debugbar", @@ -829,16 +829,16 @@ }, { "name": "laravel/framework", - "version": "v5.2.7", + "version": "v5.2.10", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "26cd65eaa4bcc0fb0be381cfb7cfdcda06a3c2b4" + "reference": "93dc5b0089eef468157fd7200e575c3861ec59a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/26cd65eaa4bcc0fb0be381cfb7cfdcda06a3c2b4", - "reference": "26cd65eaa4bcc0fb0be381cfb7cfdcda06a3c2b4", + "url": "https://api.github.com/repos/laravel/framework/zipball/93dc5b0089eef468157fd7200e575c3861ec59a5", + "reference": "93dc5b0089eef468157fd7200e575c3861ec59a5", "shasum": "" }, "require": { @@ -953,7 +953,7 @@ "framework", "laravel" ], - "time": "2016-01-07 13:54:34" + "time": "2016-01-13 20:29:10" }, { "name": "laravel/socialite", @@ -1849,16 +1849,16 @@ }, { "name": "symfony/class-loader", - "version": "v2.8.1", + "version": "v2.8.2", "source": { "type": "git", "url": "https://github.com/symfony/class-loader.git", - "reference": "ec74b0a279cf3a9bd36172b3e3061591d380ce6c" + "reference": "98e9089a428ed0e39423b67352c57ef5910a3269" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/class-loader/zipball/ec74b0a279cf3a9bd36172b3e3061591d380ce6c", - "reference": "ec74b0a279cf3a9bd36172b3e3061591d380ce6c", + "url": "https://api.github.com/repos/symfony/class-loader/zipball/98e9089a428ed0e39423b67352c57ef5910a3269", + "reference": "98e9089a428ed0e39423b67352c57ef5910a3269", "shasum": "" }, "require": { @@ -1897,7 +1897,7 @@ ], "description": "Symfony ClassLoader Component", "homepage": "https://symfony.com", - "time": "2015-12-05 17:37:59" + "time": "2016-01-03 15:33:41" }, { "name": "symfony/console", diff --git a/gulpfile.js b/gulpfile.js index e60dffc70..7deefc71a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -21,6 +21,7 @@ elixir.extend('queryVersion', function(inputFiles) { elixir(function(mix) { mix.sass('styles.scss') .sass('print-styles.scss') + .sass('export-styles.scss') .browserify('global.js', 'public/js/common.js') .queryVersion(['css/styles.css', 'css/print-styles.css', 'js/common.js']); }); diff --git a/resources/assets/sass/_fonts.scss b/resources/assets/sass/_fonts.scss new file mode 100644 index 000000000..0dc8c95b2 --- /dev/null +++ b/resources/assets/sass/_fonts.scss @@ -0,0 +1,96 @@ +/* Generated by Font Squirrel (http://www.fontsquirrel.com) on December 30, 2015 */ +@font-face { + font-family: 'Roboto'; + src: url('/fonts/roboto-bold-webfont.eot'); + src: url('/fonts/roboto-bold-webfont.eot?#iefix') format('embedded-opentype'), + url('/fonts/roboto-bold-webfont.woff2') format('woff2'), + url('/fonts/roboto-bold-webfont.woff') format('woff'), + url('/fonts/roboto-bold-webfont.ttf') format('truetype'), + url('/fonts/roboto-bold-webfont.svg#robotobold') format('svg'); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: 'Roboto'; + src: url('/fonts/roboto-bolditalic-webfont.eot'); + src: url('/fonts/roboto-bolditalic-webfont.eot?#iefix') format('embedded-opentype'), + url('/fonts/roboto-bolditalic-webfont.woff2') format('woff2'), + url('/fonts/roboto-bolditalic-webfont.woff') format('woff'), + url('/fonts/roboto-bolditalic-webfont.ttf') format('truetype'), + url('/fonts/roboto-bolditalic-webfont.svg#robotobold_italic') format('svg'); + font-weight: bold; + font-style: italic; +} + +@font-face { + font-family: 'Roboto'; + src: url('/fonts/roboto-italic-webfont.eot'); + src: url('/fonts/roboto-italic-webfont.eot?#iefix') format('embedded-opentype'), + url('/fonts/roboto-italic-webfont.woff2') format('woff2'), + url('/fonts/roboto-italic-webfont.woff') format('woff'), + url('/fonts/roboto-italic-webfont.ttf') format('truetype'), + url('/fonts/roboto-italic-webfont.svg#robotoitalic') format('svg'); + font-weight: normal; + font-style: italic; +} + +@font-face { + font-family: 'Roboto'; + src: url('/fonts/roboto-light-webfont.eot'); + src: url('/fonts/roboto-light-webfont.eot?#iefix') format('embedded-opentype'), + url('/fonts/roboto-light-webfont.woff2') format('woff2'), + url('/fonts/roboto-light-webfont.woff') format('woff'), + url('/fonts/roboto-light-webfont.ttf') format('truetype'), + url('/fonts/roboto-light-webfont.svg#robotolight') format('svg'); + font-weight: 300; + font-style: normal; +} + +@font-face { + font-family: 'Roboto'; + src: url('/fonts/roboto-lightitalic-webfont.eot'); + src: url('/fonts/roboto-lightitalic-webfont.eot?#iefix') format('embedded-opentype'), + url('/fonts/roboto-lightitalic-webfont.woff2') format('woff2'), + url('/fonts/roboto-lightitalic-webfont.woff') format('woff'), + url('/fonts/roboto-lightitalic-webfont.ttf') format('truetype'), + url('/fonts/roboto-lightitalic-webfont.svg#robotolight_italic') format('svg'); + font-weight: 300; + font-style: italic; +} + +@font-face { + font-family: 'Roboto'; + src: url('/fonts/roboto-medium-webfont.eot'); + src: url('/fonts/roboto-medium-webfont.eot?#iefix') format('embedded-opentype'), + url('/fonts/roboto-medium-webfont.woff2') format('woff2'), + url('/fonts/roboto-medium-webfont.woff') format('woff'), + url('/fonts/roboto-medium-webfont.ttf') format('truetype'), + url('/fonts/roboto-medium-webfont.svg#robotomedium') format('svg'); + font-weight: 500; + font-style: normal; +} + +@font-face { + font-family: 'Roboto'; + src: url('/fonts/roboto-mediumitalic-webfont.eot'); + src: url('/fonts/roboto-mediumitalic-webfont.eot?#iefix') format('embedded-opentype'), + url('/fonts/roboto-mediumitalic-webfont.woff2') format('woff2'), + url('/fonts/roboto-mediumitalic-webfont.woff') format('woff'), + url('/fonts/roboto-mediumitalic-webfont.ttf') format('truetype'), + url('/fonts/roboto-mediumitalic-webfont.svg#robotomedium_italic') format('svg'); + font-weight: 500; + font-style: italic; +} + +@font-face { + font-family: 'Roboto'; + src: url('/fonts/roboto-regular-webfont.eot'); + src: url('/fonts/roboto-regular-webfont.eot?#iefix') format('embedded-opentype'), + url('/fonts/roboto-regular-webfont.woff2') format('woff2'), + url('/fonts/roboto-regular-webfont.woff') format('woff'), + url('/fonts/roboto-regular-webfont.ttf') format('truetype'), + url('/fonts/roboto-regular-webfont.svg#robotoregular') format('svg'); + font-weight: normal; + font-style: normal; +} \ No newline at end of file diff --git a/resources/assets/sass/_header.scss b/resources/assets/sass/_header.scss index 2035a1822..22c9994d4 100644 --- a/resources/assets/sass/_header.scss +++ b/resources/assets/sass/_header.scss @@ -187,7 +187,7 @@ form.search-box { } .faded { - a, button, span { + a, button, span, span > div { color: #666; } .text-button { diff --git a/resources/assets/sass/_variables.scss b/resources/assets/sass/_variables.scss index c02488d86..29a46302b 100644 --- a/resources/assets/sass/_variables.scss +++ b/resources/assets/sass/_variables.scss @@ -52,101 +52,3 @@ $text-light: #EEE; $bs-light: 0 0 4px 1px #CCC; $bs-med: 0 1px 3px 1px rgba(76, 76, 76, 0.26); $bs-hover: 0 2px 2px 1px rgba(0,0,0,.13); - - -/* Generated by Font Squirrel (http://www.fontsquirrel.com) on December 30, 2015 */ -@font-face { - font-family: 'Roboto'; - src: url('/fonts/roboto-bold-webfont.eot'); - src: url('/fonts/roboto-bold-webfont.eot?#iefix') format('embedded-opentype'), - url('/fonts/roboto-bold-webfont.woff2') format('woff2'), - url('/fonts/roboto-bold-webfont.woff') format('woff'), - url('/fonts/roboto-bold-webfont.ttf') format('truetype'), - url('/fonts/roboto-bold-webfont.svg#robotobold') format('svg'); - font-weight: bold; - font-style: normal; -} - -@font-face { - font-family: 'Roboto'; - src: url('/fonts/roboto-bolditalic-webfont.eot'); - src: url('/fonts/roboto-bolditalic-webfont.eot?#iefix') format('embedded-opentype'), - url('/fonts/roboto-bolditalic-webfont.woff2') format('woff2'), - url('/fonts/roboto-bolditalic-webfont.woff') format('woff'), - url('/fonts/roboto-bolditalic-webfont.ttf') format('truetype'), - url('/fonts/roboto-bolditalic-webfont.svg#robotobold_italic') format('svg'); - font-weight: bold; - font-style: italic; -} - -@font-face { - font-family: 'Roboto'; - src: url('/fonts/roboto-italic-webfont.eot'); - src: url('/fonts/roboto-italic-webfont.eot?#iefix') format('embedded-opentype'), - url('/fonts/roboto-italic-webfont.woff2') format('woff2'), - url('/fonts/roboto-italic-webfont.woff') format('woff'), - url('/fonts/roboto-italic-webfont.ttf') format('truetype'), - url('/fonts/roboto-italic-webfont.svg#robotoitalic') format('svg'); - font-weight: normal; - font-style: italic; -} - -@font-face { - font-family: 'Roboto'; - src: url('/fonts/roboto-light-webfont.eot'); - src: url('/fonts/roboto-light-webfont.eot?#iefix') format('embedded-opentype'), - url('/fonts/roboto-light-webfont.woff2') format('woff2'), - url('/fonts/roboto-light-webfont.woff') format('woff'), - url('/fonts/roboto-light-webfont.ttf') format('truetype'), - url('/fonts/roboto-light-webfont.svg#robotolight') format('svg'); - font-weight: 300; - font-style: normal; -} - -@font-face { - font-family: 'Roboto'; - src: url('/fonts/roboto-lightitalic-webfont.eot'); - src: url('/fonts/roboto-lightitalic-webfont.eot?#iefix') format('embedded-opentype'), - url('/fonts/roboto-lightitalic-webfont.woff2') format('woff2'), - url('/fonts/roboto-lightitalic-webfont.woff') format('woff'), - url('/fonts/roboto-lightitalic-webfont.ttf') format('truetype'), - url('/fonts/roboto-lightitalic-webfont.svg#robotolight_italic') format('svg'); - font-weight: 300; - font-style: italic; -} - -@font-face { - font-family: 'Roboto'; - src: url('/fonts/roboto-medium-webfont.eot'); - src: url('/fonts/roboto-medium-webfont.eot?#iefix') format('embedded-opentype'), - url('/fonts/roboto-medium-webfont.woff2') format('woff2'), - url('/fonts/roboto-medium-webfont.woff') format('woff'), - url('/fonts/roboto-medium-webfont.ttf') format('truetype'), - url('/fonts/roboto-medium-webfont.svg#robotomedium') format('svg'); - font-weight: 500; - font-style: normal; -} - -@font-face { - font-family: 'Roboto'; - src: url('/fonts/roboto-mediumitalic-webfont.eot'); - src: url('/fonts/roboto-mediumitalic-webfont.eot?#iefix') format('embedded-opentype'), - url('/fonts/roboto-mediumitalic-webfont.woff2') format('woff2'), - url('/fonts/roboto-mediumitalic-webfont.woff') format('woff'), - url('/fonts/roboto-mediumitalic-webfont.ttf') format('truetype'), - url('/fonts/roboto-mediumitalic-webfont.svg#robotomedium_italic') format('svg'); - font-weight: 500; - font-style: italic; -} - -@font-face { - font-family: 'Roboto'; - src: url('/fonts/roboto-regular-webfont.eot'); - src: url('/fonts/roboto-regular-webfont.eot?#iefix') format('embedded-opentype'), - url('/fonts/roboto-regular-webfont.woff2') format('woff2'), - url('/fonts/roboto-regular-webfont.woff') format('woff'), - url('/fonts/roboto-regular-webfont.ttf') format('truetype'), - url('/fonts/roboto-regular-webfont.svg#robotoregular') format('svg'); - font-weight: normal; - font-style: normal; -} diff --git a/resources/assets/sass/export-styles.scss b/resources/assets/sass/export-styles.scss new file mode 100644 index 000000000..31a89b4ce --- /dev/null +++ b/resources/assets/sass/export-styles.scss @@ -0,0 +1,12 @@ +@import "reset"; +@import "variables"; +@import "mixins"; +@import "html"; +@import "text"; +@import "grid"; +@import "blocks"; +@import "forms"; +@import "tables"; +@import "header"; +@import "lists"; +@import "pages"; \ No newline at end of file diff --git a/resources/assets/sass/styles.scss b/resources/assets/sass/styles.scss index 710bee7ee..c419c08b6 100644 --- a/resources/assets/sass/styles.scss +++ b/resources/assets/sass/styles.scss @@ -1,5 +1,6 @@ @import "reset"; @import "variables"; +@import "fonts"; @import "mixins"; @import "html"; @import "text"; diff --git a/resources/views/pages/pdf.blade.php b/resources/views/pages/pdf.blade.php new file mode 100644 index 000000000..73cf131bc --- /dev/null +++ b/resources/views/pages/pdf.blade.php @@ -0,0 +1,32 @@ + + + + + {{ $page->name }} + + + + +
+
+
+
+ + @include('pages/page-display') + +
+ +

+ Created {{$page->created_at->diffForHumans()}} @if($page->createdBy) by {{$page->createdBy->name}} @endif +
+ Last Updated {{$page->updated_at->diffForHumans()}} @if($page->updatedBy) by {{$page->updatedBy->name}} @endif +

+ +
+
+
+
+ + diff --git a/resources/views/pages/show.blade.php b/resources/views/pages/show.blade.php index 612ccdba6..f2ce6fb07 100644 --- a/resources/views/pages/show.blade.php +++ b/resources/views/pages/show.blade.php @@ -19,6 +19,12 @@
+ +
Export Page
+ +
@if($currentUser->can('page-update')) Revisions Edit