Merge branch 'origin/master' into Weblate.
[phpmyadmin.git] / src / UserPreferences.php
blobd0125b2517b4c8338998878ebb3b4733788a8564
1 <?php
3 declare(strict_types=1);
5 namespace PhpMyAdmin;
7 use PhpMyAdmin\Config\ConfigFile;
8 use PhpMyAdmin\Config\Forms\User\UserFormList;
9 use PhpMyAdmin\ConfigStorage\Relation;
10 use PhpMyAdmin\Dbal\Connection;
11 use PhpMyAdmin\Identifiers\DatabaseName;
13 use function __;
14 use function array_flip;
15 use function array_merge;
16 use function htmlspecialchars;
17 use function http_build_query;
18 use function is_array;
19 use function is_int;
20 use function is_numeric;
21 use function is_string;
22 use function json_decode;
23 use function json_encode;
24 use function str_contains;
25 use function time;
26 use function urlencode;
28 /**
29 * Functions for displaying user preferences pages
31 class UserPreferences
33 public function __construct(
34 private readonly DatabaseInterface $dbi,
35 private readonly Relation $relation,
36 private readonly Template $template,
37 ) {
40 /**
41 * Common initialization for user preferences modification pages
43 * @param ConfigFile $cf Config file instance
45 public function pageInit(ConfigFile $cf): void
47 $formsAllKeys = UserFormList::getFields();
48 $cf->resetConfigData(); // start with a clean instance
49 $cf->setAllowedKeys($formsAllKeys);
50 $cf->setCfgUpdateReadMapping(
51 ['Server/hide_db' => 'Servers/1/hide_db', 'Server/only_db' => 'Servers/1/only_db'],
53 $cf->updateWithGlobalConfig(Config::getInstance()->settings);
56 /**
57 * Loads user preferences
59 * Returns an array:
60 * * config_data - path => value pairs
61 * * mtime - last modification time
62 * * type - 'db' (config read from pmadb) or 'session' (read from user session)
64 * @psalm-return array{config_data: mixed[], mtime: int, type: 'session'|'db'}
66 public function load(): array
68 $relationParameters = $this->relation->getRelationParameters();
69 if ($relationParameters->userPreferencesFeature === null) {
70 // no pmadb table, use session storage
71 if (! isset($_SESSION['userconfig']) || ! is_array($_SESSION['userconfig'])) {
72 $_SESSION['userconfig'] = ['db' => [], 'ts' => time()];
75 $configData = $_SESSION['userconfig']['db'] ?? null;
76 $timestamp = $_SESSION['userconfig']['ts'] ?? null;
78 return [
79 'config_data' => is_array($configData) ? $configData : [],
80 'mtime' => is_int($timestamp) ? $timestamp : time(),
81 'type' => 'session',
85 // load configuration from pmadb
86 $queryTable = Util::backquote($relationParameters->userPreferencesFeature->database) . '.'
87 . Util::backquote($relationParameters->userPreferencesFeature->userConfig);
88 $query = 'SELECT `config_data`, UNIX_TIMESTAMP(`timevalue`) ts'
89 . ' FROM ' . $queryTable
90 . ' WHERE `username` = '
91 . $this->dbi->quoteString((string) $relationParameters->user);
92 $row = $this->dbi->fetchSingleRow($query, DatabaseInterface::FETCH_ASSOC, Connection::TYPE_CONTROL);
93 if (! is_array($row) || ! isset($row['config_data']) || ! isset($row['ts'])) {
94 return ['config_data' => [], 'mtime' => time(), 'type' => 'db'];
97 $configData = is_string($row['config_data']) ? json_decode($row['config_data'], true) : [];
99 return [
100 'config_data' => is_array($configData) ? $configData : [],
101 'mtime' => is_numeric($row['ts']) ? (int) $row['ts'] : time(),
102 'type' => 'db',
107 * Saves user preferences
109 * @param mixed[] $configArray configuration array
111 * @return true|Message
113 public function save(array $configArray): bool|Message
115 $relationParameters = $this->relation->getRelationParameters();
116 $server = $GLOBALS['server'] ?? Config::getInstance()->settings['ServerDefault'];
117 $cacheKey = 'server_' . $server;
118 if (
119 $relationParameters->userPreferencesFeature === null
120 || $relationParameters->user === null
121 || $relationParameters->db === null
123 // no pmadb table, use session storage
124 $_SESSION['userconfig'] = ['db' => $configArray, 'ts' => time()];
125 if (isset($_SESSION['cache'][$cacheKey]['userprefs'])) {
126 unset($_SESSION['cache'][$cacheKey]['userprefs']);
129 return true;
132 // save configuration to pmadb
133 $queryTable = Util::backquote($relationParameters->userPreferencesFeature->database) . '.'
134 . Util::backquote($relationParameters->userPreferencesFeature->userConfig);
135 $query = 'SELECT `username` FROM ' . $queryTable
136 . ' WHERE `username` = '
137 . $this->dbi->quoteString($relationParameters->user);
139 $hasConfig = $this->dbi->fetchValue($query, 0, Connection::TYPE_CONTROL);
140 $configData = json_encode($configArray);
141 if ($hasConfig) {
142 $query = 'UPDATE ' . $queryTable
143 . ' SET `timevalue` = NOW(), `config_data` = '
144 . $this->dbi->quoteString($configData)
145 . ' WHERE `username` = '
146 . $this->dbi->quoteString($relationParameters->user);
147 } else {
148 $query = 'INSERT INTO ' . $queryTable
149 . ' (`username`, `timevalue`,`config_data`) '
150 . 'VALUES ('
151 . $this->dbi->quoteString($relationParameters->user) . ', NOW(), '
152 . $this->dbi->quoteString($configData) . ')';
155 if (isset($_SESSION['cache'][$cacheKey]['userprefs'])) {
156 unset($_SESSION['cache'][$cacheKey]['userprefs']);
159 if (! $this->dbi->tryQuery($query, Connection::TYPE_CONTROL)) {
160 $message = Message::error(__('Could not save configuration'));
161 $message->addMessage(
162 Message::error($this->dbi->getError(Connection::TYPE_CONTROL)),
163 '<br><br>',
165 if (! $this->hasAccessToDatabase($relationParameters->db)) {
167 * When phpMyAdmin cached the configuration storage parameters, it checked if the database can be
168 * accessed, so if it could not be accessed anymore, then the cache must be cleared as it's out of date.
170 $message->addMessage(Message::error(htmlspecialchars(
171 __('The phpMyAdmin configuration storage database could not be accessed.'),
172 )), '<br><br>');
175 return $message;
178 return true;
181 private function hasAccessToDatabase(DatabaseName $database): bool
183 $query = 'SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '
184 . $this->dbi->quoteString($database->getName());
185 if (Config::getInstance()->selectedServer['DisableIS']) {
186 $query = 'SHOW DATABASES LIKE '
187 . $this->dbi->quoteString(
188 $this->dbi->escapeMysqlWildcards($database->getName()),
192 return (bool) $this->dbi->fetchSingleRow($query, 'ASSOC', Connection::TYPE_CONTROL);
196 * Returns a user preferences array filtered by $cfg['UserprefsDisallow']
197 * (exclude list) and keys from user preferences form (allow list)
199 * @param mixed[] $configData path => value pairs
201 * @return mixed[]
203 public function apply(array $configData): array
205 $cfg = [];
206 $excludeList = array_flip(Config::getInstance()->settings['UserprefsDisallow']);
207 $allowList = array_flip(UserFormList::getFields());
208 // allow some additional fields which are custom handled
209 $allowList['ThemeDefault'] = true;
210 $allowList['lang'] = true;
211 $allowList['Server/hide_db'] = true;
212 $allowList['Server/only_db'] = true;
213 $allowList['2fa'] = true;
214 foreach ($configData as $path => $value) {
215 if (! isset($allowList[$path]) || isset($excludeList[$path])) {
216 continue;
219 Core::arrayWrite($path, $cfg, $value);
222 return $cfg;
226 * Updates one user preferences option (loads and saves to database).
228 * No validation is done!
230 * @param string $path configuration
231 * @param mixed $value value
232 * @param mixed $defaultValue default value
234 * @return true|Message
236 public function persistOption(string $path, mixed $value, mixed $defaultValue): bool|Message
238 $prefs = $this->load();
239 if ($value === $defaultValue) {
240 if (! isset($prefs['config_data'][$path])) {
241 return true;
244 unset($prefs['config_data'][$path]);
245 } else {
246 $prefs['config_data'][$path] = $value;
249 return $this->save($prefs['config_data']);
253 * Redirects after saving new user preferences
255 * @param string $fileName Filename
256 * @param mixed[]|null $params URL parameters
257 * @param string|null $hash Hash value
259 public function redirect(
260 string $fileName,
261 array|null $params = null,
262 string|null $hash = null,
263 ): void {
264 // redirect
265 $urlParams = ['saved' => 1];
266 if (is_array($params)) {
267 $urlParams = array_merge($params, $urlParams);
270 if ($hash !== null && $hash !== '') {
271 $hash = '#' . urlencode($hash);
274 ResponseRenderer::getInstance()->redirect(
275 './' . $fileName . Url::getCommonRaw($urlParams, ! str_contains($fileName, '?') ? '?' : '&') . $hash,
280 * Shows form which allows to quickly load
281 * settings stored in browser's local storage
283 public function autoloadGetHeader(): string
285 if (isset($_REQUEST['prefs_autoload']) && $_REQUEST['prefs_autoload'] === 'hide') {
286 $_SESSION['userprefs_autoload'] = true;
288 return '';
291 $returnUrl = '?' . http_build_query($_GET, '', '&');
293 return $this->template->render('preferences/autoload', [
294 'hidden_inputs' => Url::getHiddenInputs(),
295 'return_url' => $returnUrl,