Skip to content

Commit

Permalink
Merge pull request #4032 from BookStackApp/favicon
Browse files Browse the repository at this point in the history
Generate favicon.ico file
  • Loading branch information
ssddanbrown authored Feb 9, 2023
2 parents da1a66a + f333db8 commit 646f8f6
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 5 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ yarn-error.log
/public/js
/public/bower
/public/build/
/public/favicon.ico
/storage/images
_ide_helper.php
/storage/debugbar
Expand Down
12 changes: 12 additions & 0 deletions app/Http/Controllers/HomeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Entities\Tools\PageContent;
use BookStack\Uploads\FaviconHandler;
use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;

Expand Down Expand Up @@ -127,4 +128,15 @@ public function notFound()
{
return response()->view('errors.404', [], 404);
}

/**
* Serve the application favicon.
* Ensures a 'favicon.ico' file exists at the web root location (if writable) to be served
* directly by the webserver in the future.
*/
public function favicon(FaviconHandler $favicons)
{
$exists = $favicons->restoreOriginalIfNotExists();
return response()->file($exists ? $favicons->getPath() : $favicons->getOriginalPath());
}
}
14 changes: 9 additions & 5 deletions app/Settings/AppSettingsStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@

namespace BookStack\Settings;

use BookStack\Uploads\FaviconHandler;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;

class AppSettingsStore
{
protected ImageRepo $imageRepo;

public function __construct(ImageRepo $imageRepo)
{
$this->imageRepo = $imageRepo;
public function __construct(
protected ImageRepo $imageRepo,
protected FaviconHandler $faviconHandler,
) {
}

public function storeFromUpdateRequest(Request $request, string $category)
Expand Down Expand Up @@ -39,6 +39,8 @@ protected function updateAppIcon(Request $request): void
$icon = $this->imageRepo->saveNew($iconFile, 'system', 0, $size, $size);
setting()->put('app-icon-' . $size, $icon->url);
}

$this->faviconHandler->saveForUploadedImage($iconFile);
}

// Clear icon image if requested
Expand All @@ -49,6 +51,8 @@ protected function updateAppIcon(Request $request): void
$this->destroyExistingSettingImage('app-icon-' . $size);
setting()->remove('app-icon-' . $size);
}

$this->faviconHandler->restoreOriginal();
}
}

Expand Down
110 changes: 110 additions & 0 deletions app/Uploads/FaviconHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

namespace BookStack\Uploads;

use Illuminate\Http\UploadedFile;
use Intervention\Image\ImageManager;

class FaviconHandler
{
protected string $path;

public function __construct(
protected ImageManager $imageTool
) {
$this->path = public_path('favicon.ico');
}

/**
* Save the given UploadedFile instance as the application favicon.
*/
public function saveForUploadedImage(UploadedFile $file): void
{
if (!is_writeable($this->path)) {
return;
}

$imageData = file_get_contents($file->getRealPath());
$image = $this->imageTool->make($imageData);
$image->resize(32, 32);
$bmpData = $image->encode('png');
$icoData = $this->pngToIco($bmpData, 32, 32);

file_put_contents($this->path, $icoData);
}

/**
* Restore the original favicon image.
* Returned boolean indicates if the copy occurred.
*/
public function restoreOriginal(): bool
{
$permissionItem = file_exists($this->path) ? $this->path : dirname($this->path);
if (!is_writeable($permissionItem)) {
return false;
}

return copy($this->getOriginalPath(), $this->path);
}

/**
* Restore the original favicon image if no favicon image is already in use.
* Returns a boolean to indicate if the file exists.
*/
public function restoreOriginalIfNotExists(): bool
{
if (file_exists($this->path)) {
return true;
}

return $this->restoreOriginal();
}

/**
* Get the path to the favicon file.
*/
public function getPath(): string
{
return $this->path;
}

/**
* Get the path of the original favicon copy.
*/
public function getOriginalPath(): string
{
return public_path('icon.ico');
}

/**
* Convert PNG image data to ICO file format.
* Built following the file format info from Wikipedia:
* https://en.wikipedia.org/wiki/ICO_(file_format)
*/
protected function pngToIco(string $bmpData, int $width, int $height): string
{
// ICO header
$header = pack('v', 0x00); // Reserved. Must always be 0
$header .= pack('v', 0x01); // Specifies ico image
$header .= pack('v', 0x01); // Specifies number of images

// ICO Image Directory
$entry = hex2bin(dechex($width)); // Image width
$entry .= hex2bin(dechex($height)); // Image height
$entry .= "\0"; // Color palette, typically 0
$entry .= "\0"; // Reserved

// Color planes, Appears to remain 1 for bmp image data
$entry .= pack('v', 0x01);
// Bits per pixel, can range from 1 to 32. From testing conversion
// via intervention from png typically provides this as 24.
$entry .= pack('v', 0x00);
// Size of the image data in bytes
$entry .= pack('V', strlen($bmpData));
// Offset of the bmp data from file start
$entry .= pack('V', strlen($header) + strlen($entry) + 4);

// Join & return the combined parts of the ICO image data
return $header . $entry . $bmpData;
}
}
File renamed without changes.
1 change: 1 addition & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@

Route::get('/status', [StatusController::class, 'show']);
Route::get('/robots.txt', [HomeController::class, 'robots']);
Route::get('/favicon.ico', [HomeController::class, 'favicon']);

// Authenticated routes...
Route::middleware('auth')->group(function () {
Expand Down
12 changes: 12 additions & 0 deletions tests/PublicActionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,18 @@ public function test_robots_effected_by_setting()
$this->get('/robots.txt')->assertSee("User-agent: *\nDisallow: /");
}

public function test_default_favicon_file_created_upon_access()
{
$faviconPath = public_path('favicon.ico');
if (file_exists($faviconPath)) {
unlink($faviconPath);
}

$this->assertFileDoesNotExist($faviconPath);
$this->get('/favicon.ico');
$this->assertFileExists($faviconPath);
}

public function test_public_view_then_login_redirects_to_previous_content()
{
$this->setSettings(['app-public' => 'true']);
Expand Down
14 changes: 14 additions & 0 deletions tests/Settings/SettingsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ public function test_updating_and_removing_app_icon()
$this->assertFalse(setting()->get('app-icon-128'));
$this->assertFalse(setting()->get('app-icon-64'));
$this->assertFalse(setting()->get('app-icon-32'));
$this->assertEquals(
file_get_contents(public_path('icon.ico')),
file_get_contents(public_path('favicon.ico')),
);

$prevFileCount = count(glob(dirname($expectedPath) . DIRECTORY_SEPARATOR . '*.png'));

Expand All @@ -71,6 +75,11 @@ public function test_updating_and_removing_app_icon()
$resp = $this->get('/');
$this->withHtml($resp)->assertElementCount('link[sizes][href*="my-app-icon"]', 6);

$this->assertNotEquals(
file_get_contents(public_path('icon.ico')),
file_get_contents(public_path('favicon.ico')),
);

$reset = $this->post('/settings/customization', ['app_icon_reset' => 'true']);
$reset->assertRedirect('/settings/customization');

Expand All @@ -81,5 +90,10 @@ public function test_updating_and_removing_app_icon()
$this->assertFalse(setting()->get('app-icon-128'));
$this->assertFalse(setting()->get('app-icon-64'));
$this->assertFalse(setting()->get('app-icon-32'));

$this->assertEquals(
file_get_contents(public_path('icon.ico')),
file_get_contents(public_path('favicon.ico')),
);
}
}

0 comments on commit 646f8f6

Please sign in to comment.