123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- <?php
- namespace App\Services;
- use App\Exceptions\DbEncryptionException;
- use App\Models\Option;
- use Exception;
- use Illuminate\Support\Collection;
- use Illuminate\Support\Facades\Cache;
- use Illuminate\Support\Facades\Crypt;
- use Illuminate\Support\Facades\DB;
- use Illuminate\Support\Facades\Log;
- use Throwable;
- class SettingService
- {
- /**
- * All settings
- *
- * @var Collection<string, mixed>
- */
- private Collection $settings;
- /**
- * Cache duration
- */
- private int $minutes = 10;
- /**
- * Name of the cache item where options are persisted
- */
- public const CACHE_ITEM_NAME = 'adminOptions';
- /**
- * Constructor
- */
- public function __construct()
- {
- $this->settings = Cache::remember(self::CACHE_ITEM_NAME, now()->addMinutes($this->minutes), function () {
- self::build();
- return $this->settings;
- });
- }
- /**
- * Get a setting
- *
- * @param string $setting A single setting name
- * @return mixed string|int|boolean|null
- */
- public function get($setting)
- {
- return $this->settings->get($setting);
- }
- /**
- * Get all settings
- *
- * @return Collection<string, mixed> the Settings collection
- */
- public function all() : Collection
- {
- return $this->settings;
- }
- /**
- * Set a setting
- *
- * @param string|array $setting A single setting name or an associative array of name:value settings
- * @param string|int|bool|null $value The value for single setting
- */
- public function set($setting, $value = null) : void
- {
- $settings = is_array($setting) ? $setting : [$setting => $value];
- foreach ($settings as $setting => $value) {
- if ($setting === 'useEncryption') {
- $this->setEncryptionTo($value);
- }
- $settings[$setting] = $this->replaceBoolean($value);
- }
- foreach ($settings as $setting => $value) {
- Option::updateOrCreate(['key' => $setting], ['value' => $value]);
- Log::notice(sprintf('App setting %s set to %s', var_export($setting, true), var_export($this->restoreType($value), true)));
- }
- self::buildAndCache();
- }
- /**
- * Delete a setting
- *
- * @param string $name The setting name
- */
- public function delete(string $name) : void
- {
- Option::where('key', $name)->delete();
- Log::notice(sprintf('App setting %s reset to default', var_export($name, true)));
- self::buildAndCache();
- }
- /**
- * Determine if the given setting has been edited
- *
- * @param string $key
- */
- public function isEdited($key) : bool
- {
- return DB::table('options')->where('key', $key)->exists();
- }
- /**
- * Set the settings collection
- *
- * @return void
- */
- private function build()
- {
- // Get a collection of saved options
- $options = DB::table('options')->pluck('value', 'key');
- $options->transform(function ($item, $key) {
- return $this->restoreType($item);
- });
- // Merge customized values with app default values
- $settings = collect(config('2fauth.settings'))->merge($options); /** @phpstan-ignore-line */
- $this->settings = $settings;
- }
- /**
- * Build and cache the options collection
- *
- * @return void
- */
- private function buildAndCache()
- {
- self::build();
- Cache::put(self::CACHE_ITEM_NAME, $this->settings, now()->addMinutes($this->minutes));
- }
- /**
- * Replaces boolean by a patterned string as appstrack/laravel-options package does not support var type
- *
- * @return string
- */
- private function replaceBoolean(mixed $value)
- {
- return is_bool($value) ? '{{' . $value . '}}' : $value;
- }
- /**
- * Replaces patterned string that represent booleans with real booleans
- *
- * @return mixed
- */
- private function restoreType(mixed $value)
- {
- if (is_numeric($value)) {
- $value = is_float($value + 0) ? (float) $value : (int) $value;
- }
- if ($value === '{{}}') {
- return false;
- } elseif ($value === '{{1}}') {
- return true;
- } else {
- return $value;
- }
- }
- /**
- * Enable or Disable encryption of 2FAccounts sensible data
- *
- *
- * @throws DbEncryptionException Something failed, everything have been rolled back
- */
- private function setEncryptionTo(bool $state) : void
- {
- // We don't want the records to be encrypted/decrypted multiple successive times
- $isInUse = $this->get('useEncryption');
- if ($isInUse === ! $state) {
- if ($this->updateRecords($state)) {
- if ($state) {
- Log::notice('Sensible data are now encrypted');
- } else {
- Log::notice('Sensible data are now decrypted');
- }
- } else {
- Log::warning('Some data cannot be encrypted/decrypted, the useEncryption setting remain unchanged');
- throw new DbEncryptionException($state === true ? __('errors.error_during_encryption') : __('errors.error_during_decryption'));
- }
- }
- }
- /**
- * Encrypt/Decrypt accounts in database
- *
- * @param bool $encrypted Whether the record should be encrypted or not
- * @return bool Whether the operation completed successfully
- */
- private function updateRecords(bool $encrypted) : bool
- {
- $success = true;
- $twofaccounts = DB::table('twofaccounts')->get();
- $twofaccounts->each(function ($item, $key) use (&$success, $encrypted) {
- try {
- $item->legacy_uri = $encrypted ? Crypt::encryptString($item->legacy_uri) : Crypt::decryptString($item->legacy_uri);
- $item->account = $encrypted ? Crypt::encryptString($item->account) : Crypt::decryptString($item->account);
- $item->secret = $encrypted ? Crypt::encryptString($item->secret) : Crypt::decryptString($item->secret);
- } catch (Exception $ex) {
- $success = false;
- // Exit the each iteration
- return false;
- }
- });
- if ($success) {
- // The whole collection has now its sensible data encrypted/decrypted
- // We update the db using a transaction that can rollback everything if an error occured
- DB::beginTransaction();
- try {
- $twofaccounts->each(function ($item, $key) {
- DB::table('twofaccounts')
- ->where('id', $item->id)
- ->update([
- 'legacy_uri' => $item->legacy_uri,
- 'account' => $item->account,
- 'secret' => $item->secret,
- ]);
- });
- DB::commit();
- return true;
- }
- // @codeCoverageIgnoreStart
- catch (Throwable $ex) {
- DB::rollBack();
- return false;
- }
- // @codeCoverageIgnoreEnd
- } else {
- return false;
- }
- }
- }
|