Skip to content

Commit

Permalink
Implemented UserSettingStorage for AdminYard to store user settings i…
Browse files Browse the repository at this point in the history
…n the database.
  • Loading branch information
parpalak committed Aug 10, 2024
1 parent eef85d3 commit 390426b
Show file tree
Hide file tree
Showing 15 changed files with 411 additions and 53 deletions.
32 changes: 30 additions & 2 deletions _admin/db_update.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@

$s2_db->addIndex('articles', 'template_idx', ['template']);
$s2_db->dropIndex('articles', 'parent_id_idx');
$result = $s2_db->buildAndQuery([
$result = $s2_db->buildAndQuery([
'SELECT' => 'id',
'FROM' => 'users',
]);
Expand All @@ -235,7 +235,7 @@
$s2_db->addForeignKey('article_tag', 'fk_article', ['article_id'], 'articles', ['id'], 'CASCADE');
$s2_db->addForeignKey('article_tag', 'fk_tag', ['tag_id'], 'tags', ['id'], 'CASCADE');

$result = $s2_db->buildAndQuery([
$result = $s2_db->buildAndQuery([
'SELECT' => 'login',
'FROM' => 'users',
]);
Expand All @@ -249,6 +249,34 @@
$s2_db->addIndex('users_online', 'challenge_idx', ['challenge'], true);
}

if (S2_DB_REVISION < 21) {
$s2_db->createTable('user_settings', [
'FIELDS' => [
'user_id' => [
'datatype' => 'INT(10) UNSIGNED',
'allow_null' => false
],
'name' => [
'datatype' => 'VARCHAR(191)',
'allow_null' => false
],
'value' => [
'datatype' => 'TEXT',
'allow_null' => false
],
],
'PRIMARY KEY' => ['user_id', 'name'],
'FOREIGN KEYS' => [
'fk_user' => [
'columns' => ['user_id'],
'reference_table' => 'users',
'reference_columns' => ['id'],
'on_delete' => 'CASCADE',
],
]
]);
}

$s2_db->buildAndQuery([
'UPDATE' => 'config',
'SET' => 'value = \'' . S2_DB_LAST_REVISION . '\'',
Expand Down
2 changes: 1 addition & 1 deletion _admin/install.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;

define('S2_VERSION', '2.0dev');
define('S2_DB_REVISION', 20);
define('S2_DB_REVISION', 21);
define('MIN_PHP_VERSION', '8.2.0');

define('S2_ROOT', '../');
Expand Down
2 changes: 1 addition & 1 deletion _include/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ function collectParameters(): array
ini_set('session.cookie_httponly', true);
}

define('S2_DB_LAST_REVISION', 20);
define('S2_DB_LAST_REVISION', 21);
if (S2_DB_REVISION < S2_DB_LAST_REVISION) {
include __DIR__ . '/../_admin/db_update.php';
}
15 changes: 13 additions & 2 deletions _include/src/Admin/AdminExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use S2\AdminYard\Form\FormControlFactoryInterface;
use S2\AdminYard\Form\FormFactory;
use S2\AdminYard\MenuGenerator;
use S2\AdminYard\SettingStorage\SettingStorageInterface;
use S2\AdminYard\TemplateRenderer;
use S2\AdminYard\Transformer\ViewTransformer;
use S2\AdminYard\Translator;
Expand All @@ -32,6 +33,7 @@
use S2\Cms\AdminYard\CustomTemplateRenderer;
use S2\Cms\AdminYard\Form\CustomFormControlFactory;
use S2\Cms\AdminYard\Signal;
use S2\Cms\AdminYard\UserSettingStorage;
use S2\Cms\Config\DynamicConfigProvider;
use S2\Cms\Extensions\ExtensionManager;
use S2\Cms\Extensions\ExtensionManagerAdapter;
Expand Down Expand Up @@ -139,6 +141,13 @@ public function buildContainer(Container $container): void
);
});

$container->set(SettingStorageInterface::class, function (Container $container) {
return new UserSettingStorage(
$container->get(PermissionChecker::class),
$container->get(DbLayer::class),
);
}, [StatefulServiceInterface::class]);

$container->set(DynamicConfigFormBuilder::class, function (Container $container) {
return new DynamicConfigFormBuilder(
$container->get(PermissionChecker::class),
Expand All @@ -147,6 +156,7 @@ public function buildContainer(Container $container): void
$container->get(FormFactory::class),
$container->get(TemplateRenderer::class),
$container->get(RequestStack::class),
$container->get(SettingStorageInterface::class),
$container->getParameter('root_dir'),
...$container->getByTag(DynamicConfigFormExtenderInterface::class),
);
Expand Down Expand Up @@ -197,6 +207,7 @@ public function buildContainer(Container $container): void
$container->get(Translator::class),
$container->get(TemplateRenderer::class),
$container->get(FormFactory::class),
$container->get(SettingStorageInterface::class),
);
$adminPanel->setLogger($container->get(LoggerInterface::class));
return $adminPanel;
Expand Down Expand Up @@ -250,7 +261,7 @@ public function buildContainer(Container $container): void
$provider = $container->get(DynamicConfigProvider::class);
return new ArticleManager(
$container->get(DbLayer::class),
$container->get(RequestStack::class),
$container->get(SettingStorageInterface::class),
$container->get(PermissionChecker::class),
$provider->get('S2_ADMIN_NEW_POS') === '1',
$provider->get('S2_USE_HIERARCHY') === '1',
Expand Down Expand Up @@ -280,7 +291,7 @@ public function buildContainer(Container $container): void
$container->get(ExtensionManager::class),
$container->get(PermissionChecker::class),
$container->get(Translator::class),
$container->get(RequestStack::class),
$container->get(SettingStorageInterface::class),
$container->get(TemplateRenderer::class),
);
}, [AdminConfigExtenderInterface::class]);
Expand Down
2 changes: 1 addition & 1 deletion _include/src/Admin/Controller/CommentController.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public function rejectAction(Request $request): Response
}

// Borrow CSRF token from delete action
if ($this->getDeleteCsrfToken($primaryKey->toArray(), $request) !== $csrfToken) {
if ($this->getDeleteCsrfToken($primaryKey->toArray()) !== $csrfToken) {
return new JsonResponse(['errors' => [
$this->translator->trans('Unable to confirm security token. A likely cause for this is that some time passed between when you first entered the page and when you submitted the form. If that is the case and you would like to continue, submit the form again.')
]], Response::HTTP_UNPROCESSABLE_ENTITY);
Expand Down
20 changes: 11 additions & 9 deletions _include/src/Admin/DynamicConfigFormBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use S2\AdminYard\Database\TypeTransformer;
use S2\AdminYard\Form\FormFactory;
use S2\AdminYard\Form\FormParams;
use S2\AdminYard\SettingStorage\SettingStorageInterface;
use S2\AdminYard\TemplateRenderer;
use S2\AdminYard\Validator\Length;
use S2\AdminYard\Validator\Regex;
Expand Down Expand Up @@ -65,14 +66,15 @@ class DynamicConfigFormBuilder
private array $dynamicConfigFormExtenders;

public function __construct(
private readonly PermissionChecker $permissionChecker,
private readonly TranslatorInterface $translator,
private readonly TypeTransformer $typeTransformer,
private readonly FormFactory $formFactory,
private readonly TemplateRenderer $templateRenderer,
private readonly RequestStack $requestStack,
private readonly string $rootDir,
DynamicConfigFormExtenderInterface ...$dynamicConfigFormExtenders
private readonly PermissionChecker $permissionChecker,
private readonly TranslatorInterface $translator,
private readonly TypeTransformer $typeTransformer,
private readonly FormFactory $formFactory,
private readonly TemplateRenderer $templateRenderer,
private readonly RequestStack $requestStack,
private readonly SettingStorageInterface $settingStorage,
private readonly string $rootDir,
DynamicConfigFormExtenderInterface ...$dynamicConfigFormExtenders
) {
$this->dynamicConfigFormExtenders = $dynamicConfigFormExtenders;
}
Expand Down Expand Up @@ -112,7 +114,7 @@ public function transformConfigTable(string $entityName, array &$header, array &
$form = $this->formFactory->createEntityForm(new FormParams(
$entityName,
[$valFieldName => $field],
$this->requestStack->getMainRequest(),
$this->settingStorage,
'patch',
$row['primary_key'],
));
Expand Down
137 changes: 137 additions & 0 deletions _include/src/AdminYard/UserSettingStorage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php
/**
* @copyright 2024 Roman Parpalak
* @license http://opensource.org/licenses/MIT MIT
* @package S2
*/

declare(strict_types=1);

namespace S2\Cms\AdminYard;

use S2\AdminYard\SettingStorage\SettingStorageInterface;
use S2\Cms\Framework\StatefulServiceInterface;
use S2\Cms\Model\PermissionChecker;
use S2\Cms\Pdo\DbLayer;
use S2\Cms\Pdo\DbLayerException;

class UserSettingStorage implements SettingStorageInterface, StatefulServiceInterface
{
public const TABLE_NAME = 'user_settings';
private array $params = [];

public function __construct(
private readonly PermissionChecker $permissionChecker,
private readonly DbLayer $dbLayer,
) {
}

/**
* @throws DbLayerException
*/
public function has(string $string): bool
{
$this->ensureParamsAreLoaded();

return isset($this->params[$this->permissionChecker->getUserId()][$string]);
}

/**
* @throws DbLayerException
*/
public function get(string $key): array|string|int|float|bool|null
{
$this->ensureParamsAreLoaded();

return $this->params[$this->permissionChecker->getUserId()][$key] ?? null;
}

/**
* @throws DbLayerException
*/
public function set(string $key, array|string|int|float|bool|null $data): void
{
$this->ensureParamsAreLoaded();
$userId = $this->permissionChecker->getUserId();
if ($userId === null) {
throw new \RuntimeException('No authenticated user found.');
}

if (($this->params[$userId][$key] ?? null) === $data) {
return;
}

$this->params[$userId][$key] = $data;

try {
$this->dbLayer->buildAndQuery([
'UPSERT' => 'user_id, name, value',
'INTO' => self::TABLE_NAME,
'UNIQUE' => 'user_id, name',
'VALUES' => ':user_id, :name, :value',
], [
'user_id' => $userId,
'name' => $key,
'value' => json_encode($data, JSON_THROW_ON_ERROR),
]);
} catch (\JsonException $e) {
throw new \LogicException('Failed to encode user settings.', 0, $e);
}
}

/**
* @throws DbLayerException
*/
public function remove(string $key): void
{
$userId = $this->permissionChecker->getUserId();
if ($userId === null) {
throw new \RuntimeException('No authenticated user found.');
}

$this->dbLayer->buildAndQuery([
'DELETE' => self::TABLE_NAME,
'WHERE' => 'user_id = :user_id AND name = :name',
], [
'user_id' => $userId,
'name' => $key,
]);
}

/**
* @throws DbLayerException
*/
private function ensureParamsAreLoaded(): void
{
$userId = $this->permissionChecker->getUserId();
if ($userId === null) {
throw new \RuntimeException('No authenticated user found.');
}

if (isset($this->params[$userId])) {
return;
}

$result = $this->dbLayer->buildAndQuery([
'SELECT' => 'name, value',
'FROM' => self::TABLE_NAME,
'WHERE' => 'user_id = :user_id',
], [
'user_id' => $userId
]);

$this->params[$userId] = $result->fetchAll(\PDO::FETCH_KEY_PAIR);
foreach ($this->params[$userId] as $key => $value) {
try {
$this->params[$userId][$key] = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
throw new \LogicException('Failed to decode user settings.', 0, $e);
}
}
}

public function clearState(): void
{
$this->params = [];
}
}
16 changes: 8 additions & 8 deletions _include/src/Extensions/ExtensionManagerAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,22 @@
use S2\AdminYard\Config\AdminConfig;
use S2\AdminYard\Config\FieldConfig;
use S2\AdminYard\Form\FormParams;
use S2\AdminYard\SettingStorage\SettingStorageInterface;
use S2\AdminYard\TemplateRenderer;
use S2\AdminYard\Translator;
use S2\Cms\Admin\AdminConfigExtenderInterface;
use S2\Cms\Framework\Exception\AccessDeniedException;
use S2\Cms\Model\PermissionChecker;
use S2\Cms\Pdo\DbLayerException;
use Symfony\Component\HttpFoundation\RequestStack;

readonly class ExtensionManagerAdapter implements AdminConfigExtenderInterface
{
public function __construct(
private ExtensionManager $extensionManager,
private PermissionChecker $permissionChecker,
private Translator $translator,
private RequestStack $requestStack,
private TemplateRenderer $templateRenderer,
public function __construct(
private ExtensionManager $extensionManager,
private PermissionChecker $permissionChecker,
private Translator $translator,
private ?SettingStorageInterface $settingStorage,
private TemplateRenderer $templateRenderer,
) {
}

Expand Down Expand Up @@ -105,7 +105,7 @@ private function getCsrfToken(string $id): string
{
// This token is used for every action in the extension actions.
// I chose to use ACTION_DELETE since then it would be compatible with the AdminYard delete token.
$formParams = new FormParams('Extension', [], $this->requestStack->getMainRequest(), FieldConfig::ACTION_DELETE, ['id' => $id]);
$formParams = new FormParams('Extension', [], $this->settingStorage, FieldConfig::ACTION_DELETE, ['id' => $id]);

return $formParams->getCsrfToken();
}
Expand Down
14 changes: 7 additions & 7 deletions _include/src/Model/ArticleManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,20 @@

use S2\AdminYard\Config\FieldConfig;
use S2\AdminYard\Form\FormParams;
use S2\AdminYard\SettingStorage\SettingStorageInterface;
use S2\Cms\Framework\Exception\AccessDeniedException;
use S2\Cms\Framework\Exception\NotFoundException;
use S2\Cms\Pdo\DbLayer;
use S2\Cms\Pdo\DbLayerException;
use Symfony\Component\HttpFoundation\RequestStack;

readonly class ArticleManager
{
public function __construct(
private DbLayer $dbLayer,
private RequestStack $requestStack,
private PermissionChecker $permissionChecker,
private bool $newPositionOnTop,
private bool $useHierarchy,
private DbLayer $dbLayer,
private SettingStorageInterface $settingStorage,
private PermissionChecker $permissionChecker,
private bool $newPositionOnTop,
private bool $useHierarchy,
) {
}

Expand Down Expand Up @@ -345,7 +345,7 @@ public function getCsrfToken(int $id): string
{
// This token is used for every action in the tree management actions.
// I chose to use ACTION_DELETE since then it would be compatible with the AdminYard delete token.
$formParams = new FormParams('Article', [], $this->requestStack->getMainRequest(), FieldConfig::ACTION_DELETE, ['id' => (string)$id]);
$formParams = new FormParams('Article', [], $this->settingStorage, FieldConfig::ACTION_DELETE, ['id' => (string)$id]);

return $formParams->getCsrfToken();
}
Expand Down
Loading

0 comments on commit 390426b

Please sign in to comment.