mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-04-09 08:16:37 +08:00
Improved export base64 encoding of images
Now will use set storage mechanism to find image files. Fixes #786 Added test to cover
This commit is contained in:
parent
6aeb1387aa
commit
67e0c3d2a5
@ -9,14 +9,16 @@ class ExportService
|
|||||||
{
|
{
|
||||||
|
|
||||||
protected $entityRepo;
|
protected $entityRepo;
|
||||||
|
protected $imageService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ExportService constructor.
|
* ExportService constructor.
|
||||||
* @param $entityRepo
|
* @param $entityRepo
|
||||||
*/
|
*/
|
||||||
public function __construct(EntityRepo $entityRepo)
|
public function __construct(EntityRepo $entityRepo, ImageService $imageService)
|
||||||
{
|
{
|
||||||
$this->entityRepo = $entityRepo;
|
$this->entityRepo = $entityRepo;
|
||||||
|
$this->imageService = $imageService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -24,6 +26,7 @@ class ExportService
|
|||||||
* Includes required CSS & image content. Images are base64 encoded into the HTML.
|
* Includes required CSS & image content. Images are base64 encoded into the HTML.
|
||||||
* @param Page $page
|
* @param Page $page
|
||||||
* @return mixed|string
|
* @return mixed|string
|
||||||
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function pageToContainedHtml(Page $page)
|
public function pageToContainedHtml(Page $page)
|
||||||
{
|
{
|
||||||
@ -38,6 +41,7 @@ class ExportService
|
|||||||
* Convert a chapter to a self-contained HTML file.
|
* Convert a chapter to a self-contained HTML file.
|
||||||
* @param Chapter $chapter
|
* @param Chapter $chapter
|
||||||
* @return mixed|string
|
* @return mixed|string
|
||||||
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function chapterToContainedHtml(Chapter $chapter)
|
public function chapterToContainedHtml(Chapter $chapter)
|
||||||
{
|
{
|
||||||
@ -56,6 +60,7 @@ class ExportService
|
|||||||
* Convert a book to a self-contained HTML file.
|
* Convert a book to a self-contained HTML file.
|
||||||
* @param Book $book
|
* @param Book $book
|
||||||
* @return mixed|string
|
* @return mixed|string
|
||||||
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function bookToContainedHtml(Book $book)
|
public function bookToContainedHtml(Book $book)
|
||||||
{
|
{
|
||||||
@ -71,6 +76,7 @@ class ExportService
|
|||||||
* Convert a page to a PDF file.
|
* Convert a page to a PDF file.
|
||||||
* @param Page $page
|
* @param Page $page
|
||||||
* @return mixed|string
|
* @return mixed|string
|
||||||
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function pageToPdf(Page $page)
|
public function pageToPdf(Page $page)
|
||||||
{
|
{
|
||||||
@ -85,6 +91,7 @@ class ExportService
|
|||||||
* Convert a chapter to a PDF file.
|
* Convert a chapter to a PDF file.
|
||||||
* @param Chapter $chapter
|
* @param Chapter $chapter
|
||||||
* @return mixed|string
|
* @return mixed|string
|
||||||
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function chapterToPdf(Chapter $chapter)
|
public function chapterToPdf(Chapter $chapter)
|
||||||
{
|
{
|
||||||
@ -103,6 +110,7 @@ class ExportService
|
|||||||
* Convert a book to a PDF file
|
* Convert a book to a PDF file
|
||||||
* @param Book $book
|
* @param Book $book
|
||||||
* @return string
|
* @return string
|
||||||
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function bookToPdf(Book $book)
|
public function bookToPdf(Book $book)
|
||||||
{
|
{
|
||||||
@ -118,6 +126,7 @@ class ExportService
|
|||||||
* Convert normal webpage HTML to a PDF.
|
* Convert normal webpage HTML to a PDF.
|
||||||
* @param $html
|
* @param $html
|
||||||
* @return string
|
* @return string
|
||||||
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
protected function htmlToPdf($html)
|
protected function htmlToPdf($html)
|
||||||
{
|
{
|
||||||
@ -146,45 +155,14 @@ class ExportService
|
|||||||
// Replace image src with base64 encoded image strings
|
// Replace image src with base64 encoded image strings
|
||||||
if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
|
if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
|
||||||
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
|
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
|
||||||
$oldImgString = $imgMatch;
|
$oldImgTagString = $imgMatch;
|
||||||
$srcString = $imageTagsOutput[2][$index];
|
$srcString = $imageTagsOutput[2][$index];
|
||||||
$isLocal = strpos(trim($srcString), 'http') !== 0;
|
$imageEncoded = $this->imageService->imageUriToBase64($srcString);
|
||||||
if ($isLocal) {
|
if ($imageEncoded === null) {
|
||||||
$pathString = public_path(trim($srcString, '/'));
|
$imageEncoded = $srcString;
|
||||||
} else {
|
|
||||||
$pathString = $srcString;
|
|
||||||
}
|
}
|
||||||
|
$newImgTagString = str_replace($srcString, $imageEncoded, $oldImgTagString);
|
||||||
// Attempt to find local files even if url not absolute
|
$htmlContent = str_replace($oldImgTagString, $newImgTagString, $htmlContent);
|
||||||
$base = baseUrl('/');
|
|
||||||
if (strpos($srcString, $base) === 0) {
|
|
||||||
$isLocal = true;
|
|
||||||
$relString = str_replace($base, '', $srcString);
|
|
||||||
$pathString = public_path(trim($relString, '/'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($isLocal && !file_exists($pathString)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if ($isLocal) {
|
|
||||||
$imageContent = file_get_contents($pathString);
|
|
||||||
} else {
|
|
||||||
$ch = curl_init();
|
|
||||||
curl_setopt_array($ch, [CURLOPT_URL => $pathString, CURLOPT_RETURNTRANSFER => 1, CURLOPT_CONNECTTIMEOUT => 5]);
|
|
||||||
$imageContent = curl_exec($ch);
|
|
||||||
$err = curl_error($ch);
|
|
||||||
curl_close($ch);
|
|
||||||
if ($err) {
|
|
||||||
throw new \Exception("Image fetch failed, Received error: " . $err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
|
|
||||||
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
|
|
||||||
} catch (\ErrorException $e) {
|
|
||||||
$newImageString = '';
|
|
||||||
}
|
|
||||||
$htmlContent = str_replace($oldImgString, $newImageString, $htmlContent);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -321,6 +321,52 @@ class ImageService extends UploadService
|
|||||||
return $image;
|
return $image;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a image URI to a Base64 encoded string.
|
||||||
|
* Attempts to find locally via set storage method first.
|
||||||
|
* @param string $uri
|
||||||
|
* @return null|string
|
||||||
|
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
|
||||||
|
*/
|
||||||
|
public function imageUriToBase64(string $uri)
|
||||||
|
{
|
||||||
|
$isLocal = strpos(trim($uri), 'http') !== 0;
|
||||||
|
|
||||||
|
// Attempt to find local files even if url not absolute
|
||||||
|
$base = baseUrl('/');
|
||||||
|
if (!$isLocal && strpos($uri, $base) === 0) {
|
||||||
|
$isLocal = true;
|
||||||
|
$uri = str_replace($base, '', $uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
$imageData = null;
|
||||||
|
|
||||||
|
if ($isLocal) {
|
||||||
|
$uri = trim($uri, '/');
|
||||||
|
$storage = $this->getStorage();
|
||||||
|
if ($storage->exists($uri)) {
|
||||||
|
$imageData = $storage->get($uri);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt_array($ch, [CURLOPT_URL => $uri, CURLOPT_RETURNTRANSFER => 1, CURLOPT_CONNECTTIMEOUT => 5]);
|
||||||
|
$imageData = curl_exec($ch);
|
||||||
|
$err = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
if ($err) {
|
||||||
|
throw new \Exception("Image fetch failed, Received error: " . $err);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($imageData === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'data:image/' . pathinfo($uri, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageData);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a public facing url for an image by checking relevant environment variables.
|
* Gets a public facing url for an image by checking relevant environment variables.
|
||||||
* @param string $filePath
|
* @param string $filePath
|
||||||
|
@ -106,6 +106,29 @@ class ImageTest extends TestCase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_secure_images_included_in_exports()
|
||||||
|
{
|
||||||
|
config()->set('filesystems.default', 'local_secure');
|
||||||
|
$this->asEditor();
|
||||||
|
$galleryFile = $this->getTestImage('my-secure-test-upload');
|
||||||
|
$page = Page::first();
|
||||||
|
$expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m-M') . '/my-secure-test-upload');
|
||||||
|
|
||||||
|
$upload = $this->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
|
||||||
|
$imageUrl = json_decode($upload->getContent(), true)['url'];
|
||||||
|
$page->html .= "<img src=\"{$imageUrl}\">";
|
||||||
|
$page->save();
|
||||||
|
$upload->assertStatus(200);
|
||||||
|
|
||||||
|
$encodedImageContent = base64_encode(file_get_contents($expectedPath));
|
||||||
|
$export = $this->get($page->getUrl('/export/html'));
|
||||||
|
$this->assertTrue(str_contains($export->getContent(), $encodedImageContent), 'Uploaded image in export content');
|
||||||
|
|
||||||
|
if (file_exists($expectedPath)) {
|
||||||
|
unlink($expectedPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function test_system_images_remain_public()
|
public function test_system_images_remain_public()
|
||||||
{
|
{
|
||||||
config()->set('filesystems.default', 'local_secure');
|
config()->set('filesystems.default', 'local_secure');
|
||||||
|
Loading…
x
Reference in New Issue
Block a user