Switch Settings to be a flat single entity to use EntityManager built-in functions. (#4045)

This commit is contained in:
Buster "Silver Eagle" Neece 2021-04-21 16:15:52 -05:00 committed by GitHub
parent 3930070637
commit 2dc41d080a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 779 additions and 603 deletions

View File

@ -56,6 +56,7 @@
"psr/http-factory": "^1.0",
"psr/simple-cache": "^1.0",
"ramsey/uuid": "^4.0",
"ramsey/uuid-doctrine": "^1.6",
"rlanvin/php-ip": "^2.0",
"slim/http": "^1.1",
"slim/slim": "^4.2",

54
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "86025d9340610c6ae8f5853b3fa5e2bf",
"content-hash": "17f74c9a9115bff2e514f58d32a7f93f",
"packages": [
{
"name": "aws/aws-sdk-php",
@ -5670,6 +5670,58 @@
],
"time": "2020-08-18T17:17:46+00:00"
},
{
"name": "ramsey/uuid-doctrine",
"version": "1.6.0",
"source": {
"type": "git",
"url": "https://github.com/ramsey/uuid-doctrine.git",
"reference": "9facc4689547e72e03c1e18df4a0ee162b2778b0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ramsey/uuid-doctrine/zipball/9facc4689547e72e03c1e18df4a0ee162b2778b0",
"reference": "9facc4689547e72e03c1e18df4a0ee162b2778b0",
"shasum": ""
},
"require": {
"doctrine/orm": "^2.5",
"php": "^5.4 | ^7 | ^8",
"ramsey/uuid": "^3.5 | ^4"
},
"require-dev": {
"jakub-onderka/php-parallel-lint": "^1",
"mockery/mockery": "^0.9 | ^1",
"phpunit/phpunit": "^4.8.36 | ^5.7 | ^6.5 | ^7",
"squizlabs/php_codesniffer": "^3.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Ramsey\\Uuid\\Doctrine\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Allow the use of ramsey/uuid as a Doctrine field type.",
"homepage": "https://github.com/ramsey/uuid-doctrine",
"keywords": [
"database",
"doctrine",
"guid",
"identifier",
"uuid"
],
"support": {
"issues": "https://github.com/ramsey/uuid-doctrine/issues",
"rss": "https://github.com/ramsey/uuid-doctrine/releases.atom",
"source": "https://github.com/ramsey/uuid-doctrine",
"wiki": "https://github.com/ramsey/uuid/wiki"
},
"time": "2020-01-27T05:09:17+00:00"
},
{
"name": "rlanvin/php-ip",
"version": "v2.1.0",

View File

@ -8,11 +8,13 @@ return [
'elements' => [
'backupEnabled' => [
'backup_enabled' => [
'toggle',
[
'label' => __('Run Automatic Nightly Backups'),
'description' => __('Enable to have AzuraCast automatically run nightly backups at the time specified.'),
'description' => __(
'Enable to have AzuraCast automatically run nightly backups at the time specified.'
),
'selected_text' => __('Yes'),
'deselected_text' => __('No'),
'default' => false,
@ -20,7 +22,7 @@ return [
],
],
'backupTimeCode' => [
'backup_time_code' => [
'PlaylistTime',
[
'label' => __('Scheduled Backup Time'),
@ -29,11 +31,13 @@ return [
],
],
'backupExcludeMedia' => [
'backup_exclude_media' => [
'toggle',
[
'label' => __('Exclude Media from Backups'),
'description' => __('Excluding media from automated backups will save space, but you should make sure to back up your media elsewhere. Note that only locally stored media will be backed up.'),
'description' => __(
'Excluding media from automated backups will save space, but you should make sure to back up your media elsewhere. Note that only locally stored media will be backed up.'
),
'selected_text' => __('Yes'),
'deselected_text' => __('No'),
'default' => false,
@ -41,11 +45,13 @@ return [
],
],
'backupKeepCopies' => [
'backup_keep_copies' => [
'number',
[
'label' => __('Number of Backup Copies to Keep'),
'description' => __('Copies older than the specified number of days will automatically be deleted. Set to zero to disable automatic deletion.'),
'description' => __(
'Copies older than the specified number of days will automatically be deleted. Set to zero to disable automatic deletion.'
),
'min' => 0,
'max' => 365,
'default' => 0,
@ -53,7 +59,7 @@ return [
],
],
'backupStorageLocation' => [
'backup_storage_location' => [
'select',
[
'label' => __('Storage Location'),

View File

@ -8,11 +8,13 @@ return [
'use_grid' => true,
'elements' => [
'publicTheme' => [
'public_theme' => [
'radio',
[
'label' => __('Base Theme for Public Pages'),
'description' => __('Select a theme to use as a base for station public pages and the login page.'),
'description' => __(
'Select a theme to use as a base for station public pages and the login page.'
),
'choices' => [
'light' => __('Light') . ' (' . __('Default') . ')',
'dark' => __('Dark'),
@ -22,7 +24,7 @@ return [
],
],
'hideAlbumArt' => [
'hide_album_art' => [
'toggle',
[
'label' => __('Hide Album Art on Public Pages'),
@ -34,31 +36,37 @@ return [
],
],
'homepageRedirectUrl' => [
'homepage_redirect_url' => [
'text',
[
'label' => __('Homepage Redirect URL'),
'description' => __('If a visitor is not signed in and visits the AzuraCast homepage, you can automatically redirect them to the URL specified here. Leave blank to redirect them to the login screen by default.'),
'description' => __(
'If a visitor is not signed in and visits the AzuraCast homepage, you can automatically redirect them to the URL specified here. Leave blank to redirect them to the login screen by default.'
),
'default' => '',
'form_group_class' => 'col-md-6',
],
],
'defaultAlbumArtUrl' => [
'default_album_art_url' => [
'text',
[
'label' => __('Default Album Art URL'),
'description' => __('If a song has no album art, this URL will be listed instead. Leave blank to use the standard placeholder art.'),
'description' => __(
'If a song has no album art, this URL will be listed instead. Leave blank to use the standard placeholder art.'
),
'default' => '',
'form_group_class' => 'col-md-6',
],
],
'hideProductName' => [
'hide_product_name' => [
'toggle',
[
'label' => __('Hide AzuraCast Branding on Public Pages'),
'description' => __('If selected, this will remove the AzuraCast branding from public-facing pages.'),
'description' => __(
'If selected, this will remove the AzuraCast branding from public-facing pages.'
),
'selected_text' => __('Yes'),
'deselected_text' => __('No'),
'default' => false,
@ -66,7 +74,7 @@ return [
],
],
'publicCustomCss' => [
'public_custom_css' => [
'textarea',
[
'label' => __('Custom CSS for Public Pages'),
@ -80,11 +88,13 @@ return [
],
],
'publicCustomJs' => [
'public_custom_js' => [
'textarea',
[
'label' => __('Custom JS for Public Pages'),
'description' => __('This javascript code will be applied to the station public pages and login page.'),
'description' => __(
'This javascript code will be applied to the station public pages and login page.'
),
'spellcheck' => 'false',
'class' => 'js-editor',
'filter' => function ($val) {
@ -94,7 +104,7 @@ return [
],
],
'internalCustomCss' => [
'internal_custom_css' => [
'textarea',
[
'label' => __('Custom CSS for Internal Pages'),

View File

@ -28,7 +28,7 @@ return [
'tab' => 'system',
'elements' => [
'baseUrl' => [
'base_url' => [
'url',
[
'label' => __('Site Base URL'),
@ -40,7 +40,7 @@ return [
],
],
'instanceName' => [
'instance_name' => [
'text',
[
'label' => __('AzuraCast Instance Name'),
@ -51,7 +51,7 @@ return [
],
],
'preferBrowserUrl' => [
'prefer_browser_url' => [
'toggle',
[
'label' => __('Prefer Browser URL (If Available)'),
@ -65,7 +65,7 @@ return [
],
],
'useRadioProxy' => [
'use_radio_proxy' => [
'toggle',
[
'label' => __('Use Web Proxy for Radio'),
@ -79,7 +79,7 @@ return [
],
],
'historyKeepDays' => [
'history_keep_days' => [
'radio',
[
'label' => __('Days of Playback History to Keep'),
@ -99,7 +99,7 @@ return [
],
],
'enableWebsockets' => [
'enable_websockets' => [
'toggle',
[
'label' => __('Use WebSockets for Now Playing Updates'),
@ -113,7 +113,7 @@ return [
],
],
'enableAdvancedFeatures' => [
'enable_advanced_features' => [
'toggle',
[
'label' => __('Enable Advanced Features'),
@ -136,7 +136,7 @@ return [
'elements' => [
'alwaysUseSsl' => [
'always_use_ssl' => [
'toggle',
[
'label' => __('Always Use HTTPS'),
@ -150,7 +150,7 @@ return [
],
],
'apiAccessControl' => [
'api_access_control' => [
'text',
[
'label' => __('API "Access-Control-Allow-Origin" header'),
@ -212,7 +212,7 @@ return [
],
],
'checkForUpdates' => [
'check_for_updates' => [
'toggle',
[
'label' => __('Show Update Announcements'),
@ -232,7 +232,7 @@ return [
'elements' => [
'mailEnabled' => [
'mail_enabled' => [
'toggle',
[
'label' => __('Enable Mail Delivery'),
@ -243,7 +243,7 @@ return [
],
],
'mailSenderName' => [
'mail_sender_name' => [
'text',
[
'label' => __('Sender Name'),
@ -252,7 +252,7 @@ return [
],
],
'mailSenderEmail' => [
'mail_sender_email' => [
'email',
[
'label' => __('Sender E-mail Address'),
@ -262,7 +262,7 @@ return [
],
],
'mailSmtpHost' => [
'mail_smtp_host' => [
'text',
[
'label' => __('SMTP Host'),
@ -271,7 +271,7 @@ return [
],
],
'mailSmtpPort' => [
'mail_smtp_port' => [
'number',
[
'label' => __('SMTP Port'),
@ -280,7 +280,7 @@ return [
],
],
'mailSmtpSecure' => [
'mail_smtp_secure' => [
'toggle',
[
'label' => __('Use Secure (TLS) SMTP Connection'),
@ -293,7 +293,7 @@ return [
],
],
'mailSmtpUsername' => [
'mail_smtp_username' => [
'text',
[
'label' => __('SMTP Username'),
@ -302,7 +302,7 @@ return [
],
],
'mailSmtpPassword' => [
'mail_smtp_password' => [
'password',
[
'label' => __('SMTP Password'),
@ -320,7 +320,7 @@ return [
'elements' => [
'avatarService' => [
'avatar_service' => [
'radio',
[
'label' => __('Avatar Service'),
@ -335,7 +335,7 @@ return [
],
],
'avatarDefaultUrl' => [
'avatar_default_url' => [
'text',
[
'label' => __('Default Avatar URL'),
@ -354,7 +354,7 @@ return [
'elements' => [
'useExternalAlbumArtInApis' => [
'use_external_album_art_in_apis' => [
'toggle',
[
'label' => __('Check Web Services for Album Art for "Now Playing" Tracks'),
@ -365,7 +365,7 @@ return [
],
],
'useExternalAlbumArtWhenProcessingMedia' => [
'use_external_album_art_when_processing_media' => [
'toggle',
[
'label' => __('Check Web Services for Album Art When Uploading Media'),
@ -376,7 +376,7 @@ return [
],
],
'lastFmApiKey' => [
'last_fm_api_key' => [
'text',
[
'label' => __('Last.fm API Key'),

View File

@ -108,6 +108,10 @@ return [
$config->addCustomNumericFunction('RAND', DoctrineExtensions\Query\Mysql\Rand::class);
if (!Doctrine\DBAL\Types\Type::hasType('uuid')) {
Doctrine\DBAL\Types\Type::addType('uuid', Ramsey\Uuid\Doctrine\UuidType::class);
}
if (!Doctrine\DBAL\Types\Type::hasType('carbon_immutable')) {
Doctrine\DBAL\Types\Type::addType('carbon_immutable', Carbon\Doctrine\CarbonImmutableType::class);
}

View File

@ -8,7 +8,6 @@ use App\Environment;
use App\EventDispatcher;
use App\MessageQueue\LogWorkerExceptionSubscriber;
use App\MessageQueue\QueueManager;
use App\MessageQueue\ReloadSettingsMiddleware;
use App\MessageQueue\ResetArrayCacheMiddleware;
use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\EventListener\StopWorkerOnTimeLimitListener;
@ -42,7 +41,6 @@ class ProcessCommand extends CommandAbstract
$eventDispatcher->addServiceSubscriber(ClearEntityManagerSubscriber::class);
$eventDispatcher->addServiceSubscriber(LogWorkerExceptionSubscriber::class);
$eventDispatcher->addServiceSubscriber(ReloadSettingsMiddleware::class);
$eventDispatcher->addServiceSubscriber(ResetArrayCacheMiddleware::class);
if ($runtime <= 0) {

View File

@ -21,7 +21,9 @@ class ListCommand extends CommandAbstract
];
$rows = [];
$all_settings = $settingsTableRepo->readSettingsArray();
$settings = $settingsTableRepo->readSettings();
$all_settings = $settingsTableRepo->toArray($settings);
foreach ($all_settings as $setting_key => $setting_value) {
$value = print_r($setting_value, true);
$value = Utilities\Strings::truncateText($value, 600);

View File

@ -73,7 +73,7 @@ class SetupCommand extends CommandAbstract
$this->runCommand($output, 'queue:clear');
$settings = $settingsRepo->readSettings(true);
$settings = $settingsRepo->readSettings();
$settings->setNowplaying(null);
$stationRepo->clearNowPlaying();
@ -92,10 +92,6 @@ class SetupCommand extends CommandAbstract
$settings->setExternalIp(null);
}
if (!$update) {
$settings->setAppUniqueIdentifier(null);
}
$settingsRepo->writeSettings($settings);
$storageLocationRepo->createDefaultStorageLocations();

View File

@ -26,7 +26,7 @@ class InstallGeoLiteController
$flash = $request->getFlash();
try {
$settings = $form->getEntityRepository()->readSettings();
$settings = $form->getSettingsRepository()->readSettings();
$syncTask->updateDatabase($settings->getGeoliteLicenseKey() ?? '');
$flash->addMessage(__('Changes saved.'), Flash::SUCCESS);
} catch (Exception $e) {

View File

@ -2,6 +2,7 @@
namespace App\Controller\Api\Admin;
use App\Controller\Api\AbstractApiCrudController;
use App\Entity;
use App\Exception\ValidationException;
use App\Http\Response;
@ -12,25 +13,18 @@ use Psr\Http\Message\ResponseInterface;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class SettingsController
class SettingsController extends AbstractApiCrudController
{
protected EntityManagerInterface $em;
protected Serializer $serializer;
protected ValidatorInterface $validator;
protected Entity\Repository\SettingsRepository $settingsRepo;
public function __construct(
EntityManagerInterface $em,
Entity\Repository\SettingsRepository $settingsRepo,
Serializer $serializer,
ValidatorInterface $validator
ValidatorInterface $validator,
Entity\Repository\SettingsRepository $settingsRepo
) {
$this->em = $em;
$this->serializer = $serializer;
$this->validator = $validator;
parent::__construct($em, $serializer, $validator);
$this->settingsRepo = $settingsRepo;
}
@ -51,7 +45,7 @@ class SettingsController
public function listAction(ServerRequest $request, Response $response): ResponseInterface
{
$settings = $this->settingsRepo->readSettings();
return $response->withJson($this->serializer->normalize($settings, null));
return $response->withJson($this->toArray($settings));
}
/**
@ -75,7 +69,8 @@ class SettingsController
*/
public function updateAction(ServerRequest $request, Response $response): ResponseInterface
{
$this->settingsRepo->writeSettings($request->getParsedBody());
$settings = $this->settingsRepo->readSettings();
$this->editRecord($request->getParsedBody(), $settings);
return $response->withJson(new Entity\Api\Status());
}

View File

@ -10,38 +10,41 @@ class Settings extends AbstractFixture
{
public function load(ObjectManager $em): void
{
$settings = [
'baseUrl' => getenv('INIT_BASE_URL') ?? 'docker.local',
'instanceName' => getenv('INIT_INSTANCE_NAME') ?? 'local test',
'geoliteLicenseKey' => getenv('INIT_GEOLITE_LICENSE_KEY') ?? '',
'setupCompleteTime' => time(),
'preferBrowserUrl' => true,
'useRadioProxy' => true,
'checkForUpdates' => true,
'externalIp' => '127.0.0.1',
'enableAdvancedFeatures' => true,
];
$existingSettings = $em->getRepository(Entity\Settings::class)->findAll();
foreach ($existingSettings as $row) {
$em->remove($row);
}
$settings = new Entity\Settings();
$settings->setBaseUrl(getenv('INIT_BASE_URL') ?? 'docker.local');
$settings->setInstanceName(getenv('INIT_INSTANCE_NAME') ?? 'local test');
$settings->setGeoliteLicenseKey(getenv('INIT_GEOLITE_LICENSE_KEY') ?? '');
$settings->setSetupCompleteTime(time());
$settings->setPreferBrowserUrl(true);
$settings->setUseRadioProxy(true);
$settings->setCheckForUpdates(true);
$settings->setExternalIp('127.0.0.1');
$settings->setEnableAdvancedFeatures(true);
$isDemoMode = (!empty(getenv('INIT_DEMO_API_KEY') ?? ''));
if ($isDemoMode) {
$settings['analytics'] = Entity\Analytics::LEVEL_NO_IP;
$settings['checkForUpdates'] = false;
$settings['publicCustomJs'] = <<<'JS'
$settings->setAnalytics(Entity\Analytics::LEVEL_NO_IP);
$settings->setCheckForUpdates(false);
$settings->setPublicCustomJs(
<<<'JS'
$(function() {
if ($('body').hasClass('login-content')) {
$('input[name="username"]').val('demo@azuracast.com');
$('input[name="password"]').val('demo');
}
});
JS;
}
foreach ($settings as $setting_key => $setting_value) {
$record = new Entity\SettingsTable($setting_key);
$record->setSettingValue($setting_value);
$em->persist($record);
JS
);
}
$em->persist($settings);
$em->flush();
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Entity\Migration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20210419033245 extends AbstractMigration
{
public function getDescription(): string
{
return 'Settings entity migration, part 1';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE new_settings (app_unique_identifier CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\', base_url VARCHAR(255) DEFAULT NULL, instance_name VARCHAR(255) DEFAULT NULL, prefer_browser_url TINYINT(1) NOT NULL, use_radio_proxy TINYINT(1) NOT NULL, history_keep_days SMALLINT NOT NULL, always_use_ssl TINYINT(1) NOT NULL, api_access_control VARCHAR(255) DEFAULT NULL, enable_websockets TINYINT(1) NOT NULL, analytics VARCHAR(50) DEFAULT NULL, check_for_updates TINYINT(1) NOT NULL, update_results LONGTEXT DEFAULT NULL COMMENT \'(DC2Type:json)\', update_last_run INT NOT NULL, public_theme VARCHAR(50) DEFAULT NULL, hide_album_art TINYINT(1) NOT NULL, homepage_redirect_url VARCHAR(255) DEFAULT NULL, default_album_art_url VARCHAR(255) DEFAULT NULL, use_external_album_art_when_processing_media TINYINT(1) NOT NULL, use_external_album_art_in_apis TINYINT(1) NOT NULL, last_fm_api_key VARCHAR(255) DEFAULT NULL, hide_product_name TINYINT(1) NOT NULL, public_custom_css LONGTEXT DEFAULT NULL, public_custom_js LONGTEXT DEFAULT NULL, internal_custom_css LONGTEXT DEFAULT NULL, backup_enabled TINYINT(1) NOT NULL, backup_time_code VARCHAR(4) DEFAULT NULL, backup_exclude_media TINYINT(1) NOT NULL, backup_keep_copies SMALLINT NOT NULL, backup_storage_location INT DEFAULT NULL, backup_last_run INT NOT NULL, backup_last_result LONGTEXT DEFAULT NULL, backup_last_output LONGTEXT DEFAULT NULL, setup_complete_time INT NOT NULL, nowplaying LONGTEXT DEFAULT NULL COMMENT \'(DC2Type:json)\', sync_nowplaying_last_run INT NOT NULL, sync_short_last_run INT NOT NULL, sync_medium_last_run INT NOT NULL, sync_long_last_run INT NOT NULL, external_ip VARCHAR(45) DEFAULT NULL, geolite_license_key VARCHAR(255) DEFAULT NULL, geolite_last_run INT NOT NULL, enable_advanced_features TINYINT(1) NOT NULL, mail_enabled TINYINT(1) NOT NULL, mail_sender_name VARCHAR(255) DEFAULT NULL, mail_sender_email VARCHAR(255) DEFAULT NULL, mail_smtp_host VARCHAR(255) DEFAULT NULL, mail_smtp_port SMALLINT NOT NULL, mail_smtp_username VARCHAR(255) DEFAULT NULL, mail_smtp_password VARCHAR(255) DEFAULT NULL, mail_smtp_secure TINYINT(1) NOT NULL, avatar_service VARCHAR(25) DEFAULT NULL, avatar_default_url VARCHAR(255) DEFAULT NULL, PRIMARY KEY(app_unique_identifier)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_general_ci` ENGINE = InnoDB');
$this->addSql('RENAME TABLE settings TO old_settings');
}
public function down(Schema $schema): void
{
$this->addSql('DROP TABLE new_settings');
$this->addSql('RENAME TABLE old_settings TO settings');
}
}

View File

@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
namespace App\Entity\Migration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
use Ramsey\Uuid\Uuid;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20210419043231 extends AbstractMigration
{
public function getDescription(): string
{
return 'Settings entity migration, part 2';
}
public function preUp(Schema $schema): void
{
$oldSettings = [];
$oldSettingsRaw = $this->connection->fetchAllAssociative(
'SELECT setting_key, setting_value FROM old_settings'
);
foreach ($oldSettingsRaw as $row) {
$key = $row['setting_key'];
$key = preg_replace('~(?<=\\w)([A-Z])~u', '_$1', $key);
$key = mb_strtolower($key);
$value = $row['setting_value'];
$value = ($value === null || $value === '')
? null
: json_decode($value, true);
$oldSettings[$key] = $value;
}
$newSettings = [];
$appUniqueIdentifier = $oldSettings['app_unique_identifier'] ?? null;
if (empty($appUniqueIdentifier)) {
$appUniqueIdentifier = Uuid::uuid4()->toString();
}
$newSettings['app_unique_identifier'] = $appUniqueIdentifier;
$textFields = [
'public_custom_css',
'public_custom_js',
'internal_custom_css',
'backup_last_result',
'backup_last_output',
];
$stringFields = [
'base_url' => 255,
'instance_name' => 255,
'api_access_control' => 255,
'analytics' => 50,
'public_theme' => 50,
'homepage_redirect_url' => 255,
'default_album_art_url' => 255,
'last_fm_api_key' => 255,
'backup_time_code' => 4,
'external_ip' => 45,
'geolite_license_key' => 255,
'mail_sender_name' => 255,
'mail_sender_email' => 255,
'mail_smtp_host' => 255,
'mail_smtp_username' => 255,
'mail_smtp_password' => 255,
'avatar_service' => 25,
'avatar_default_url' => 255,
];
$boolFields = [
'prefer_browser_url',
'use_radio_proxy',
'always_use_ssl',
'enable_websockets',
'check_for_updates',
'hide_album_art',
'use_external_album_art_when_processing_media',
'use_external_album_art_in_apis',
'hide_product_name',
'backup_enabled',
'backup_exclude_media',
'enable_advanced_features',
'mail_enabled',
'mail_smtp_secure',
];
$smallIntFields = [
'history_keep_days',
'backup_keep_copies',
'mail_smtp_port',
];
$intFields = [
'update_last_run',
'backup_storage_location',
'backup_last_run',
'setup_complete_time',
'sync_nowplaying_last_run',
'sync_short_last_run',
'sync_medium_last_run',
'sync_long_last_run',
'geolite_last_run',
];
foreach ($textFields as $field) {
$value = $oldSettings[$field] ?? null;
if (null === $value) {
continue;
}
$newSettings[$field] = $value;
}
foreach ($stringFields as $field => $length) {
$value = $oldSettings[$field] ?? null;
if (null === $value) {
continue;
}
$newSettings[$field] = mb_substr($value, 0, $length, 'UTF-8');
}
foreach ($boolFields as $field) {
$value = $oldSettings[$field] ?? null;
if (null === $value) {
$newSettings[$field] = 0;
continue;
}
$newSettings[$field] = $value ? 1 : 0;
}
foreach ($smallIntFields as $field) {
$value = $oldSettings[$field] ?? null;
if (null === $value) {
$newSettings[$field] = 0;
continue;
}
$value = (int)$value;
if ($value > 32767) {
$value = 32767;
}
$newSettings[$field] = $value;
}
foreach ($intFields as $field) {
$value = $oldSettings[$field] ?? null;
if (null === $value) {
$value = 0;
}
$newSettings[$field] = (int)$value;
}
$this->connection->executeQuery('DELETE FROM new_settings');
$this->connection->insert('new_settings', $newSettings);
}
public function up(Schema $schema): void
{
$this->addSql('RENAME TABLE new_settings TO settings');
$this->addSql('DROP TABLE old_settings');
}
public function down(Schema $schema): void
{
$this->addSql('RENAME TABLE settings TO new_settings');
$this->addSql('CREATE TABLE old_settings (setting_key VARCHAR(64) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_general_ci`, setting_value LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_general_ci` COMMENT \'(DC2Type:json)\', PRIMARY KEY(setting_key)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB COMMENT = \'\' ');
}
}

View File

@ -2,84 +2,44 @@
namespace App\Entity\Repository;
use App\Annotations\AuditLog\AuditIgnore;
use App\Doctrine\ReloadableEntityManagerInterface;
use App\Doctrine\Repository;
use App\Entity;
use App\Environment;
use App\Exception\ValidationException;
use Doctrine\Common\Annotations\Reader;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
use ReflectionObject;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class SettingsRepository extends Repository
{
protected static ?Entity\Settings $instance = null;
protected const CACHE_KEY = 'settings';
protected const CACHE_TTL = 600;
protected CacheInterface $cache;
protected ValidatorInterface $validator;
protected Reader $annotationReader;
protected string $entityClass = Entity\SettingsTable::class;
protected string $entityClass = Entity\Settings::class;
public function __construct(
ReloadableEntityManagerInterface $em,
Serializer $serializer,
Environment $environment,
LoggerInterface $logger,
CacheInterface $cache,
ValidatorInterface $validator,
Reader $annotationReader
ValidatorInterface $validator
) {
parent::__construct($em, $serializer, $environment, $logger);
$this->cache = $cache;
$this->validator = $validator;
$this->annotationReader = $annotationReader;
}
public function readSettings(bool $reload = false): Entity\Settings
public function readSettings(): Entity\Settings
{
if ($reload || null === self::$instance) {
self::$instance = $this->arrayToObject($this->readSettingsArray());
$settings = $this->repository->findOneBy([]);
if (!($settings instanceof Entity\Settings)) {
$settings = new Entity\Settings();
$this->em->persist($settings);
$this->em->flush();
}
return self::$instance;
}
public function clearSettingsInstance(): void
{
self::$instance = null;
}
/**
* @return mixed[]
*/
public function readSettingsArray(bool $reload = false): array
{
$allRecords = $this->cache->get(self::CACHE_KEY);
if ($reload || null === $allRecords) {
$allRecords = [];
foreach ($this->repository->findAll() as $record) {
/** @var Entity\SettingsTable $record */
$allRecords[$record->getSettingKey()] = $record->getSettingValue();
}
$this->cache->set(self::CACHE_KEY, $allRecords, self::CACHE_TTL);
}
return $allRecords;
return $settings;
}
/**
@ -88,7 +48,10 @@ class SettingsRepository extends Repository
public function writeSettings($settingsObj): void
{
if (is_array($settingsObj)) {
$settingsObj = $this->arrayToObject($settingsObj, $this->readSettings(true));
$settings = $this->readSettings();
$settings = $this->fromArray($settingsObj, $settings);
} else {
$settings = $settingsObj;
}
$errors = $this->validator->validate($settingsObj);
@ -98,102 +61,7 @@ class SettingsRepository extends Repository
throw $e;
}
$settings = $this->objectToArray($settingsObj);
$currentRecords = $this->repository->findAll();
$allRecords = [];
foreach ($currentRecords as $record) {
/** @var Entity\SettingsTable $record */
$allRecords[$record->getSettingKey()] = $record;
}
$changes = [];
foreach ($settings as $settingKey => $settingValue) {
if (isset($allRecords[$settingKey])) {
$record = $allRecords[$settingKey];
$prev = $record->getSettingValue();
} else {
$record = new Entity\SettingsTable($settingKey);
$prev = null;
}
$record->setSettingValue($settingValue);
$this->em->persist($record);
// Include change in audit log.
if ($prev !== $settingValue) {
$changes[$settingKey] = [$prev, $settingValue];
}
}
if (!empty($changes)) {
// Ignore any properties tagged with "AuditIgnore".
$reflectionClass = new ReflectionObject($settingsObj);
$loggableChanges = array_filter(
$changes,
function ($settingKey) use ($reflectionClass) {
$reflectionProp = $reflectionClass->getProperty($settingKey);
$ignoreAnnotation = $this->annotationReader->getPropertyAnnotation(
$reflectionProp,
AuditIgnore::class
);
return (null === $ignoreAnnotation);
},
ARRAY_FILTER_USE_KEY
);
if (!empty($loggableChanges)) {
$auditLog = new Entity\AuditLog(
Entity\AuditLog::OPER_UPDATE,
Entity\SettingsTable::class,
'Settings',
null,
null,
$loggableChanges
);
$this->em->persist($auditLog);
}
}
$this->em->persist($settings);
$this->em->flush();
$this->cache->delete(self::CACHE_KEY);
}
/**
* @param Entity\Settings $settings
*
* @return mixed[]
*/
protected function objectToArray(Entity\Settings $settings): array
{
$reflectionClass = new ReflectionObject($settings);
$array = $this->serializer->normalize($settings, null);
// Prevent serializer from returning things that aren't actually properties on the class.
return array_filter(
$array,
function ($key) use ($reflectionClass) {
return $reflectionClass->hasProperty($key);
},
ARRAY_FILTER_USE_KEY
);
}
protected function arrayToObject(array $settings, ?Entity\Settings $existingSettings = null): Entity\Settings
{
$settings = array_filter(
$settings,
function ($value) {
return null !== $value;
}
);
$context = [];
if (null !== $existingSettings) {
$context[ObjectNormalizer::OBJECT_TO_POPULATE] = $existingSettings;
}
return $this->serializer->denormalize($settings, Entity\Settings::class, null, $context);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,51 +0,0 @@
<?php
/** @noinspection PhpMissingFieldTypeInspection */
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="settings")
* @ORM\Entity()
*/
class SettingsTable
{
/**
* @ORM\Column(name="setting_key", type="string", length=64)
* @ORM\Id
* @ORM\GeneratedValue(strategy="NONE")
* @var string
*/
protected $setting_key;
/**
* @ORM\Column(name="setting_value", type="json", nullable=true)
* @var mixed
*/
protected $setting_value;
public function __construct(string $setting_key)
{
$this->setting_key = $setting_key;
}
public function getSettingKey(): string
{
return $this->setting_key;
}
/**
* @return mixed
*/
public function getSettingValue()
{
return $this->setting_value;
}
public function setSettingValue($setting_value): void
{
$this->setting_value = $setting_value;
}
}

View File

@ -6,34 +6,32 @@ use App\Entity;
use App\Environment;
use App\Http\ServerRequest;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Validator\Validator\ValidatorInterface;
abstract class AbstractSettingsForm extends Form
abstract class AbstractSettingsForm extends EntityForm
{
protected EntityManagerInterface $em;
protected Entity\Repository\SettingsRepository $settingsRepo;
protected Environment $environment;
public function __construct(
EntityManagerInterface $em,
Serializer $serializer,
ValidatorInterface $validator,
Entity\Repository\SettingsRepository $settingsRepo,
Environment $environment,
array $formConfig
array $options = [],
?array $defaults = null
) {
parent::__construct($formConfig);
parent::__construct($em, $serializer, $validator, $options, $defaults);
$this->em = $em;
$this->environment = $environment;
$this->entityClass = Entity\Settings::class;
$this->settingsRepo = $settingsRepo;
$this->environment = $environment;
}
public function getEntityManager(): EntityManagerInterface
{
return $this->em;
}
public function getEntityRepository(): Entity\Repository\SettingsRepository
public function getSettingsRepository(): Entity\Repository\SettingsRepository
{
return $this->settingsRepo;
}
@ -43,26 +41,19 @@ abstract class AbstractSettingsForm extends Form
return $this->environment;
}
public function process(ServerRequest $request): bool
/** @inheritDoc */
public function process(ServerRequest $request, $record = null)
{
// Populate the form with existing values (if they exist).
$defaults = $this->settingsRepo->readSettingsArray();
if (null === $record) {
$record = $this->settingsRepo->readSettings();
}
// Use current URI from request if the base URL isn't set.
if (empty($defaults['baseUrl'])) {
/** @var Entity\Settings $record */
if (empty($record->getBaseUrl())) {
$currentUri = $request->getUri()->withPath('');
$defaults['baseUrl'] = (string)$currentUri;
$record->setBaseUrl((string)$currentUri);
}
$this->populate($defaults);
// Handle submission.
if ('POST' === $request->getMethod() && $this->isValid($request->getParsedBody())) {
$data = $this->getValues();
$this->settingsRepo->writeSettings($data);
return true;
}
return false;
return parent::process($request, $record);
}
}

View File

@ -6,14 +6,18 @@ use App\Config;
use App\Entity;
use App\Environment;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class BackupSettingsForm extends AbstractSettingsForm
{
public function __construct(
EntityManagerInterface $em,
Serializer $serializer,
ValidatorInterface $validator,
Entity\Repository\SettingsRepository $settingsRepo,
Entity\Repository\StorageLocationRepository $storageLocationRepo,
Environment $environment,
Entity\Repository\StorageLocationRepository $storageLocationRepo,
Config $config
) {
$formConfig = $config->get(
@ -28,11 +32,6 @@ class BackupSettingsForm extends AbstractSettingsForm
]
);
parent::__construct(
$em,
$settingsRepo,
$environment,
$formConfig
);
parent::__construct($em, $serializer, $validator, $settingsRepo, $environment, $formConfig);
}
}

View File

@ -6,11 +6,15 @@ use App\Config;
use App\Entity;
use App\Environment;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class BrandingSettingsForm extends AbstractSettingsForm
{
public function __construct(
EntityManagerInterface $em,
Serializer $serializer,
ValidatorInterface $validator,
Entity\Repository\SettingsRepository $settingsRepo,
Environment $environment,
Config $config
@ -22,11 +26,6 @@ class BrandingSettingsForm extends AbstractSettingsForm
]
);
parent::__construct(
$em,
$settingsRepo,
$environment,
$formConfig
);
parent::__construct($em, $serializer, $validator, $settingsRepo, $environment, $formConfig);
}
}

View File

@ -7,6 +7,8 @@ use App\Entity;
use App\Environment;
use App\Sync\Task\UpdateGeoLiteTask;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class GeoLiteSettingsForm extends AbstractSettingsForm
{
@ -14,6 +16,8 @@ class GeoLiteSettingsForm extends AbstractSettingsForm
public function __construct(
EntityManagerInterface $em,
Serializer $serializer,
ValidatorInterface $validator,
Entity\Repository\SettingsRepository $settingsRepo,
Environment $environment,
Config $config,
@ -21,12 +25,7 @@ class GeoLiteSettingsForm extends AbstractSettingsForm
) {
$formConfig = $config->get('forms/install_geolite');
parent::__construct(
$em,
$settingsRepo,
$environment,
$formConfig
);
parent::__construct($em, $serializer, $validator, $settingsRepo, $environment, $formConfig);
$this->syncTask = $syncTask;
}

View File

@ -8,11 +8,15 @@ use App\Environment;
use App\Http\ServerRequest;
use App\Version;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class SettingsForm extends AbstractSettingsForm
{
public function __construct(
EntityManagerInterface $em,
Serializer $serializer,
ValidatorInterface $validator,
Entity\Repository\SettingsRepository $settingsRepo,
Environment $environment,
Version $version,
@ -26,18 +30,14 @@ class SettingsForm extends AbstractSettingsForm
]
);
parent::__construct(
$em,
$settingsRepo,
$environment,
$formConfig
);
parent::__construct($em, $serializer, $validator, $settingsRepo, $environment, $formConfig);
}
public function process(ServerRequest $request): bool
/** @inheritDoc */
public function process(ServerRequest $request, $record = null)
{
if ('https' !== $request->getUri()->getScheme()) {
$alwaysUseSsl = $this->getField('alwaysUseSsl');
$alwaysUseSsl = $this->getField('always_use_ssl');
$alwaysUseSsl->setAttribute('disabled', 'disabled');
$alwaysUseSsl->setOption(
'description',
@ -45,6 +45,6 @@ class SettingsForm extends AbstractSettingsForm
);
}
return parent::process($request);
return parent::process($request, $record);
}
}

View File

@ -22,7 +22,7 @@ class Router implements RouterInterface
protected ?ServerRequestInterface $currentRequest = null;
protected Entity\Settings $settings;
protected Entity\Repository\SettingsRepository $settingsRepo;
public function __construct(
Environment $environment,
@ -30,7 +30,7 @@ class Router implements RouterInterface
Entity\Repository\SettingsRepository $settingsRepo
) {
$this->environment = $environment;
$this->settings = $settingsRepo->readSettings();
$this->settingsRepo = $settingsRepo;
$this->routeParser = $app->getRouteCollector()->getRouteParser();
}
@ -155,7 +155,9 @@ class Router implements RouterInterface
public function getBaseUrl(bool $useRequest = true): UriInterface
{
$settingsBaseUrl = $this->settings->getBaseUrl();
$settings = $this->settingsRepo->readSettings();
$settingsBaseUrl = $settings->getBaseUrl();
if (!empty($settingsBaseUrl)) {
if (strpos($settingsBaseUrl, 'http') !== 0) {
$settingsBaseUrl = 'http://' . $settingsBaseUrl;
@ -166,7 +168,7 @@ class Router implements RouterInterface
$baseUrl = new Uri('');
}
$useHttps = $this->settings->getAlwaysUseSsl();
$useHttps = $settings->getAlwaysUseSsl();
if ($useRequest && $this->currentRequest instanceof ServerRequestInterface) {
$currentUri = $this->currentRequest->getUri();
@ -175,7 +177,7 @@ class Router implements RouterInterface
$useHttps = true;
}
$preferBrowserUrl = $this->settings->getPreferBrowserUrl();
$preferBrowserUrl = $settings->getPreferBrowserUrl();
if ($preferBrowserUrl || $baseUrl->getHost() === '') {
$ignoredHosts = ['web', 'nginx', 'localhost'];
if (!in_array($currentUri->getHost(), $ignoredHosts, true)) {

View File

@ -1,32 +0,0 @@
<?php
namespace App\MessageQueue;
use App\Entity\Repository\SettingsRepository;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent;
class ReloadSettingsMiddleware implements EventSubscriberInterface
{
protected SettingsRepository $settingsRepo;
public function __construct(SettingsRepository $settingsRepo)
{
$this->settingsRepo = $settingsRepo;
}
/**
* @inheritDoc
*/
public static function getSubscribedEvents()
{
return [
WorkerMessageReceivedEvent::class => 'resetSettings',
];
}
public function resetSettings(WorkerMessageReceivedEvent $event): void
{
$this->settingsRepo->clearSettingsInstance();
}
}

View File

@ -8,7 +8,6 @@ use App\Version;
use Exception;
use GuzzleHttp\Client;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
class AzuraCastCentral
{
@ -59,6 +58,13 @@ class AzuraCastCentral
$request_body['release'] = Version::FALLBACK_VERSION;
}
$this->logger->debug(
'Update request body',
[
'body' => $request_body,
]
);
try {
$response = $this->httpClient->request(
'POST',
@ -80,16 +86,7 @@ class AzuraCastCentral
public function getUniqueIdentifier(): string
{
$settings = $this->settingsRepo->readSettings();
$appUuid = $settings->getAppUniqueIdentifier();
if (empty($appUuid)) {
$appUuid = Uuid::uuid4()->toString();
$settings->setAppUniqueIdentifier($appUuid);
$this->settingsRepo->writeSettings($settings);
}
return $appUuid;
return (string)$settings->getAppUniqueIdentifier();
}
/**

View File

@ -145,7 +145,7 @@ class Runner
gc_collect_cycles();
}
$settings = $this->settingsRepo->readSettings(true);
$settings = $this->settingsRepo->readSettings();
$settings->updateSyncLastRunTime($type);
$this->settingsRepo->writeSettings($settings);
} finally {

View File

@ -102,7 +102,7 @@ class NowPlayingTask extends AbstractTask implements EventSubscriberInterface
$this->cache->set('nowplaying', $nowplaying, 120);
$settings = $this->settingsRepo->readSettings(true);
$settings = $this->settingsRepo->readSettings();
$settings->setNowplaying($nowplaying);
$this->settingsRepo->writeSettings($settings);
}

View File

@ -41,7 +41,7 @@ class RunBackupTask extends AbstractTask
public function __invoke(Message\AbstractMessage $message): void
{
if ($message instanceof Message\BackupMessage) {
$settings = $this->settingsRepo->readSettings(true);
$settings = $this->settingsRepo->readSettings();
$settings->updateBackupLastRun();
$this->settingsRepo->writeSettings($settings);
@ -53,7 +53,7 @@ class RunBackupTask extends AbstractTask
$message->storageLocationId
);
$settings = $this->settingsRepo->readSettings(true);
$settings = $this->settingsRepo->readSettings();
$settings->setBackupLastResult($result_code);
$settings->setBackupLastOutput($result_output);
$this->settingsRepo->writeSettings($settings);

View File

@ -62,7 +62,7 @@ class UpdateGeoLiteTask extends AbstractTask
);
}
$settings = $this->settingsRepo->readSettings(true);
$settings = $this->settingsRepo->readSettings();
$settings->updateGeoliteLastRun();
$this->settingsRepo->writeSettings($settings);
}

View File

@ -60,7 +60,7 @@ class LocalWebhookHandler
$this->cache->set('nowplaying', $np_new, 120);
$settings = $this->settingsRepo->readSettings(true);
$settings = $this->settingsRepo->readSettings();
$settings->setNowplaying($np_new);
$this->settingsRepo->writeSettings($settings);
}

View File

@ -48,12 +48,12 @@ abstract class CestAbstract
{
$I->wantTo('Start with an incomplete setup.');
$settings = $this->settingsRepo->readSettings(true);
$this->_cleanTables();
$settings = $this->settingsRepo->readSettings();
$settings->setSetupCompleteTime(0);
$this->settingsRepo->writeSettings($settings);
$this->_cleanTables();
}
protected function setupComplete(FunctionalTester $I): void
@ -93,9 +93,9 @@ abstract class CestAbstract
$this->test_station = $this->stationRepo->create($test_station);
// Set settings.
$settings = $this->settingsRepo->readSettings(true);
$settings = $this->settingsRepo->readSettings();
$settings->updateSetupComplete();
$settings->setBaseUrl('localhost');
$settings->setBaseUrl('http://localhost');
$this->settingsRepo->writeSettings($settings);
}
@ -136,11 +136,14 @@ abstract class CestAbstract
Entity\User::class,
Entity\Role::class,
Entity\Station::class,
Entity\Settings::class,
];
foreach ($clean_tables as $clean_table) {
$this->em->createQuery('DELETE FROM ' . $clean_table . ' t')->execute();
}
$this->em->clear();
}
protected function login(FunctionalTester $I): void