2FAuth/app/Services/IconStoreService.php
2024-11-27 11:37:14 +01:00

308 lines
7.5 KiB
PHP

<?php
namespace App\Services;
use App\Exceptions\FailedIconStoreDatabaseTogglingException;
use App\Facades\Settings;
use App\Models\Icon;
use App\Models\TwoFAccount;
use enshrined\svgSanitize\Sanitizer;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Mockery\MockInterface;
class IconStoreService
{
/**
* The storage disk to use
*/
protected ?string $disk;
/**
* Icon replication to database to ease backup
*/
protected bool $usesDatabase;
/**
* The SVG sanitizer
*/
protected Sanitizer $svgSanitizer;
public function __construct(Sanitizer $svgSanitizer)
{
$this->usesDatabase = Settings::get('storeIconsInDatabase');
$this->setDisk();
$this->svgSanitizer = $svgSanitizer;
$this->svgSanitizer->removeRemoteReferences(true);
$this->svgSanitizer->minify(true);
}
/**
* The storage disk instance
*/
protected function disk() : Filesystem|MockInterface
{
return Storage::disk($this->disk);
}
/**
* Set the storage disk to use
*
* @return $this
*/
public function setDisk(string $diskName = 'icons')
{
$this->disk = $diskName;
return $this;
}
/**
* Whether or not database replication is enabled on the store.
* This should always equals the 'storeIconsInDatabase' setting
*/
public function usesDatabase() : bool
{
return $this->usesDatabase;
}
/**
* Toggle database replication
*/
public function setDatabaseReplication(bool $usesDatabase) : void
{
if ($this->usesDatabase != $usesDatabase) {
if ($usesDatabase) {
$this->clearDatabase();
$this->mirrorDiskToDatabase();
} else {
$this->mirrorDatabaseToDisk();
$this->clearDatabase();
}
$this->usesDatabase = $usesDatabase;
}
}
/**
* Insert all registered icons into the database
*/
protected function mirrorDiskToDatabase() : void
{
DB::beginTransaction();
try {
foreach ($this->registeredIcons() as $filename) {
if ($content = $this->get($filename)) {
$this->storeToDatabase($filename, $content);
}
}
DB::commit();
} catch (\Exception $e) {
DB::rollback();
throw new FailedIconStoreDatabaseTogglingException;
}
}
/**
* Save all database records as file in the disk
*/
protected function mirrorDatabaseToDisk() : void
{
foreach (Icon::all() as $icon) {
if (! $this->storeToDisk($icon->name, $icon->content)) {
throw new FailedIconStoreDatabaseTogglingException;
}
}
}
/**
* Get the list of all icon names registered in the TwoFAccount table
*
* @return Collection<array-key, mixed>
*/
protected function registeredIcons()
{
return TwoFAccount::whereNotNull('icon')->pluck('icon');
}
/**
* Get the content of a given icon resource, prior to the database record
*/
public function get(string $name) : ?string
{
if ($this->usesDatabase) {
return Icon::find($name)?->content;
} else {
return $this->disk()->get($name) ?: null;
}
}
/**
* Get the mime-type of a given icon resource
*/
public function mimeType(string $name) : string|false
{
if ($this->usesDatabase && $this->missingInDisk($name)) {
$this->storeToDisk($name, $this->get($name));
}
return $this->disk()->mimeType($name);
}
/**
* Delete all icons from the storage
*/
public function clear() : bool
{
$diskCleared = $this->clearDisk();
if ($diskCleared && $this->usesDatabase) {
$this->clearDatabase();
}
return $diskCleared;
}
/**
* Delete all icons on the disk
*/
protected function clearDisk() : bool
{
$filesForDelete = Arr::where($this->disk()->files(), function (string $filename) {
return Str::endsWith($filename, ['png', 'jpg', 'jpeg', 'bmp', 'webp', 'svg']);
});
return $this->disk()->delete($filesForDelete);
}
/**
* Delete all icons from the database
*/
protected function clearDatabase() : void
{
Icon::truncate();
}
/**
* Delete the given icons from the storage
*/
public function delete(array|string $names) : bool
{
$names = is_array($names) ? $names : func_get_args();
$deletedFromDisk = $this->disk()->delete($names);
if ($deletedFromDisk && $this->usesDatabase) {
Icon::destroy($names);
return Icon::whereIn('name', $names)->count() == 0;
}
return $deletedFromDisk;
}
/**
* Create the given icon in the storage
*/
public function store(string $name, string $content) : bool
{
$storedToDisk = $this->storeToDisk($name, $content);
if ($this->mimeType($name) == 'image/svg+xml') {
$sanitized = $this->sanitize($content);
if (! $sanitized) {
$this->delete($name);
return false;
}
if ($content != $sanitized) {
$content = $sanitized;
$storedToDisk = $this->storeToDisk($name, $content);
}
}
if ($this->usesDatabase) {
return $this->storeToDatabase($name, $content);
}
return $storedToDisk;
}
/**
* Sanitize the given content (when icon is an svg image)
*/
protected function sanitize(string $content) : string
{
return $this->svgSanitizer->sanitize($content);
}
/**
* Create the given icon in the disk
*/
protected function storeToDisk(string $name, string $content) : bool
{
return $this->disk()->put($name, $content);
}
/**
* Create the given icon in the database
*/
protected function storeToDatabase(string $name, string $content) : bool
{
$icon = Icon::firstOrNew(['name' => $name]);
$icon->content = $content;
return $icon->save();
}
/**
* Determines if an icon exists in the store, prior to the database.
* If a database record does not have the corresponding file in disk, it will create it.
*/
public function exists(string $name) : bool
{
if ($this->usesDatabase) {
$exists = $this->existsInDatabase($name);
if ($exists && $this->missingInDisk($name)) {
$this->storeToDisk($name, $this->get($name));
}
return $exists;
} else {
return $this->existsInDisk($name);
}
}
/**
* Determine if an icon exists in the database
*/
protected function existsInDatabase(string $name) : bool
{
return Icon::find($name) != null;
}
/**
* Determine if an icon exists in the database
*/
protected function existsInDisk(string $name) : bool
{
return $this->disk()->exists($name);
}
/**
* Determine if an icon is missing in the database
*/
protected function missingInDisk(string $name) : bool
{
return ! $this->existsInDisk($name);
}
}