3 declare(strict_types
=1);
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
;
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
;
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
;
26 use function urlencode
;
29 * Functions for displaying user preferences pages
33 public function __construct(
34 private readonly DatabaseInterface
$dbi,
35 private readonly Relation
$relation,
36 private readonly Template
$template,
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
);
57 * Loads user preferences
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;
79 'config_data' => is_array($configData) ?
$configData : [],
80 'mtime' => is_int($timestamp) ?
$timestamp : time(),
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) : [];
100 'config_data' => is_array($configData) ?
$configData : [],
101 'mtime' => is_numeric($row['ts']) ?
(int) $row['ts'] : time(),
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;
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']);
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);
142 $query = 'UPDATE ' . $queryTable
143 . ' SET `timevalue` = NOW(), `config_data` = '
144 . $this->dbi
->quoteString($configData)
145 . ' WHERE `username` = '
146 . $this->dbi
->quoteString($relationParameters->user
);
148 $query = 'INSERT INTO ' . $queryTable
149 . ' (`username`, `timevalue`,`config_data`) '
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
)),
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.'),
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
203 public function apply(array $configData): array
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])) {
219 Core
::arrayWrite($path, $cfg, $value);
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])) {
244 unset($prefs['config_data'][$path]);
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(
261 array|
null $params = null,
262 string|
null $hash = null,
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;
291 $returnUrl = '?' . http_build_query($_GET, '', '&');
293 return $this->template
->render('preferences/autoload', [
294 'hidden_inputs' => Url
::getHiddenInputs(),
295 'return_url' => $returnUrl,