1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace PhpMyAdmin\ConfigStorage; |
6
|
|
|
|
7
|
|
|
use PhpMyAdmin\Config; |
8
|
|
|
use PhpMyAdmin\ConfigStorage\Features\PdfFeature; |
9
|
|
|
use PhpMyAdmin\Current; |
10
|
|
|
use PhpMyAdmin\DatabaseInterface; |
11
|
|
|
use PhpMyAdmin\Dbal\ConnectionType; |
12
|
|
|
use PhpMyAdmin\Identifiers\DatabaseName; |
|
|
|
|
13
|
|
|
use PhpMyAdmin\Identifiers\TableName; |
|
|
|
|
14
|
|
|
use PhpMyAdmin\InternalRelations; |
15
|
|
|
use PhpMyAdmin\SqlParser\Parser; |
16
|
|
|
use PhpMyAdmin\SqlParser\Statements\CreateStatement; |
17
|
|
|
use PhpMyAdmin\SqlParser\Utils\Table as TableUtils; |
18
|
|
|
use PhpMyAdmin\Table\Table; |
19
|
|
|
use PhpMyAdmin\Util; |
20
|
|
|
use PhpMyAdmin\Version; |
21
|
|
|
|
22
|
|
|
use function __; |
23
|
|
|
use function array_fill_keys; |
24
|
|
|
use function array_keys; |
25
|
|
|
use function array_reverse; |
26
|
|
|
use function array_search; |
27
|
|
|
use function array_shift; |
28
|
|
|
use function asort; |
29
|
|
|
use function bin2hex; |
30
|
|
|
use function count; |
31
|
|
|
use function explode; |
32
|
|
|
use function file_get_contents; |
33
|
|
|
use function htmlspecialchars; |
34
|
|
|
use function implode; |
35
|
|
|
use function in_array; |
36
|
|
|
use function is_string; |
37
|
|
|
use function ksort; |
38
|
|
|
use function mb_check_encoding; |
39
|
|
|
use function mb_strlen; |
40
|
|
|
use function mb_strtolower; |
41
|
|
|
use function mb_strtoupper; |
42
|
|
|
use function mb_substr; |
43
|
|
|
use function natcasesort; |
44
|
|
|
use function preg_match; |
45
|
|
|
use function sprintf; |
46
|
|
|
use function str_contains; |
47
|
|
|
use function str_replace; |
48
|
|
|
use function strnatcasecmp; |
49
|
|
|
use function trim; |
50
|
|
|
use function uksort; |
51
|
|
|
use function usort; |
52
|
|
|
|
53
|
|
|
use const SQL_DIR; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* Set of functions used with the relation and PDF feature |
57
|
|
|
*/ |
58
|
|
|
class Relation |
59
|
|
|
{ |
60
|
|
|
private static RelationParameters|null $cache = null; |
61
|
|
|
private readonly Config $config; |
62
|
|
|
|
63
|
116 |
|
public function __construct(public DatabaseInterface $dbi, Config|null $config = null) |
64
|
|
|
{ |
65
|
116 |
|
$this->config = $config ?? Config::getInstance(); |
|
|
|
|
66
|
|
|
} |
67
|
|
|
|
68
|
76 |
|
public function getRelationParameters(): RelationParameters |
69
|
|
|
{ |
70
|
76 |
|
if (self::$cache === null) { |
71
|
48 |
|
self::$cache = RelationParameters::fromArray($this->checkRelationsParam()); |
72
|
|
|
} |
73
|
|
|
|
74
|
76 |
|
return self::$cache; |
|
|
|
|
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* @param array<string, bool|string|null> $relationParams |
79
|
|
|
* |
80
|
|
|
* @return array<string, bool|string|null> |
81
|
|
|
*/ |
82
|
32 |
|
private function checkTableAccess(array $relationParams): array |
83
|
|
|
{ |
84
|
32 |
|
if (isset($relationParams['relation'], $relationParams['table_info'])) { |
85
|
|
|
if ($this->canAccessStorageTable((string) $relationParams['table_info'])) { |
86
|
|
|
$relationParams['displaywork'] = true; |
87
|
|
|
} |
88
|
|
|
} |
89
|
|
|
|
90
|
32 |
|
if (isset($relationParams['table_coords'], $relationParams['pdf_pages'])) { |
91
|
|
|
if ($this->canAccessStorageTable((string) $relationParams['table_coords'])) { |
92
|
|
|
if ($this->canAccessStorageTable((string) $relationParams['pdf_pages'])) { |
93
|
|
|
$relationParams['pdfwork'] = true; |
94
|
|
|
} |
95
|
|
|
} |
96
|
|
|
} |
97
|
|
|
|
98
|
32 |
|
if (isset($relationParams['column_info'])) { |
99
|
|
|
if ($this->canAccessStorageTable((string) $relationParams['column_info'])) { |
100
|
|
|
$relationParams['commwork'] = true; |
101
|
|
|
// phpMyAdmin 4.3+ |
102
|
|
|
// Check for input transformations upgrade. |
103
|
|
|
$relationParams['mimework'] = $this->tryUpgradeTransformations(); |
104
|
|
|
} |
105
|
|
|
} |
106
|
|
|
|
107
|
32 |
|
if (isset($relationParams['users'], $relationParams['usergroups'])) { |
108
|
|
|
if ($this->canAccessStorageTable((string) $relationParams['users'])) { |
109
|
|
|
if ($this->canAccessStorageTable((string) $relationParams['usergroups'])) { |
110
|
|
|
$relationParams['menuswork'] = true; |
111
|
|
|
} |
112
|
|
|
} |
113
|
|
|
} |
114
|
|
|
|
115
|
32 |
|
$settings = [ |
116
|
32 |
|
'export_templates' => 'exporttemplateswork', |
117
|
32 |
|
'designer_settings' => 'designersettingswork', |
118
|
32 |
|
'central_columns' => 'centralcolumnswork', |
119
|
32 |
|
'savedsearches' => 'savedsearcheswork', |
120
|
32 |
|
'navigationhiding' => 'navwork', |
121
|
32 |
|
'bookmark' => 'bookmarkwork', |
122
|
32 |
|
'userconfig' => 'userconfigwork', |
123
|
32 |
|
'tracking' => 'trackingwork', |
124
|
32 |
|
'table_uiprefs' => 'uiprefswork', |
125
|
32 |
|
'favorite' => 'favoritework', |
126
|
32 |
|
'recent' => 'recentwork', |
127
|
32 |
|
'history' => 'historywork', |
128
|
32 |
|
'relation' => 'relwork', |
129
|
32 |
|
]; |
130
|
|
|
|
131
|
32 |
|
foreach ($settings as $setingName => $worksKey) { |
132
|
32 |
|
if (! isset($relationParams[$setingName])) { |
133
|
32 |
|
continue; |
134
|
|
|
} |
135
|
|
|
|
136
|
28 |
|
if (! $this->canAccessStorageTable((string) $relationParams[$setingName])) { |
137
|
4 |
|
continue; |
138
|
|
|
} |
139
|
|
|
|
140
|
24 |
|
$relationParams[$worksKey] = true; |
141
|
|
|
} |
142
|
|
|
|
143
|
32 |
|
return $relationParams; |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* @param array<string, bool|string|null> $relationParams |
148
|
|
|
* |
149
|
|
|
* @return array<string, bool|string|null>|null |
150
|
|
|
*/ |
151
|
40 |
|
private function fillRelationParamsWithTableNames(array $relationParams): array|null |
152
|
|
|
{ |
153
|
40 |
|
if ($this->arePmadbTablesAllDisabled()) { |
154
|
|
|
return null; |
155
|
|
|
} |
156
|
|
|
|
157
|
40 |
|
$tables = $this->dbi->getTables($this->config->selectedServer['pmadb'], ConnectionType::ControlUser); |
158
|
40 |
|
if ($tables === []) { |
159
|
8 |
|
return null; |
160
|
|
|
} |
161
|
|
|
|
162
|
32 |
|
foreach ($tables as $table) { |
163
|
32 |
|
if ($table == $this->config->selectedServer['bookmarktable']) { |
164
|
|
|
$relationParams['bookmark'] = $table; |
165
|
32 |
|
} elseif ($table == $this->config->selectedServer['relation']) { |
166
|
|
|
$relationParams['relation'] = $table; |
167
|
32 |
|
} elseif ($table == $this->config->selectedServer['table_info']) { |
168
|
|
|
$relationParams['table_info'] = $table; |
169
|
32 |
|
} elseif ($table == $this->config->selectedServer['table_coords']) { |
170
|
|
|
$relationParams['table_coords'] = $table; |
171
|
32 |
|
} elseif ($table == $this->config->selectedServer['column_info']) { |
172
|
|
|
$relationParams['column_info'] = $table; |
173
|
32 |
|
} elseif ($table == $this->config->selectedServer['pdf_pages']) { |
174
|
|
|
$relationParams['pdf_pages'] = $table; |
175
|
32 |
|
} elseif ($table == $this->config->selectedServer['history']) { |
176
|
|
|
$relationParams['history'] = $table; |
177
|
32 |
|
} elseif ($table == $this->config->selectedServer['recent']) { |
178
|
|
|
$relationParams['recent'] = $table; |
179
|
32 |
|
} elseif ($table == $this->config->selectedServer['favorite']) { |
180
|
4 |
|
$relationParams['favorite'] = $table; |
181
|
28 |
|
} elseif ($table == $this->config->selectedServer['table_uiprefs']) { |
182
|
|
|
$relationParams['table_uiprefs'] = $table; |
183
|
28 |
|
} elseif ($table == $this->config->selectedServer['tracking']) { |
184
|
|
|
$relationParams['tracking'] = $table; |
185
|
28 |
|
} elseif ($table == $this->config->selectedServer['userconfig']) { |
186
|
24 |
|
$relationParams['userconfig'] = $table; |
187
|
8 |
|
} elseif ($table == $this->config->selectedServer['users']) { |
188
|
|
|
$relationParams['users'] = $table; |
189
|
8 |
|
} elseif ($table == $this->config->selectedServer['usergroups']) { |
190
|
|
|
$relationParams['usergroups'] = $table; |
191
|
8 |
|
} elseif ($table == $this->config->selectedServer['navigationhiding']) { |
192
|
|
|
$relationParams['navigationhiding'] = $table; |
193
|
8 |
|
} elseif ($table == $this->config->selectedServer['savedsearches']) { |
194
|
|
|
$relationParams['savedsearches'] = $table; |
195
|
8 |
|
} elseif ($table == $this->config->selectedServer['central_columns']) { |
196
|
|
|
$relationParams['central_columns'] = $table; |
197
|
8 |
|
} elseif ($table == $this->config->selectedServer['designer_settings']) { |
198
|
|
|
$relationParams['designer_settings'] = $table; |
199
|
8 |
|
} elseif ($table == $this->config->selectedServer['export_templates']) { |
200
|
|
|
$relationParams['export_templates'] = $table; |
201
|
|
|
} |
202
|
|
|
} |
203
|
|
|
|
204
|
32 |
|
return $relationParams; |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* Defines the relation parameters for the current user |
209
|
|
|
* just a copy of the functions used for relations ;-) |
210
|
|
|
* but added some stuff to check what will work |
211
|
|
|
* |
212
|
|
|
* @return array<string, bool|string|null> the relation parameters for the current user |
213
|
|
|
*/ |
214
|
48 |
|
private function checkRelationsParam(): array |
215
|
|
|
{ |
216
|
48 |
|
$workToTable = [ |
217
|
48 |
|
'relwork' => 'relation', |
218
|
48 |
|
'displaywork' => ['relation', 'table_info'], |
219
|
48 |
|
'bookmarkwork' => 'bookmarktable', |
220
|
48 |
|
'pdfwork' => ['table_coords', 'pdf_pages'], |
221
|
48 |
|
'commwork' => 'column_info', |
222
|
48 |
|
'mimework' => 'column_info', |
223
|
48 |
|
'historywork' => 'history', |
224
|
48 |
|
'recentwork' => 'recent', |
225
|
48 |
|
'favoritework' => 'favorite', |
226
|
48 |
|
'uiprefswork' => 'table_uiprefs', |
227
|
48 |
|
'trackingwork' => 'tracking', |
228
|
48 |
|
'userconfigwork' => 'userconfig', |
229
|
48 |
|
'menuswork' => ['users', 'usergroups'], |
230
|
48 |
|
'navwork' => 'navigationhiding', |
231
|
48 |
|
'savedsearcheswork' => 'savedsearches', |
232
|
48 |
|
'centralcolumnswork' => 'central_columns', |
233
|
48 |
|
'designersettingswork' => 'designer_settings', |
234
|
48 |
|
'exporttemplateswork' => 'export_templates', |
235
|
48 |
|
]; |
236
|
|
|
|
237
|
48 |
|
$relationParams = array_fill_keys(array_keys($workToTable), false); |
238
|
|
|
|
239
|
48 |
|
$relationParams['version'] = Version::VERSION; |
240
|
48 |
|
$relationParams['allworks'] = false; |
241
|
48 |
|
$relationParams['user'] = null; |
242
|
48 |
|
$relationParams['db'] = null; |
243
|
|
|
|
244
|
|
|
if ( |
245
|
48 |
|
Current::$server === 0 |
246
|
44 |
|
|| $this->config->selectedServer['pmadb'] === '' |
247
|
48 |
|
|| ! $this->dbi->selectDb($this->config->selectedServer['pmadb'], ConnectionType::ControlUser) |
248
|
|
|
) { |
249
|
8 |
|
$this->config->selectedServer['pmadb'] = ''; |
250
|
|
|
|
251
|
8 |
|
return $relationParams; |
252
|
|
|
} |
253
|
|
|
|
254
|
40 |
|
$relationParams['user'] = $this->config->selectedServer['user']; |
255
|
40 |
|
$relationParams['db'] = $this->config->selectedServer['pmadb']; |
256
|
|
|
|
257
|
40 |
|
$relationParamsFilled = $this->fillRelationParamsWithTableNames($relationParams); |
258
|
|
|
|
259
|
40 |
|
if ($relationParamsFilled === null) { |
260
|
8 |
|
return $relationParams; |
261
|
|
|
} |
262
|
|
|
|
263
|
32 |
|
$relationParams = $this->checkTableAccess($relationParamsFilled); |
264
|
|
|
|
265
|
32 |
|
$allWorks = true; |
266
|
32 |
|
foreach ($workToTable as $work => $table) { |
267
|
32 |
|
if ($relationParams[$work]) { |
268
|
|
|
continue; |
269
|
|
|
} |
270
|
|
|
|
271
|
32 |
|
if (is_string($table)) { |
272
|
32 |
|
if (isset($this->config->selectedServer[$table]) && $this->config->selectedServer[$table] !== false) { |
273
|
32 |
|
$allWorks = false; |
274
|
32 |
|
break; |
275
|
|
|
} |
276
|
|
|
} else { |
277
|
|
|
$oneNull = false; |
278
|
|
|
foreach ($table as $t) { |
279
|
|
|
if (isset($this->config->selectedServer[$t]) && $this->config->selectedServer[$t] === false) { |
280
|
|
|
$oneNull = true; |
281
|
|
|
break; |
282
|
|
|
} |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
if (! $oneNull) { |
286
|
|
|
$allWorks = false; |
287
|
|
|
break; |
288
|
|
|
} |
289
|
|
|
} |
290
|
|
|
} |
291
|
|
|
|
292
|
32 |
|
$relationParams['allworks'] = $allWorks; |
293
|
|
|
|
294
|
32 |
|
return $relationParams; |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
/** |
298
|
|
|
* Check if the table is accessible |
299
|
|
|
* |
300
|
|
|
* @param string $tableDbName The table or table.db |
301
|
|
|
*/ |
302
|
28 |
|
public function canAccessStorageTable(string $tableDbName): bool |
303
|
|
|
{ |
304
|
28 |
|
$result = $this->dbi->tryQueryAsControlUser('SELECT NULL FROM ' . Util::backquote($tableDbName) . ' LIMIT 0'); |
305
|
|
|
|
306
|
28 |
|
return $result !== false; |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
/** |
310
|
|
|
* Check whether column_info table input transformation |
311
|
|
|
* upgrade is required and try to upgrade silently |
312
|
|
|
*/ |
313
|
4 |
|
public function tryUpgradeTransformations(): bool |
314
|
|
|
{ |
315
|
|
|
// From 4.3, new input oriented transformation feature was introduced. |
316
|
|
|
// Check whether column_info table has input transformation columns |
317
|
4 |
|
$newCols = ['input_transformation', 'input_transformation_options']; |
318
|
4 |
|
$query = 'SHOW COLUMNS FROM ' |
319
|
4 |
|
. Util::backquote($this->config->selectedServer['pmadb']) |
320
|
4 |
|
. '.' . Util::backquote($this->config->selectedServer['column_info']) |
321
|
4 |
|
. ' WHERE Field IN (\'' . implode('\', \'', $newCols) . '\')'; |
322
|
4 |
|
$result = $this->dbi->tryQueryAsControlUser($query); |
323
|
4 |
|
if ($result) { |
324
|
4 |
|
$rows = $result->numRows(); |
325
|
4 |
|
unset($result); |
326
|
|
|
// input transformations are present |
327
|
|
|
// no need to upgrade |
328
|
4 |
|
if ($rows === 2) { |
329
|
|
|
return true; |
330
|
|
|
|
331
|
|
|
// try silent upgrade without disturbing the user |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
// read upgrade query file |
335
|
4 |
|
$query = @file_get_contents(SQL_DIR . 'upgrade_column_info_4_3_0+.sql'); |
336
|
|
|
// replace database name from query to with set in config.inc.php |
337
|
|
|
// replace pma__column_info table name from query |
338
|
|
|
// to with set in config.inc.php |
339
|
4 |
|
$query = str_replace( |
340
|
4 |
|
['`phpmyadmin`', '`pma__column_info`'], |
341
|
4 |
|
[ |
342
|
4 |
|
Util::backquote($this->config->selectedServer['pmadb']), |
343
|
4 |
|
Util::backquote($this->config->selectedServer['column_info']), |
344
|
4 |
|
], |
345
|
4 |
|
(string) $query, |
346
|
4 |
|
); |
347
|
4 |
|
$this->dbi->tryMultiQuery($query, ConnectionType::ControlUser); |
348
|
|
|
// skips result sets of query as we are not interested in it |
349
|
|
|
/** @infection-ignore-all */ |
350
|
|
|
do { |
351
|
4 |
|
$hasResult = $this->dbi->nextResult(ConnectionType::ControlUser); |
352
|
4 |
|
} while ($hasResult !== false); |
353
|
|
|
|
354
|
4 |
|
$error = $this->dbi->getError(ConnectionType::ControlUser); |
355
|
|
|
|
356
|
|
|
// return true if no error exists otherwise false |
357
|
4 |
|
return $error === ''; |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
// some failure, either in upgrading or something else |
361
|
|
|
// make some noise, time to wake up user. |
362
|
|
|
return false; |
363
|
|
|
} |
364
|
|
|
|
365
|
|
|
/** |
366
|
|
|
* Gets all Relations to foreign tables for a given table or |
367
|
|
|
* optionally a given column in a table |
368
|
|
|
* |
369
|
|
|
* @param string $db the name of the db to check for |
370
|
|
|
* @param string $table the name of the table to check for |
371
|
|
|
* @param string $column the name of the column to check for |
372
|
|
|
* @param string $source the source for foreign key information |
373
|
|
|
* |
374
|
|
|
* @return mixed[] db,table,column |
375
|
|
|
*/ |
376
|
|
|
public function getForeigners(string $db, string $table, string $column = '', string $source = 'both'): array |
377
|
|
|
{ |
378
|
|
|
$relationFeature = $this->getRelationParameters()->relationFeature; |
379
|
|
|
$foreign = []; |
380
|
|
|
|
381
|
|
|
if ($relationFeature !== null && ($source === 'both' || $source === 'internal')) { |
382
|
|
|
$relQuery = 'SELECT `master_field`, `foreign_db`, ' |
383
|
|
|
. '`foreign_table`, `foreign_field`' |
384
|
|
|
. ' FROM ' . Util::backquote($relationFeature->database) |
385
|
|
|
. '.' . Util::backquote($relationFeature->relation) |
386
|
|
|
. ' WHERE `master_db` = ' . $this->dbi->quoteString($db) |
387
|
|
|
. ' AND `master_table` = ' . $this->dbi->quoteString($table); |
388
|
|
|
if ($column !== '') { |
389
|
|
|
$relQuery .= ' AND `master_field` = ' . $this->dbi->quoteString($column); |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
$foreign = $this->dbi->fetchResult($relQuery, 'master_field', null, ConnectionType::ControlUser); |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
if (($source === 'both' || $source === 'foreign') && $table !== '') { |
396
|
|
|
$tableObj = new Table($table, $db, $this->dbi); |
397
|
|
|
$showCreateTable = $tableObj->showCreate(); |
398
|
|
|
if ($showCreateTable !== '') { |
399
|
|
|
$parser = new Parser($showCreateTable); |
400
|
|
|
$stmt = $parser->statements[0]; |
401
|
|
|
$foreign['foreign_keys_data'] = []; |
402
|
|
|
if ($stmt instanceof CreateStatement) { |
403
|
|
|
$foreign['foreign_keys_data'] = TableUtils::getForeignKeys($stmt); |
404
|
|
|
} |
405
|
|
|
} |
406
|
|
|
} |
407
|
|
|
|
408
|
|
|
/** |
409
|
|
|
* Emulating relations for some information_schema tables |
410
|
|
|
*/ |
411
|
|
|
$isInformationSchema = mb_strtolower($db) === 'information_schema'; |
412
|
|
|
$isMysql = mb_strtolower($db) === 'mysql'; |
413
|
|
|
if (($isInformationSchema || $isMysql) && ($source === 'internal' || $source === 'both')) { |
414
|
|
|
if ($isInformationSchema) { |
415
|
|
|
$internalRelations = InternalRelations::INFORMATION_SCHEMA; |
416
|
|
|
} else { |
417
|
|
|
$internalRelations = InternalRelations::MYSQL; |
418
|
|
|
} |
419
|
|
|
|
420
|
|
|
if (isset($internalRelations[$table])) { |
421
|
|
|
foreach ($internalRelations[$table] as $field => $relations) { |
422
|
|
|
if ( |
423
|
|
|
($column !== '' && $column != $field) |
424
|
|
|
|| (isset($foreign[$field]) |
425
|
|
|
&& $foreign[$field] != '') |
426
|
|
|
) { |
427
|
|
|
continue; |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
$foreign[$field] = $relations; |
431
|
|
|
} |
432
|
|
|
} |
433
|
|
|
} |
434
|
|
|
|
435
|
|
|
return $foreign; |
436
|
|
|
} |
437
|
|
|
|
438
|
|
|
/** |
439
|
|
|
* Gets the display field of a table |
440
|
|
|
* |
441
|
|
|
* @param string $db the name of the db to check for |
442
|
|
|
* @param string $table the name of the table to check for |
443
|
|
|
* |
444
|
|
|
* @return string field name |
445
|
|
|
*/ |
446
|
4 |
|
public function getDisplayField(string $db, string $table): string |
447
|
|
|
{ |
448
|
4 |
|
$displayFeature = $this->getRelationParameters()->displayFeature; |
449
|
|
|
|
450
|
|
|
/** |
451
|
|
|
* Try to fetch the display field from DB. |
452
|
|
|
*/ |
453
|
4 |
|
if ($displayFeature !== null) { |
454
|
|
|
$dispQuery = 'SELECT `display_field`' |
455
|
|
|
. ' FROM ' . Util::backquote($displayFeature->database) |
456
|
|
|
. '.' . Util::backquote($displayFeature->tableInfo) |
457
|
|
|
. ' WHERE `db_name` = ' . $this->dbi->quoteString($db) |
458
|
|
|
. ' AND `table_name` = ' . $this->dbi->quoteString($table); |
459
|
|
|
|
460
|
|
|
$row = $this->dbi->fetchSingleRow($dispQuery, DatabaseInterface::FETCH_ASSOC, ConnectionType::ControlUser); |
461
|
|
|
if (isset($row['display_field'])) { |
462
|
|
|
return $row['display_field']; |
463
|
|
|
} |
464
|
|
|
} |
465
|
|
|
|
466
|
|
|
/** |
467
|
|
|
* Emulating the display field for some information_schema tables. |
468
|
|
|
*/ |
469
|
4 |
|
if ($db === 'information_schema') { |
470
|
|
|
switch ($table) { |
471
|
4 |
|
case 'CHARACTER_SETS': |
472
|
4 |
|
return 'DESCRIPTION'; |
473
|
|
|
|
474
|
4 |
|
case 'TABLES': |
475
|
4 |
|
return 'TABLE_COMMENT'; |
476
|
|
|
} |
477
|
|
|
} |
478
|
|
|
|
479
|
|
|
/** |
480
|
|
|
* Pick first char field |
481
|
|
|
*/ |
482
|
4 |
|
$columns = $this->dbi->getColumnsFull($db, $table); |
483
|
4 |
|
foreach ($columns as $column) { |
484
|
|
|
if ($this->dbi->types->getTypeClass($column['DATA_TYPE']) === 'CHAR') { |
485
|
|
|
return $column['COLUMN_NAME']; |
486
|
|
|
} |
487
|
|
|
} |
488
|
|
|
|
489
|
4 |
|
return ''; |
490
|
|
|
} |
491
|
|
|
|
492
|
|
|
/** |
493
|
|
|
* Gets the comments for all columns of a table or the db itself |
494
|
|
|
* |
495
|
|
|
* @param string $db the name of the db to check for |
496
|
|
|
* @param string $table the name of the table to check for |
497
|
|
|
* |
498
|
|
|
* @return string[] [column_name] = comment |
499
|
|
|
*/ |
500
|
4 |
|
public function getComments(string $db, string $table = ''): array |
501
|
|
|
{ |
502
|
4 |
|
if ($table === '') { |
503
|
4 |
|
return [$this->getDbComment($db)]; |
504
|
|
|
} |
505
|
|
|
|
506
|
4 |
|
$comments = []; |
507
|
|
|
|
508
|
|
|
// MySQL native column comments |
509
|
4 |
|
$columns = $this->dbi->getColumns($db, $table, true); |
510
|
4 |
|
foreach ($columns as $column) { |
511
|
4 |
|
if ($column->comment === '') { |
512
|
|
|
continue; |
513
|
|
|
} |
514
|
|
|
|
515
|
4 |
|
$comments[$column->field] = $column->comment; |
516
|
|
|
} |
517
|
|
|
|
518
|
4 |
|
return $comments; |
519
|
|
|
} |
520
|
|
|
|
521
|
|
|
/** |
522
|
|
|
* Gets the comment for a db |
523
|
|
|
* |
524
|
|
|
* @param string $db the name of the db to check for |
525
|
|
|
*/ |
526
|
4 |
|
public function getDbComment(string $db): string |
527
|
|
|
{ |
528
|
4 |
|
$columnCommentsFeature = $this->getRelationParameters()->columnCommentsFeature; |
529
|
4 |
|
if ($columnCommentsFeature !== null) { |
530
|
|
|
// pmadb internal db comment |
531
|
|
|
$comQry = 'SELECT `comment`' |
532
|
|
|
. ' FROM ' . Util::backquote($columnCommentsFeature->database) |
533
|
|
|
. '.' . Util::backquote($columnCommentsFeature->columnInfo) |
534
|
|
|
. ' WHERE db_name = ' . $this->dbi->quoteString($db, ConnectionType::ControlUser) |
535
|
|
|
. ' AND table_name = \'\'' |
536
|
|
|
. ' AND column_name = \'(db_comment)\''; |
537
|
|
|
$comRs = $this->dbi->tryQueryAsControlUser($comQry); |
538
|
|
|
|
539
|
|
|
if ($comRs && $comRs->numRows() > 0) { |
540
|
|
|
$row = $comRs->fetchAssoc(); |
541
|
|
|
|
542
|
|
|
return (string) $row['comment']; |
543
|
|
|
} |
544
|
|
|
} |
545
|
|
|
|
546
|
4 |
|
return ''; |
547
|
|
|
} |
548
|
|
|
|
549
|
|
|
/** |
550
|
|
|
* Set a database comment to a certain value. |
551
|
|
|
* |
552
|
|
|
* @param string $db the name of the db |
553
|
|
|
* @param string $comment the value of the column |
554
|
|
|
*/ |
555
|
|
|
public function setDbComment(string $db, string $comment = ''): bool |
556
|
|
|
{ |
557
|
|
|
$columnCommentsFeature = $this->getRelationParameters()->columnCommentsFeature; |
558
|
|
|
if ($columnCommentsFeature === null) { |
559
|
|
|
return false; |
560
|
|
|
} |
561
|
|
|
|
562
|
|
|
if ($comment !== '') { |
563
|
|
|
$updQuery = 'INSERT INTO ' |
564
|
|
|
. Util::backquote($columnCommentsFeature->database) . '.' |
565
|
|
|
. Util::backquote($columnCommentsFeature->columnInfo) |
566
|
|
|
. ' (`db_name`, `table_name`, `column_name`, `comment`)' |
567
|
|
|
. ' VALUES (' |
568
|
|
|
. $this->dbi->quoteString($db, ConnectionType::ControlUser) |
569
|
|
|
. ", '', '(db_comment)', " |
570
|
|
|
. $this->dbi->quoteString($comment, ConnectionType::ControlUser) |
571
|
|
|
. ') ' |
572
|
|
|
. ' ON DUPLICATE KEY UPDATE ' |
573
|
|
|
. '`comment` = ' . $this->dbi->quoteString($comment, ConnectionType::ControlUser); |
574
|
|
|
} else { |
575
|
|
|
$updQuery = 'DELETE FROM ' |
576
|
|
|
. Util::backquote($columnCommentsFeature->database) . '.' |
577
|
|
|
. Util::backquote($columnCommentsFeature->columnInfo) |
578
|
|
|
. ' WHERE `db_name` = ' . $this->dbi->quoteString($db, ConnectionType::ControlUser) |
579
|
|
|
. ' |
580
|
|
|
AND `table_name` = \'\' |
581
|
|
|
AND `column_name` = \'(db_comment)\''; |
582
|
|
|
} |
583
|
|
|
|
584
|
|
|
return (bool) $this->dbi->queryAsControlUser($updQuery); |
585
|
|
|
} |
586
|
|
|
|
587
|
|
|
/** |
588
|
|
|
* Set a SQL history entry |
589
|
|
|
* |
590
|
|
|
* @param string $db the name of the db |
591
|
|
|
* @param string $table the name of the table |
592
|
|
|
* @param string $username the username |
593
|
|
|
* @param string $sqlquery the sql query |
594
|
|
|
*/ |
595
|
|
|
public function setHistory(string $db, string $table, string $username, string $sqlquery): void |
596
|
|
|
{ |
597
|
|
|
$maxCharactersInDisplayedSQL = $this->config->settings['MaxCharactersInDisplayedSQL']; |
598
|
|
|
// Prevent to run this automatically on Footer class destroying in testsuite |
599
|
|
|
if (mb_strlen($sqlquery) > $maxCharactersInDisplayedSQL) { |
600
|
|
|
return; |
601
|
|
|
} |
602
|
|
|
|
603
|
|
|
$sqlHistoryFeature = $this->getRelationParameters()->sqlHistoryFeature; |
604
|
|
|
|
605
|
|
|
if (! isset($_SESSION['sql_history'])) { |
606
|
|
|
$_SESSION['sql_history'] = []; |
607
|
|
|
} |
608
|
|
|
|
609
|
|
|
$_SESSION['sql_history'][] = ['db' => $db, 'table' => $table, 'sqlquery' => $sqlquery]; |
610
|
|
|
|
611
|
|
|
if (count($_SESSION['sql_history']) > $this->config->settings['QueryHistoryMax']) { |
612
|
|
|
// history should not exceed a maximum count |
613
|
|
|
array_shift($_SESSION['sql_history']); |
614
|
|
|
} |
615
|
|
|
|
616
|
|
|
if ($sqlHistoryFeature === null || ! $this->config->settings['QueryHistoryDB']) { |
617
|
|
|
return; |
618
|
|
|
} |
619
|
|
|
|
620
|
|
|
$this->dbi->queryAsControlUser( |
621
|
|
|
'INSERT INTO ' |
622
|
|
|
. Util::backquote($sqlHistoryFeature->database) . '.' |
623
|
|
|
. Util::backquote($sqlHistoryFeature->history) . ' |
624
|
|
|
(`username`, |
625
|
|
|
`db`, |
626
|
|
|
`table`, |
627
|
|
|
`timevalue`, |
628
|
|
|
`sqlquery`) |
629
|
|
|
VALUES |
630
|
|
|
(' . $this->dbi->quoteString($username, ConnectionType::ControlUser) . ', |
631
|
|
|
' . $this->dbi->quoteString($db, ConnectionType::ControlUser) . ', |
632
|
|
|
' . $this->dbi->quoteString($table, ConnectionType::ControlUser) . ', |
633
|
|
|
NOW(), |
634
|
|
|
' . $this->dbi->quoteString($sqlquery, ConnectionType::ControlUser) . ')', |
635
|
|
|
); |
636
|
|
|
|
637
|
|
|
$this->purgeHistory($username); |
638
|
|
|
} |
639
|
|
|
|
640
|
|
|
/** |
641
|
|
|
* Gets a SQL history entry |
642
|
|
|
* |
643
|
|
|
* @param string $username the username |
644
|
|
|
* |
645
|
|
|
* @return mixed[]|bool list of history items |
646
|
|
|
*/ |
647
|
|
|
public function getHistory(string $username): array|bool |
648
|
|
|
{ |
649
|
|
|
$sqlHistoryFeature = $this->getRelationParameters()->sqlHistoryFeature; |
650
|
|
|
if ($sqlHistoryFeature === null) { |
651
|
|
|
return false; |
652
|
|
|
} |
653
|
|
|
|
654
|
|
|
/** |
655
|
|
|
* if db-based history is disabled but there exists a session-based |
656
|
|
|
* history, use it |
657
|
|
|
*/ |
658
|
|
|
if (! $this->config->settings['QueryHistoryDB']) { |
659
|
|
|
if (isset($_SESSION['sql_history'])) { |
660
|
|
|
return array_reverse($_SESSION['sql_history']); |
661
|
|
|
} |
662
|
|
|
|
663
|
|
|
return false; |
664
|
|
|
} |
665
|
|
|
|
666
|
|
|
$histQuery = ' |
667
|
|
|
SELECT `db`, |
668
|
|
|
`table`, |
669
|
|
|
`sqlquery`, |
670
|
|
|
`timevalue` |
671
|
|
|
FROM ' . Util::backquote($sqlHistoryFeature->database) |
672
|
|
|
. '.' . Util::backquote($sqlHistoryFeature->history) . ' |
673
|
|
|
WHERE `username` = ' . $this->dbi->quoteString($username) . ' |
674
|
|
|
ORDER BY `id` DESC'; |
675
|
|
|
|
676
|
|
|
return $this->dbi->fetchResult($histQuery, null, null, ConnectionType::ControlUser); |
677
|
|
|
} |
678
|
|
|
|
679
|
|
|
/** |
680
|
|
|
* purges SQL history |
681
|
|
|
* |
682
|
|
|
* deletes entries that exceeds $cfg['QueryHistoryMax'], oldest first, for the |
683
|
|
|
* given user |
684
|
|
|
* |
685
|
|
|
* @param string $username the username |
686
|
|
|
*/ |
687
|
|
|
public function purgeHistory(string $username): void |
688
|
|
|
{ |
689
|
|
|
$sqlHistoryFeature = $this->getRelationParameters()->sqlHistoryFeature; |
690
|
|
|
if (! $this->config->settings['QueryHistoryDB'] || $sqlHistoryFeature === null) { |
691
|
|
|
return; |
692
|
|
|
} |
693
|
|
|
|
694
|
|
|
$searchQuery = ' |
695
|
|
|
SELECT `timevalue` |
696
|
|
|
FROM ' . Util::backquote($sqlHistoryFeature->database) |
697
|
|
|
. '.' . Util::backquote($sqlHistoryFeature->history) . ' |
698
|
|
|
WHERE `username` = ' . $this->dbi->quoteString($username) . ' |
699
|
|
|
ORDER BY `timevalue` DESC |
700
|
|
|
LIMIT ' . $this->config->settings['QueryHistoryMax'] . ', 1'; |
701
|
|
|
|
702
|
|
|
$maxTime = $this->dbi->fetchValue($searchQuery, 0, ConnectionType::ControlUser); |
703
|
|
|
|
704
|
|
|
if (! $maxTime) { |
705
|
|
|
return; |
706
|
|
|
} |
707
|
|
|
|
708
|
|
|
$this->dbi->queryAsControlUser( |
709
|
|
|
'DELETE FROM ' |
710
|
|
|
. Util::backquote($sqlHistoryFeature->database) . '.' |
711
|
|
|
. Util::backquote($sqlHistoryFeature->history) . ' |
712
|
|
|
WHERE `username` = ' . $this->dbi->quoteString($username, ConnectionType::ControlUser) |
713
|
|
|
. ' |
714
|
|
|
AND `timevalue` <= \'' . $maxTime . '\'', |
715
|
|
|
); |
716
|
|
|
} |
717
|
|
|
|
718
|
|
|
/** |
719
|
|
|
* Prepares the dropdown for one mode |
720
|
|
|
* |
721
|
|
|
* @param mixed[] $foreign the keys and values for foreigns |
722
|
|
|
* @param string $data the current data of the dropdown |
723
|
|
|
* @param string $mode the needed mode |
724
|
|
|
* |
725
|
|
|
* @return string[] the <option value=""><option>s |
726
|
|
|
*/ |
727
|
|
|
public function buildForeignDropdown(array $foreign, string $data, string $mode): array |
728
|
|
|
{ |
729
|
|
|
$reloptions = []; |
730
|
|
|
|
731
|
|
|
// id-only is a special mode used when no foreign display column |
732
|
|
|
// is available |
733
|
|
|
if ($mode === 'id-content' || $mode === 'id-only') { |
734
|
|
|
// sort for id-content |
735
|
|
|
if ($this->config->settings['NaturalOrder']) { |
736
|
|
|
uksort($foreign, strnatcasecmp(...)); |
|
|
|
|
737
|
|
|
} else { |
738
|
|
|
ksort($foreign); |
739
|
|
|
} |
740
|
|
|
} elseif ($mode === 'content-id') { |
741
|
|
|
// sort for content-id |
742
|
|
|
if ($this->config->settings['NaturalOrder']) { |
743
|
|
|
natcasesort($foreign); |
744
|
|
|
} else { |
745
|
|
|
asort($foreign); |
746
|
|
|
} |
747
|
|
|
} |
748
|
|
|
|
749
|
|
|
foreach ($foreign as $key => $value) { |
750
|
|
|
$key = (string) $key; |
751
|
|
|
$value = (string) $value; |
752
|
|
|
|
753
|
|
|
if (mb_check_encoding($key, 'utf-8') && ! preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\x9F]/u', $key)) { |
754
|
|
|
$selected = $key === $data; |
755
|
|
|
// show as text if it's valid utf-8 |
756
|
|
|
$key = htmlspecialchars($key); |
757
|
|
|
} else { |
758
|
|
|
$key = '0x' . bin2hex($key); |
759
|
|
|
if (str_contains($data, '0x')) { |
760
|
|
|
$selected = $key === trim($data); |
761
|
|
|
} else { |
762
|
|
|
$selected = $key === '0x' . $data; |
763
|
|
|
} |
764
|
|
|
} |
765
|
|
|
|
766
|
|
|
if ( |
767
|
|
|
mb_check_encoding($value, 'utf-8') |
768
|
|
|
&& ! preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\x9F]/u', $value) |
769
|
|
|
) { |
770
|
|
|
if (mb_strlen($value) <= $this->config->settings['LimitChars']) { |
771
|
|
|
// show as text if it's valid utf-8 |
772
|
|
|
$value = htmlspecialchars($value); |
773
|
|
|
} else { |
774
|
|
|
// show as truncated text if it's valid utf-8 |
775
|
|
|
$value = htmlspecialchars( |
776
|
|
|
mb_substr( |
777
|
|
|
$value, |
778
|
|
|
0, |
779
|
|
|
$this->config->settings['LimitChars'], |
780
|
|
|
) . '...', |
781
|
|
|
); |
782
|
|
|
} |
783
|
|
|
} else { |
784
|
|
|
$value = '0x' . bin2hex($value); |
785
|
|
|
} |
786
|
|
|
|
787
|
|
|
$reloption = '<option value="' . $key . '"'; |
788
|
|
|
|
789
|
|
|
if ($selected) { |
790
|
|
|
$reloption .= ' selected="selected"'; |
791
|
|
|
} |
792
|
|
|
|
793
|
|
|
if ($mode === 'content-id') { |
794
|
|
|
$reloptions[] = $reloption . '>' |
795
|
|
|
. $value . ' - ' . $key . '</option>'; |
796
|
|
|
} elseif ($mode === 'id-content') { |
797
|
|
|
$reloptions[] = $reloption . '>' |
798
|
|
|
. $key . ' - ' . $value . '</option>'; |
799
|
|
|
} elseif ($mode === 'id-only') { |
800
|
|
|
$reloptions[] = $reloption . '>' |
801
|
|
|
. $key . '</option>'; |
802
|
|
|
} |
803
|
|
|
} |
804
|
|
|
|
805
|
|
|
return $reloptions; |
806
|
|
|
} |
807
|
|
|
|
808
|
|
|
/** |
809
|
|
|
* Outputs dropdown with values of foreign fields |
810
|
|
|
* |
811
|
|
|
* @param mixed[][] $dispRow array of the displayed row |
812
|
|
|
* @param string $foreignField the foreign field |
813
|
|
|
* @param string $foreignDisplay the foreign field to display |
814
|
|
|
* @param string $data the current data of the dropdown (field in row) |
815
|
|
|
* @param int|null $max maximum number of items in the dropdown |
816
|
|
|
* |
817
|
|
|
* @return string the <option value=""><option>s |
818
|
|
|
*/ |
819
|
|
|
public function foreignDropdown( |
820
|
|
|
array $dispRow, |
821
|
|
|
string $foreignField, |
822
|
|
|
string $foreignDisplay, |
823
|
|
|
string $data, |
824
|
|
|
int|null $max = null, |
825
|
|
|
): string { |
826
|
|
|
if ($max === null) { |
827
|
|
|
$max = $this->config->settings['ForeignKeyMaxLimit']; |
828
|
|
|
} |
829
|
|
|
|
830
|
|
|
$foreign = []; |
831
|
|
|
|
832
|
|
|
// collect the data |
833
|
|
|
foreach ($dispRow as $relrow) { |
834
|
|
|
$key = $relrow[$foreignField]; |
835
|
|
|
|
836
|
|
|
// if the display field has been defined for this foreign table |
837
|
|
|
$value = $foreignDisplay !== '' ? $relrow[$foreignDisplay] : ''; |
838
|
|
|
|
839
|
|
|
$foreign[$key] = $value; |
840
|
|
|
} |
841
|
|
|
|
842
|
|
|
// put the dropdown sections in correct order |
843
|
|
|
$bottom = []; |
844
|
|
|
if ($foreignDisplay !== '') { |
845
|
|
|
$top = $this->buildForeignDropdown($foreign, $data, $this->config->settings['ForeignKeyDropdownOrder'][0]); |
846
|
|
|
|
847
|
|
|
if (isset($this->config->settings['ForeignKeyDropdownOrder'][1])) { |
848
|
|
|
$bottom = $this->buildForeignDropdown( |
849
|
|
|
$foreign, |
850
|
|
|
$data, |
851
|
|
|
$this->config->settings['ForeignKeyDropdownOrder'][1], |
852
|
|
|
); |
853
|
|
|
} |
854
|
|
|
} else { |
855
|
|
|
$top = $this->buildForeignDropdown($foreign, $data, 'id-only'); |
856
|
|
|
} |
857
|
|
|
|
858
|
|
|
// beginning of dropdown |
859
|
|
|
$ret = '<option value=""> </option>'; |
860
|
|
|
$topCount = count($top); |
861
|
|
|
if ($max == -1 || $topCount < $max) { |
862
|
|
|
$ret .= implode('', $top); |
863
|
|
|
if ($foreignDisplay && $topCount > 0) { |
864
|
|
|
// this empty option is to visually mark the beginning of the |
865
|
|
|
// second series of values (bottom) |
866
|
|
|
$ret .= '<option value=""> </option>'; |
867
|
|
|
} |
868
|
|
|
} |
869
|
|
|
|
870
|
|
|
if ($foreignDisplay !== '') { |
871
|
|
|
$ret .= implode('', $bottom); |
872
|
|
|
} |
873
|
|
|
|
874
|
|
|
return $ret; |
875
|
|
|
} |
876
|
|
|
|
877
|
|
|
/** |
878
|
|
|
* Gets foreign keys in preparation for a drop-down selector |
879
|
|
|
* |
880
|
|
|
* @param mixed[]|bool $foreigners array of the foreign keys |
881
|
|
|
* @param string $field the foreign field name |
882
|
|
|
* @param bool $overrideTotal whether to override the total |
883
|
|
|
* @param string $foreignFilter a possible filter |
884
|
|
|
* @param string $foreignLimit a possible LIMIT clause |
885
|
|
|
* @param bool $getTotal optional, whether to get total num of rows |
886
|
|
|
* in $foreignData['the_total;] |
887
|
|
|
* (has an effect of performance) |
888
|
|
|
* |
889
|
|
|
* @return array<string, mixed> data about the foreign keys |
890
|
|
|
* @psalm-return array{ |
891
|
|
|
* foreign_link: bool, |
892
|
|
|
* the_total: int|string|null, |
893
|
|
|
* foreign_display: string, |
894
|
|
|
* disp_row: list<non-empty-array>|null, |
895
|
|
|
* foreign_field: mixed |
896
|
|
|
* } |
897
|
|
|
*/ |
898
|
|
|
public function getForeignData( |
899
|
|
|
array|bool $foreigners, |
900
|
|
|
string $field, |
901
|
|
|
bool $overrideTotal, |
902
|
|
|
string $foreignFilter, |
903
|
|
|
string $foreignLimit, |
904
|
|
|
bool $getTotal = false, |
905
|
|
|
): array { |
906
|
|
|
// we always show the foreign field in the drop-down; if a display |
907
|
|
|
// field is defined, we show it besides the foreign field |
908
|
|
|
$foreignLink = false; |
909
|
|
|
$dispRow = $foreignDisplay = $theTotal = $foreignField = null; |
910
|
|
|
do { |
911
|
|
|
if ($foreigners === false || $foreigners === []) { |
912
|
|
|
break; |
913
|
|
|
} |
914
|
|
|
|
915
|
|
|
$foreigner = $this->searchColumnInForeigners($foreigners, $field); |
916
|
|
|
if ($foreigner == false) { |
917
|
|
|
break; |
918
|
|
|
} |
919
|
|
|
|
920
|
|
|
$foreignDb = $foreigner['foreign_db']; |
921
|
|
|
$foreignTable = $foreigner['foreign_table']; |
922
|
|
|
$foreignField = $foreigner['foreign_field']; |
923
|
|
|
|
924
|
|
|
// Count number of rows in the foreign table. Currently we do |
925
|
|
|
// not use a drop-down if more than ForeignKeyMaxLimit rows in the |
926
|
|
|
// foreign table, |
927
|
|
|
// for speed reasons and because we need a better interface for this. |
928
|
|
|
// |
929
|
|
|
// We could also do the SELECT anyway, with a LIMIT, and ensure that |
930
|
|
|
// the current value of the field is one of the choices. |
931
|
|
|
|
932
|
|
|
// Check if table has more rows than specified by ForeignKeyMaxLimit |
933
|
|
|
$moreThanLimit = $this->dbi->getTable($foreignDb, $foreignTable) |
934
|
|
|
->checkIfMinRecordsExist($this->config->settings['ForeignKeyMaxLimit']); |
935
|
|
|
|
936
|
|
|
if ($overrideTotal || ! $moreThanLimit) { |
937
|
|
|
// foreign_display can be false if no display field defined: |
938
|
|
|
$foreignDisplay = $this->getDisplayField($foreignDb, $foreignTable); |
939
|
|
|
|
940
|
|
|
$fQueryMain = 'SELECT ' . Util::backquote($foreignField) |
941
|
|
|
. ( |
942
|
|
|
$foreignDisplay === '' |
943
|
|
|
? '' |
944
|
|
|
: ', ' . Util::backquote($foreignDisplay) |
945
|
|
|
); |
946
|
|
|
$fQueryFrom = ' FROM ' . Util::backquote($foreignDb) |
947
|
|
|
. '.' . Util::backquote($foreignTable); |
948
|
|
|
$fQueryFilter = $foreignFilter === '' ? '' : ' WHERE ' |
949
|
|
|
. Util::backquote($foreignField) |
950
|
|
|
. ' LIKE ' . $this->dbi->quoteString( |
951
|
|
|
'%' . $this->dbi->escapeMysqlWildcards($foreignFilter) . '%', |
952
|
|
|
) |
953
|
|
|
. ( |
954
|
|
|
$foreignDisplay === '' |
955
|
|
|
? '' |
956
|
|
|
: ' OR ' . Util::backquote($foreignDisplay) |
957
|
|
|
. ' LIKE ' . $this->dbi->quoteString( |
958
|
|
|
'%' . $this->dbi->escapeMysqlWildcards($foreignFilter) . '%', |
959
|
|
|
) |
960
|
|
|
); |
961
|
|
|
$fQueryOrder = $foreignDisplay === '' ? '' : ' ORDER BY ' |
962
|
|
|
. Util::backquote($foreignTable) . '.' |
963
|
|
|
. Util::backquote($foreignDisplay); |
964
|
|
|
|
965
|
|
|
$fQueryLimit = $foreignLimit; |
966
|
|
|
|
967
|
|
|
if ($foreignFilter !== '') { |
968
|
|
|
$theTotal = $this->dbi->fetchValue('SELECT COUNT(*)' . $fQueryFrom . $fQueryFilter); |
969
|
|
|
if ($theTotal === false) { |
970
|
|
|
$theTotal = 0; |
971
|
|
|
} |
972
|
|
|
} |
973
|
|
|
|
974
|
|
|
$disp = $this->dbi->tryQuery($fQueryMain . $fQueryFrom . $fQueryFilter . $fQueryOrder . $fQueryLimit); |
975
|
|
|
if ($disp && $disp->numRows() > 0) { |
976
|
|
|
// If a resultset has been created, pre-cache it in the $disp_row |
977
|
|
|
// array. This helps us from not needing to use mysql_data_seek by |
978
|
|
|
// accessing a pre-cached PHP array. Usually those resultsets are |
979
|
|
|
// not that big, so a performance hit should not be expected. |
980
|
|
|
$dispRow = $disp->fetchAllAssoc(); |
981
|
|
|
} else { |
982
|
|
|
// Either no data in the foreign table or |
983
|
|
|
// user does not have select permission to foreign table/field |
984
|
|
|
// Show an input field with a 'Browse foreign values' link |
985
|
|
|
$dispRow = null; |
986
|
|
|
$foreignLink = true; |
987
|
|
|
} |
988
|
|
|
} else { |
989
|
|
|
$dispRow = null; |
990
|
|
|
$foreignLink = true; |
991
|
|
|
} |
992
|
|
|
} while (false); |
993
|
|
|
|
994
|
|
|
if ($getTotal && isset($foreignDb, $foreignTable)) { |
995
|
|
|
$theTotal = $this->dbi->getTable($foreignDb, $foreignTable) |
996
|
|
|
->countRecords(true); |
997
|
|
|
} |
998
|
|
|
|
999
|
|
|
return [ |
1000
|
|
|
'foreign_link' => $foreignLink, |
1001
|
|
|
'the_total' => $theTotal, |
1002
|
|
|
'foreign_display' => is_string($foreignDisplay) ? $foreignDisplay : '', |
1003
|
|
|
'disp_row' => $dispRow, |
1004
|
|
|
'foreign_field' => $foreignField, |
1005
|
|
|
]; |
1006
|
|
|
} |
1007
|
|
|
|
1008
|
|
|
/** |
1009
|
|
|
* Rename a field in relation tables |
1010
|
|
|
* |
1011
|
|
|
* usually called after a column in a table was renamed |
1012
|
|
|
* |
1013
|
|
|
* @param string $db database name |
1014
|
|
|
* @param string $table table name |
1015
|
|
|
* @param string $field old field name |
1016
|
|
|
* @param string $newName new field name |
1017
|
|
|
*/ |
1018
|
|
|
public function renameField(string $db, string $table, string $field, string $newName): void |
1019
|
|
|
{ |
1020
|
|
|
$relationParameters = $this->getRelationParameters(); |
1021
|
|
|
|
1022
|
|
|
if ($relationParameters->displayFeature !== null) { |
1023
|
|
|
$tableQuery = 'UPDATE ' |
1024
|
|
|
. Util::backquote($relationParameters->displayFeature->database) . '.' |
1025
|
|
|
. Util::backquote($relationParameters->displayFeature->tableInfo) |
1026
|
|
|
. ' SET display_field = ' . $this->dbi->quoteString($newName, ConnectionType::ControlUser) |
1027
|
|
|
. ' WHERE db_name = ' . $this->dbi->quoteString($db, ConnectionType::ControlUser) |
1028
|
|
|
. ' AND table_name = ' . $this->dbi->quoteString($table, ConnectionType::ControlUser) |
1029
|
|
|
. ' AND display_field = ' . $this->dbi->quoteString($field, ConnectionType::ControlUser); |
1030
|
|
|
$this->dbi->queryAsControlUser($tableQuery); |
1031
|
|
|
} |
1032
|
|
|
|
1033
|
|
|
if ($relationParameters->relationFeature === null) { |
1034
|
|
|
return; |
1035
|
|
|
} |
1036
|
|
|
|
1037
|
|
|
$tableQuery = 'UPDATE ' |
1038
|
|
|
. Util::backquote($relationParameters->relationFeature->database) . '.' |
1039
|
|
|
. Util::backquote($relationParameters->relationFeature->relation) |
1040
|
|
|
. ' SET master_field = ' . $this->dbi->quoteString($newName, ConnectionType::ControlUser) |
1041
|
|
|
. ' WHERE master_db = ' . $this->dbi->quoteString($db, ConnectionType::ControlUser) |
1042
|
|
|
. ' AND master_table = ' . $this->dbi->quoteString($table, ConnectionType::ControlUser) |
1043
|
|
|
. ' AND master_field = ' . $this->dbi->quoteString($field, ConnectionType::ControlUser); |
1044
|
|
|
$this->dbi->queryAsControlUser($tableQuery); |
1045
|
|
|
|
1046
|
|
|
$tableQuery = 'UPDATE ' |
1047
|
|
|
. Util::backquote($relationParameters->relationFeature->database) . '.' |
1048
|
|
|
. Util::backquote($relationParameters->relationFeature->relation) |
1049
|
|
|
. ' SET foreign_field = ' . $this->dbi->quoteString($newName, ConnectionType::ControlUser) |
1050
|
|
|
. ' WHERE foreign_db = ' . $this->dbi->quoteString($db, ConnectionType::ControlUser) |
1051
|
|
|
. ' AND foreign_table = ' . $this->dbi->quoteString($table, ConnectionType::ControlUser) |
1052
|
|
|
. ' AND foreign_field = ' . $this->dbi->quoteString($field, ConnectionType::ControlUser); |
1053
|
|
|
$this->dbi->queryAsControlUser($tableQuery); |
1054
|
|
|
} |
1055
|
|
|
|
1056
|
|
|
/** |
1057
|
|
|
* Performs SQL query used for renaming table. |
1058
|
|
|
* |
1059
|
|
|
* @param string $sourceDb Source database name |
1060
|
|
|
* @param string $targetDb Target database name |
1061
|
|
|
* @param string $sourceTable Source table name |
1062
|
|
|
* @param string $targetTable Target table name |
1063
|
|
|
* @param string $dbField Name of database field |
1064
|
|
|
* @param string $tableField Name of table field |
1065
|
|
|
*/ |
1066
|
24 |
|
public function renameSingleTable( |
1067
|
|
|
DatabaseName $configStorageDatabase, |
1068
|
|
|
TableName $configStorageTable, |
1069
|
|
|
string $sourceDb, |
1070
|
|
|
string $targetDb, |
1071
|
|
|
string $sourceTable, |
1072
|
|
|
string $targetTable, |
1073
|
|
|
string $dbField, |
1074
|
|
|
string $tableField, |
1075
|
|
|
): void { |
1076
|
24 |
|
$query = 'UPDATE ' |
1077
|
24 |
|
. Util::backquote($configStorageDatabase) . '.' |
1078
|
24 |
|
. Util::backquote($configStorageTable) |
1079
|
24 |
|
. ' SET ' |
1080
|
24 |
|
. $dbField . ' = ' . $this->dbi->quoteString($targetDb, ConnectionType::ControlUser) |
1081
|
24 |
|
. ', ' |
1082
|
24 |
|
. $tableField . ' = ' . $this->dbi->quoteString($targetTable, ConnectionType::ControlUser) |
1083
|
24 |
|
. ' WHERE ' |
1084
|
24 |
|
. $dbField . ' = ' . $this->dbi->quoteString($sourceDb, ConnectionType::ControlUser) |
1085
|
24 |
|
. ' AND ' |
1086
|
24 |
|
. $tableField . ' = ' . $this->dbi->quoteString($sourceTable, ConnectionType::ControlUser); |
1087
|
24 |
|
$this->dbi->queryAsControlUser($query); |
1088
|
|
|
} |
1089
|
|
|
|
1090
|
|
|
/** |
1091
|
|
|
* Rename a table in relation tables |
1092
|
|
|
* |
1093
|
|
|
* usually called after table has been moved |
1094
|
|
|
* |
1095
|
|
|
* @param string $sourceDb Source database name |
1096
|
|
|
* @param string $targetDb Target database name |
1097
|
|
|
* @param string $sourceTable Source table name |
1098
|
|
|
* @param string $targetTable Target table name |
1099
|
|
|
*/ |
1100
|
28 |
|
public function renameTable(string $sourceDb, string $targetDb, string $sourceTable, string $targetTable): void |
1101
|
|
|
{ |
1102
|
28 |
|
$relationParameters = $this->getRelationParameters(); |
1103
|
|
|
|
1104
|
|
|
// Move old entries from PMA-DBs to new table |
1105
|
28 |
|
if ($relationParameters->columnCommentsFeature !== null) { |
1106
|
4 |
|
$this->renameSingleTable( |
1107
|
4 |
|
$relationParameters->columnCommentsFeature->database, |
1108
|
4 |
|
$relationParameters->columnCommentsFeature->columnInfo, |
1109
|
4 |
|
$sourceDb, |
1110
|
4 |
|
$targetDb, |
1111
|
4 |
|
$sourceTable, |
1112
|
4 |
|
$targetTable, |
1113
|
4 |
|
'db_name', |
1114
|
4 |
|
'table_name', |
1115
|
4 |
|
); |
1116
|
|
|
} |
1117
|
|
|
|
1118
|
|
|
// updating bookmarks is not possible since only a single table is |
1119
|
|
|
// moved, and not the whole DB. |
1120
|
|
|
|
1121
|
28 |
|
if ($relationParameters->displayFeature !== null) { |
1122
|
4 |
|
$this->renameSingleTable( |
1123
|
4 |
|
$relationParameters->displayFeature->database, |
1124
|
4 |
|
$relationParameters->displayFeature->tableInfo, |
1125
|
4 |
|
$sourceDb, |
1126
|
4 |
|
$targetDb, |
1127
|
4 |
|
$sourceTable, |
1128
|
4 |
|
$targetTable, |
1129
|
4 |
|
'db_name', |
1130
|
4 |
|
'table_name', |
1131
|
4 |
|
); |
1132
|
|
|
} |
1133
|
|
|
|
1134
|
28 |
|
if ($relationParameters->relationFeature !== null) { |
1135
|
4 |
|
$this->renameSingleTable( |
1136
|
4 |
|
$relationParameters->relationFeature->database, |
1137
|
4 |
|
$relationParameters->relationFeature->relation, |
1138
|
4 |
|
$sourceDb, |
1139
|
4 |
|
$targetDb, |
1140
|
4 |
|
$sourceTable, |
1141
|
4 |
|
$targetTable, |
1142
|
4 |
|
'foreign_db', |
1143
|
4 |
|
'foreign_table', |
1144
|
4 |
|
); |
1145
|
|
|
|
1146
|
4 |
|
$this->renameSingleTable( |
1147
|
4 |
|
$relationParameters->relationFeature->database, |
1148
|
4 |
|
$relationParameters->relationFeature->relation, |
1149
|
4 |
|
$sourceDb, |
1150
|
4 |
|
$targetDb, |
1151
|
4 |
|
$sourceTable, |
1152
|
4 |
|
$targetTable, |
1153
|
4 |
|
'master_db', |
1154
|
4 |
|
'master_table', |
1155
|
4 |
|
); |
1156
|
|
|
} |
1157
|
|
|
|
1158
|
28 |
|
if ($relationParameters->pdfFeature !== null) { |
1159
|
8 |
|
if ($sourceDb === $targetDb) { |
1160
|
|
|
// rename within the database can be handled |
1161
|
4 |
|
$this->renameSingleTable( |
1162
|
4 |
|
$relationParameters->pdfFeature->database, |
1163
|
4 |
|
$relationParameters->pdfFeature->tableCoords, |
1164
|
4 |
|
$sourceDb, |
1165
|
4 |
|
$targetDb, |
1166
|
4 |
|
$sourceTable, |
1167
|
4 |
|
$targetTable, |
1168
|
4 |
|
'db_name', |
1169
|
4 |
|
'table_name', |
1170
|
4 |
|
); |
1171
|
|
|
} else { |
1172
|
|
|
// if the table is moved out of the database we can no longer keep the |
1173
|
|
|
// record for table coordinate |
1174
|
4 |
|
$removeQuery = 'DELETE FROM ' |
1175
|
4 |
|
. Util::backquote($relationParameters->pdfFeature->database) . '.' |
1176
|
4 |
|
. Util::backquote($relationParameters->pdfFeature->tableCoords) |
1177
|
4 |
|
. ' WHERE db_name = ' . $this->dbi->quoteString($sourceDb, ConnectionType::ControlUser) |
1178
|
4 |
|
. ' AND table_name = ' . $this->dbi->quoteString($sourceTable, ConnectionType::ControlUser); |
1179
|
4 |
|
$this->dbi->queryAsControlUser($removeQuery); |
1180
|
|
|
} |
1181
|
|
|
} |
1182
|
|
|
|
1183
|
28 |
|
if ($relationParameters->uiPreferencesFeature !== null) { |
1184
|
4 |
|
$this->renameSingleTable( |
1185
|
4 |
|
$relationParameters->uiPreferencesFeature->database, |
1186
|
4 |
|
$relationParameters->uiPreferencesFeature->tableUiPrefs, |
1187
|
4 |
|
$sourceDb, |
1188
|
4 |
|
$targetDb, |
1189
|
4 |
|
$sourceTable, |
1190
|
4 |
|
$targetTable, |
1191
|
4 |
|
'db_name', |
1192
|
4 |
|
'table_name', |
1193
|
4 |
|
); |
1194
|
|
|
} |
1195
|
|
|
|
1196
|
28 |
|
if ($relationParameters->navigationItemsHidingFeature === null) { |
1197
|
24 |
|
return; |
1198
|
|
|
} |
1199
|
|
|
|
1200
|
|
|
// update hidden items inside table |
1201
|
4 |
|
$this->renameSingleTable( |
1202
|
4 |
|
$relationParameters->navigationItemsHidingFeature->database, |
1203
|
4 |
|
$relationParameters->navigationItemsHidingFeature->navigationHiding, |
1204
|
4 |
|
$sourceDb, |
1205
|
4 |
|
$targetDb, |
1206
|
4 |
|
$sourceTable, |
1207
|
4 |
|
$targetTable, |
1208
|
4 |
|
'db_name', |
1209
|
4 |
|
'table_name', |
1210
|
4 |
|
); |
1211
|
|
|
|
1212
|
|
|
// update data for hidden table |
1213
|
4 |
|
$query = 'UPDATE ' |
1214
|
4 |
|
. Util::backquote($relationParameters->navigationItemsHidingFeature->database) . '.' |
1215
|
4 |
|
. Util::backquote($relationParameters->navigationItemsHidingFeature->navigationHiding) |
1216
|
4 |
|
. ' SET db_name = ' . $this->dbi->quoteString($targetDb, ConnectionType::ControlUser) |
1217
|
4 |
|
. ',' |
1218
|
4 |
|
. ' item_name = ' . $this->dbi->quoteString($targetTable, ConnectionType::ControlUser) |
1219
|
4 |
|
. ' WHERE db_name = ' . $this->dbi->quoteString($sourceDb, ConnectionType::ControlUser) |
1220
|
4 |
|
. ' AND item_name = ' . $this->dbi->quoteString($sourceTable, ConnectionType::ControlUser) |
1221
|
4 |
|
. " AND item_type = 'table'"; |
1222
|
4 |
|
$this->dbi->queryAsControlUser($query); |
1223
|
|
|
} |
1224
|
|
|
|
1225
|
|
|
/** |
1226
|
|
|
* Create a PDF page |
1227
|
|
|
* |
1228
|
|
|
* @param string|null $newpage name of the new PDF page |
1229
|
|
|
* @param string $db database name |
1230
|
|
|
*/ |
1231
|
|
|
public function createPage(string|null $newpage, PdfFeature $pdfFeature, string $db): int |
1232
|
|
|
{ |
1233
|
|
|
$insQuery = 'INSERT INTO ' |
1234
|
|
|
. Util::backquote($pdfFeature->database) . '.' |
1235
|
|
|
. Util::backquote($pdfFeature->pdfPages) |
1236
|
|
|
. ' (db_name, page_descr)' |
1237
|
|
|
. ' VALUES (' |
1238
|
|
|
. $this->dbi->quoteString($db, ConnectionType::ControlUser) . ', ' |
1239
|
|
|
. $this->dbi->quoteString( |
1240
|
|
|
$newpage !== null && $newpage !== '' ? $newpage : __('no description'), |
1241
|
|
|
ConnectionType::ControlUser, |
1242
|
|
|
) . ')'; |
1243
|
|
|
$this->dbi->tryQueryAsControlUser($insQuery); |
1244
|
|
|
|
1245
|
|
|
return $this->dbi->insertId(ConnectionType::ControlUser); |
1246
|
|
|
} |
1247
|
|
|
|
1248
|
|
|
/** |
1249
|
|
|
* Get child table references for a table column. |
1250
|
|
|
* This works only if 'DisableIS' is false. An empty array is returned otherwise. |
1251
|
|
|
* |
1252
|
|
|
* @param string $db name of master table db. |
1253
|
|
|
* @param string $table name of master table. |
1254
|
|
|
* @param string $column name of master table column. |
1255
|
|
|
* |
1256
|
|
|
* @return mixed[] |
1257
|
|
|
*/ |
1258
|
|
|
public function getChildReferences(string $db, string $table, string $column = ''): array |
1259
|
|
|
{ |
1260
|
|
|
if (! $this->config->selectedServer['DisableIS']) { |
1261
|
|
|
$relQuery = 'SELECT `column_name`, `table_name`,' |
1262
|
|
|
. ' `table_schema`, `referenced_column_name`' |
1263
|
|
|
. ' FROM `information_schema`.`key_column_usage`' |
1264
|
|
|
. ' WHERE `referenced_table_name` = ' |
1265
|
|
|
. $this->dbi->quoteString($table) |
1266
|
|
|
. ' AND `referenced_table_schema` = ' |
1267
|
|
|
. $this->dbi->quoteString($db); |
1268
|
|
|
if ($column !== '') { |
1269
|
|
|
$relQuery .= ' AND `referenced_column_name` = ' |
1270
|
|
|
. $this->dbi->quoteString($column); |
1271
|
|
|
} |
1272
|
|
|
|
1273
|
|
|
return $this->dbi->fetchResult( |
1274
|
|
|
$relQuery, |
1275
|
|
|
['referenced_column_name', null], |
1276
|
|
|
); |
1277
|
|
|
} |
1278
|
|
|
|
1279
|
|
|
return []; |
1280
|
|
|
} |
1281
|
|
|
|
1282
|
|
|
/** |
1283
|
|
|
* Check child table references and foreign key for a table column. |
1284
|
|
|
* |
1285
|
|
|
* @param string $db name of master table db. |
1286
|
|
|
* @param string $table name of master table. |
1287
|
|
|
* @param string $column name of master table column. |
1288
|
|
|
* @param mixed[]|null $foreignersFull foreigners array for the whole table. |
1289
|
|
|
* @param mixed[]|null $childReferencesFull child references for the whole table. |
1290
|
|
|
* |
1291
|
|
|
* @return array<string, mixed> telling about references if foreign key. |
1292
|
|
|
* @psalm-return array{isEditable: bool, isForeignKey: bool, isReferenced: bool, references: string[]} |
1293
|
|
|
*/ |
1294
|
|
|
public function checkChildForeignReferences( |
1295
|
|
|
string $db, |
1296
|
|
|
string $table, |
1297
|
|
|
string $column, |
1298
|
|
|
array|null $foreignersFull = null, |
1299
|
|
|
array|null $childReferencesFull = null, |
1300
|
|
|
): array { |
1301
|
|
|
$columnStatus = ['isEditable' => true, 'isReferenced' => false, 'isForeignKey' => false, 'references' => []]; |
1302
|
|
|
|
1303
|
|
|
$foreigners = []; |
1304
|
|
|
if ($foreignersFull !== null) { |
1305
|
|
|
if (isset($foreignersFull[$column])) { |
1306
|
|
|
$foreigners[$column] = $foreignersFull[$column]; |
1307
|
|
|
} |
1308
|
|
|
|
1309
|
|
|
if (isset($foreignersFull['foreign_keys_data'])) { |
1310
|
|
|
$foreigners['foreign_keys_data'] = $foreignersFull['foreign_keys_data']; |
1311
|
|
|
} |
1312
|
|
|
} else { |
1313
|
|
|
$foreigners = $this->getForeigners($db, $table, $column, 'foreign'); |
1314
|
|
|
} |
1315
|
|
|
|
1316
|
|
|
$foreigner = $this->searchColumnInForeigners($foreigners, $column); |
1317
|
|
|
|
1318
|
|
|
$childReferences = []; |
1319
|
|
|
if ($childReferencesFull !== null) { |
1320
|
|
|
if (isset($childReferencesFull[$column])) { |
1321
|
|
|
$childReferences = $childReferencesFull[$column]; |
1322
|
|
|
} |
1323
|
|
|
} else { |
1324
|
|
|
$childReferences = $this->getChildReferences($db, $table, $column); |
1325
|
|
|
} |
1326
|
|
|
|
1327
|
|
|
if (count($childReferences) > 0 || $foreigner) { |
1328
|
|
|
$columnStatus['isEditable'] = false; |
1329
|
|
|
if (count($childReferences) > 0) { |
1330
|
|
|
$columnStatus['isReferenced'] = true; |
1331
|
|
|
foreach ($childReferences as $columns) { |
1332
|
|
|
$columnStatus['references'][] = Util::backquote($columns['table_schema']) |
1333
|
|
|
. '.' . Util::backquote($columns['table_name']); |
1334
|
|
|
} |
1335
|
|
|
} |
1336
|
|
|
|
1337
|
|
|
if ($foreigner) { |
1338
|
|
|
$columnStatus['isForeignKey'] = true; |
1339
|
|
|
} |
1340
|
|
|
} |
1341
|
|
|
|
1342
|
|
|
return $columnStatus; |
1343
|
|
|
} |
1344
|
|
|
|
1345
|
|
|
/** |
1346
|
|
|
* Search a table column in foreign data. |
1347
|
|
|
* |
1348
|
|
|
* @param mixed[] $foreigners Table Foreign data |
1349
|
|
|
* @param string $column Column name |
1350
|
|
|
*/ |
1351
|
8 |
|
public function searchColumnInForeigners(array $foreigners, string $column): array|false |
1352
|
|
|
{ |
1353
|
8 |
|
if (isset($foreigners[$column])) { |
1354
|
|
|
return $foreigners[$column]; |
1355
|
|
|
} |
1356
|
|
|
|
1357
|
8 |
|
if (! isset($foreigners['foreign_keys_data'])) { |
1358
|
4 |
|
return false; |
1359
|
|
|
} |
1360
|
|
|
|
1361
|
4 |
|
$foreigner = []; |
1362
|
4 |
|
foreach ($foreigners['foreign_keys_data'] as $oneKey) { |
1363
|
4 |
|
$columnIndex = array_search($column, $oneKey['index_list']); |
1364
|
4 |
|
if ($columnIndex !== false) { |
1365
|
4 |
|
$foreigner['foreign_field'] = $oneKey['ref_index_list'][$columnIndex]; |
1366
|
4 |
|
$foreigner['foreign_db'] = $oneKey['ref_db_name'] ?? Current::$database; |
1367
|
4 |
|
$foreigner['foreign_table'] = $oneKey['ref_table_name']; |
1368
|
4 |
|
$foreigner['constraint'] = $oneKey['constraint']; |
1369
|
4 |
|
$foreigner['on_update'] = $oneKey['on_update'] ?? 'RESTRICT'; |
1370
|
4 |
|
$foreigner['on_delete'] = $oneKey['on_delete'] ?? 'RESTRICT'; |
1371
|
|
|
|
1372
|
4 |
|
return $foreigner; |
1373
|
|
|
} |
1374
|
|
|
} |
1375
|
|
|
|
1376
|
|
|
return false; |
1377
|
|
|
} |
1378
|
|
|
|
1379
|
|
|
/** |
1380
|
|
|
* Returns default PMA table names and their create queries. |
1381
|
|
|
* |
1382
|
|
|
* @param array<string, string> $tableNameReplacements |
1383
|
|
|
* |
1384
|
|
|
* @return array<string, string> table name, create query |
1385
|
|
|
*/ |
1386
|
16 |
|
public function getCreateTableSqlQueries(array $tableNameReplacements): array |
1387
|
|
|
{ |
1388
|
16 |
|
$pmaTables = []; |
1389
|
16 |
|
$createTablesFile = (string) file_get_contents(SQL_DIR . 'create_tables.sql'); |
1390
|
|
|
|
1391
|
16 |
|
$queries = explode(';', $createTablesFile); |
1392
|
|
|
|
1393
|
16 |
|
foreach ($queries as $query) { |
1394
|
16 |
|
if (! preg_match('/CREATE TABLE IF NOT EXISTS `(.*)` \(/', $query, $table)) { |
1395
|
16 |
|
continue; |
1396
|
|
|
} |
1397
|
|
|
|
1398
|
16 |
|
$tableName = $table[1]; |
1399
|
|
|
|
1400
|
|
|
// Replace the table name with another one |
1401
|
16 |
|
if (isset($tableNameReplacements[$tableName])) { |
1402
|
8 |
|
$query = str_replace($tableName, $tableNameReplacements[$tableName], $query); |
1403
|
|
|
} |
1404
|
|
|
|
1405
|
16 |
|
$pmaTables[$tableName] = $query . ';'; |
1406
|
|
|
} |
1407
|
|
|
|
1408
|
16 |
|
return $pmaTables; |
1409
|
|
|
} |
1410
|
|
|
|
1411
|
|
|
/** |
1412
|
|
|
* Create a database to be used as configuration storage |
1413
|
|
|
*/ |
1414
|
12 |
|
public function createPmaDatabase(string $configurationStorageDbName): bool |
1415
|
|
|
{ |
1416
|
12 |
|
$this->dbi->tryQuery( |
1417
|
12 |
|
'CREATE DATABASE IF NOT EXISTS ' . Util::backquote($configurationStorageDbName), |
1418
|
12 |
|
ConnectionType::ControlUser, |
1419
|
12 |
|
); |
1420
|
|
|
|
1421
|
12 |
|
$error = $this->dbi->getError(ConnectionType::ControlUser); |
1422
|
12 |
|
if ($error === '') { |
1423
|
|
|
// Re-build the cache to show the list of tables created or not |
1424
|
|
|
// This is the case when the DB could be created but no tables just after |
1425
|
|
|
// So just purge the cache and show the new configuration storage state |
1426
|
4 |
|
self::$cache = null; |
1427
|
4 |
|
$this->getRelationParameters(); |
1428
|
|
|
|
1429
|
4 |
|
return true; |
1430
|
|
|
} |
1431
|
|
|
|
1432
|
8 |
|
$GLOBALS['message'] = $error; |
1433
|
|
|
|
1434
|
8 |
|
if ($GLOBALS['errno'] === 1044) { |
1435
|
4 |
|
$GLOBALS['message'] = sprintf( |
1436
|
4 |
|
__( |
1437
|
4 |
|
'You do not have necessary privileges to create a database named' |
1438
|
4 |
|
. ' \'%s\'. You may go to \'Operations\' tab of any' |
1439
|
4 |
|
. ' database to set up the phpMyAdmin configuration storage there.', |
1440
|
4 |
|
), |
1441
|
4 |
|
$configurationStorageDbName, |
1442
|
4 |
|
); |
1443
|
|
|
} |
1444
|
|
|
|
1445
|
8 |
|
return false; |
1446
|
|
|
} |
1447
|
|
|
|
1448
|
|
|
/** |
1449
|
|
|
* Creates PMA tables in the given db, updates if already exists. |
1450
|
|
|
* |
1451
|
|
|
* @param string $db database |
1452
|
|
|
* @param bool $create whether to create tables if they don't exist. |
1453
|
|
|
*/ |
1454
|
48 |
|
public function fixPmaTables(string $db, bool $create = true): void |
1455
|
|
|
{ |
1456
|
48 |
|
if ($this->arePmadbTablesAllDisabled()) { |
1457
|
|
|
return; |
1458
|
|
|
} |
1459
|
|
|
|
1460
|
48 |
|
$tablesToFeatures = [ |
1461
|
48 |
|
'pma__bookmark' => 'bookmarktable', |
1462
|
48 |
|
'pma__relation' => 'relation', |
1463
|
48 |
|
'pma__table_info' => 'table_info', |
1464
|
48 |
|
'pma__table_coords' => 'table_coords', |
1465
|
48 |
|
'pma__pdf_pages' => 'pdf_pages', |
1466
|
48 |
|
'pma__column_info' => 'column_info', |
1467
|
48 |
|
'pma__history' => 'history', |
1468
|
48 |
|
'pma__recent' => 'recent', |
1469
|
48 |
|
'pma__favorite' => 'favorite', |
1470
|
48 |
|
'pma__table_uiprefs' => 'table_uiprefs', |
1471
|
48 |
|
'pma__tracking' => 'tracking', |
1472
|
48 |
|
'pma__userconfig' => 'userconfig', |
1473
|
48 |
|
'pma__users' => 'users', |
1474
|
48 |
|
'pma__usergroups' => 'usergroups', |
1475
|
48 |
|
'pma__navigationhiding' => 'navigationhiding', |
1476
|
48 |
|
'pma__savedsearches' => 'savedsearches', |
1477
|
48 |
|
'pma__central_columns' => 'central_columns', |
1478
|
48 |
|
'pma__designer_settings' => 'designer_settings', |
1479
|
48 |
|
'pma__export_templates' => 'export_templates', |
1480
|
48 |
|
]; |
1481
|
|
|
|
1482
|
48 |
|
$existingTables = $this->dbi->getTables($db, ConnectionType::ControlUser); |
1483
|
|
|
|
1484
|
48 |
|
$tableNameReplacements = $this->getTableReplacementNames($tablesToFeatures); |
1485
|
|
|
|
1486
|
48 |
|
$createQueries = []; |
1487
|
48 |
|
if ($create) { |
1488
|
12 |
|
$createQueries = $this->getCreateTableSqlQueries($tableNameReplacements); |
1489
|
12 |
|
if (! $this->dbi->selectDb($db, ConnectionType::ControlUser)) { |
1490
|
|
|
$GLOBALS['message'] = $this->dbi->getError(ConnectionType::ControlUser); |
1491
|
|
|
|
1492
|
|
|
return; |
1493
|
|
|
} |
1494
|
|
|
} |
1495
|
|
|
|
1496
|
48 |
|
$foundOne = false; |
1497
|
48 |
|
foreach ($tablesToFeatures as $table => $feature) { |
1498
|
48 |
|
if (($this->config->selectedServer[$feature] ?? null) === false) { |
1499
|
|
|
// The feature is disabled by the user in config |
1500
|
8 |
|
continue; |
1501
|
|
|
} |
1502
|
|
|
|
1503
|
|
|
// Check if the table already exists |
1504
|
|
|
// use the possible replaced name first and fallback on the table name |
1505
|
|
|
// if no replacement exists |
1506
|
48 |
|
if (! in_array($tableNameReplacements[$table] ?? $table, $existingTables, true)) { |
1507
|
48 |
|
if (! $create) { |
1508
|
36 |
|
continue; |
1509
|
|
|
} |
1510
|
|
|
|
1511
|
12 |
|
$this->dbi->tryQuery($createQueries[$table], ConnectionType::ControlUser); |
1512
|
|
|
|
1513
|
12 |
|
$error = $this->dbi->getError(ConnectionType::ControlUser); |
1514
|
12 |
|
if ($error !== '') { |
1515
|
4 |
|
$GLOBALS['message'] = $error; |
1516
|
|
|
|
1517
|
4 |
|
return; |
1518
|
|
|
} |
1519
|
|
|
} |
1520
|
|
|
|
1521
|
32 |
|
$foundOne = true; |
1522
|
|
|
|
1523
|
|
|
// Do not override a user defined value, only fill if empty |
1524
|
32 |
|
if (isset($this->config->selectedServer[$feature]) && $this->config->selectedServer[$feature] !== '') { |
1525
|
12 |
|
continue; |
1526
|
|
|
} |
1527
|
|
|
|
1528
|
|
|
// Fill it with the default table name |
1529
|
24 |
|
$this->config->selectedServer[$feature] = $table; |
1530
|
|
|
} |
1531
|
|
|
|
1532
|
44 |
|
if (! $foundOne) { |
1533
|
16 |
|
return; |
1534
|
|
|
} |
1535
|
|
|
|
1536
|
32 |
|
$this->config->selectedServer['pmadb'] = $db; |
1537
|
|
|
|
1538
|
|
|
// Unset the cache as new tables might have been added |
1539
|
32 |
|
self::$cache = null; |
1540
|
|
|
// Fill back the cache |
1541
|
32 |
|
$this->getRelationParameters(); |
1542
|
|
|
} |
1543
|
|
|
|
1544
|
|
|
/** |
1545
|
|
|
* Gets the relations info and status, depending on the condition |
1546
|
|
|
* |
1547
|
|
|
* @param bool $condition whether to look for foreigners or not |
1548
|
|
|
* @param string $db database name |
1549
|
|
|
* @param string $table table name |
1550
|
|
|
* |
1551
|
|
|
* @return mixed[] |
1552
|
|
|
*/ |
1553
|
|
|
public function getRelationsAndStatus(bool $condition, string $db, string $table): array |
1554
|
|
|
{ |
1555
|
|
|
if ($condition) { |
1556
|
|
|
// Find which tables are related with the current one and write it in an array |
1557
|
|
|
return $this->getForeigners($db, $table); |
1558
|
|
|
} |
1559
|
|
|
|
1560
|
|
|
return []; |
1561
|
|
|
} |
1562
|
|
|
|
1563
|
|
|
/** |
1564
|
|
|
* Verifies that all pmadb features are disabled |
1565
|
|
|
*/ |
1566
|
60 |
|
public function arePmadbTablesAllDisabled(): bool |
1567
|
|
|
{ |
1568
|
60 |
|
return ($this->config->selectedServer['bookmarktable'] ?? null) === false |
1569
|
60 |
|
&& ($this->config->selectedServer['relation'] ?? null) === false |
1570
|
60 |
|
&& ($this->config->selectedServer['table_info'] ?? null) === false |
1571
|
60 |
|
&& ($this->config->selectedServer['table_coords'] ?? null) === false |
1572
|
60 |
|
&& ($this->config->selectedServer['column_info'] ?? null) === false |
1573
|
60 |
|
&& ($this->config->selectedServer['pdf_pages'] ?? null) === false |
1574
|
60 |
|
&& ($this->config->selectedServer['history'] ?? null) === false |
1575
|
60 |
|
&& ($this->config->selectedServer['recent'] ?? null) === false |
1576
|
60 |
|
&& ($this->config->selectedServer['favorite'] ?? null) === false |
1577
|
60 |
|
&& ($this->config->selectedServer['table_uiprefs'] ?? null) === false |
1578
|
60 |
|
&& ($this->config->selectedServer['tracking'] ?? null) === false |
1579
|
60 |
|
&& ($this->config->selectedServer['userconfig'] ?? null) === false |
1580
|
60 |
|
&& ($this->config->selectedServer['users'] ?? null) === false |
1581
|
60 |
|
&& ($this->config->selectedServer['usergroups'] ?? null) === false |
1582
|
60 |
|
&& ($this->config->selectedServer['navigationhiding'] ?? null) === false |
1583
|
60 |
|
&& ($this->config->selectedServer['savedsearches'] ?? null) === false |
1584
|
60 |
|
&& ($this->config->selectedServer['central_columns'] ?? null) === false |
1585
|
60 |
|
&& ($this->config->selectedServer['designer_settings'] ?? null) === false |
1586
|
60 |
|
&& ($this->config->selectedServer['export_templates'] ?? null) === false; |
1587
|
|
|
} |
1588
|
|
|
|
1589
|
|
|
/** |
1590
|
|
|
* Verifies if all the pmadb tables are defined |
1591
|
|
|
*/ |
1592
|
4 |
|
public function arePmadbTablesDefined(): bool |
1593
|
|
|
{ |
1594
|
4 |
|
return ! (empty($this->config->selectedServer['bookmarktable']) |
1595
|
4 |
|
|| empty($this->config->selectedServer['relation']) |
1596
|
4 |
|
|| empty($this->config->selectedServer['table_info']) |
1597
|
4 |
|
|| empty($this->config->selectedServer['table_coords']) |
1598
|
4 |
|
|| empty($this->config->selectedServer['column_info']) |
1599
|
4 |
|
|| empty($this->config->selectedServer['pdf_pages']) |
1600
|
4 |
|
|| empty($this->config->selectedServer['history']) |
1601
|
4 |
|
|| empty($this->config->selectedServer['recent']) |
1602
|
4 |
|
|| empty($this->config->selectedServer['favorite']) |
1603
|
4 |
|
|| empty($this->config->selectedServer['table_uiprefs']) |
1604
|
4 |
|
|| empty($this->config->selectedServer['tracking']) |
1605
|
4 |
|
|| empty($this->config->selectedServer['userconfig']) |
1606
|
4 |
|
|| empty($this->config->selectedServer['users']) |
1607
|
4 |
|
|| empty($this->config->selectedServer['usergroups']) |
1608
|
4 |
|
|| empty($this->config->selectedServer['navigationhiding']) |
1609
|
4 |
|
|| empty($this->config->selectedServer['savedsearches']) |
1610
|
4 |
|
|| empty($this->config->selectedServer['central_columns']) |
1611
|
4 |
|
|| empty($this->config->selectedServer['designer_settings']) |
1612
|
4 |
|
|| empty($this->config->selectedServer['export_templates'])); |
1613
|
|
|
} |
1614
|
|
|
|
1615
|
|
|
/** |
1616
|
|
|
* Get tables for foreign key constraint |
1617
|
|
|
* |
1618
|
|
|
* @param string $foreignDb Database name |
1619
|
|
|
* @param string $tblStorageEngine Table storage engine |
1620
|
|
|
* |
1621
|
|
|
* @return mixed[] Table names |
1622
|
|
|
*/ |
1623
|
|
|
public function getTables(string $foreignDb, string $tblStorageEngine): array |
1624
|
|
|
{ |
1625
|
|
|
$tables = []; |
1626
|
|
|
$tablesRows = $this->dbi->query('SHOW TABLE STATUS FROM ' . Util::backquote($foreignDb)); |
1627
|
|
|
while ($row = $tablesRows->fetchRow()) { |
1628
|
|
|
if (! isset($row[1]) || mb_strtoupper($row[1]) !== $tblStorageEngine) { |
|
|
|
|
1629
|
|
|
continue; |
1630
|
|
|
} |
1631
|
|
|
|
1632
|
|
|
$tables[] = $row[0]; |
1633
|
|
|
} |
1634
|
|
|
|
1635
|
|
|
if ($this->config->settings['NaturalOrder']) { |
1636
|
|
|
usort($tables, strnatcasecmp(...)); |
1637
|
|
|
} |
1638
|
|
|
|
1639
|
|
|
return $tables; |
1640
|
|
|
} |
1641
|
|
|
|
1642
|
|
|
public function getConfigurationStorageDbName(): string |
1643
|
|
|
{ |
1644
|
|
|
$cfgStorageDbName = $this->config->selectedServer['pmadb'] ?? ''; |
1645
|
|
|
|
1646
|
|
|
// Use "phpmyadmin" as a default database name to check to keep the behavior consistent |
1647
|
|
|
return empty($cfgStorageDbName) ? 'phpmyadmin' : $cfgStorageDbName; |
1648
|
|
|
} |
1649
|
|
|
|
1650
|
|
|
/** |
1651
|
|
|
* This function checks and initializes the phpMyAdmin configuration |
1652
|
|
|
* storage state before it is used into session cache. |
1653
|
|
|
*/ |
1654
|
28 |
|
public function initRelationParamsCache(): void |
1655
|
|
|
{ |
1656
|
28 |
|
$storageDbName = $this->config->selectedServer['pmadb'] ?? ''; |
1657
|
|
|
// Use "phpmyadmin" as a default database name to check to keep the behavior consistent |
1658
|
28 |
|
$storageDbName = $storageDbName !== '' ? $storageDbName : 'phpmyadmin'; |
1659
|
|
|
|
1660
|
|
|
// This will make users not having explicitly listed databases |
1661
|
|
|
// have config values filled by the default phpMyAdmin storage table name values |
1662
|
28 |
|
$this->fixPmaTables($storageDbName, false); |
1663
|
|
|
|
1664
|
|
|
// This global will be changed if fixPmaTables did find one valid table |
1665
|
|
|
// Empty means that until now no pmadb was found eligible |
1666
|
28 |
|
if ($this->config->selectedServer['pmadb'] !== '') { |
1667
|
20 |
|
return; |
1668
|
|
|
} |
1669
|
|
|
|
1670
|
8 |
|
$this->fixPmaTables(Current::$database, false); |
1671
|
|
|
} |
1672
|
|
|
|
1673
|
|
|
/** |
1674
|
|
|
* @param non-empty-array<string, string> $tablesToFeatures |
|
|
|
|
1675
|
|
|
* |
1676
|
|
|
* @return array<string, string> |
1677
|
|
|
*/ |
1678
|
48 |
|
private function getTableReplacementNames(array $tablesToFeatures): array |
1679
|
|
|
{ |
1680
|
48 |
|
$tableNameReplacements = []; |
1681
|
|
|
|
1682
|
48 |
|
foreach ($tablesToFeatures as $table => $feature) { |
1683
|
48 |
|
if (empty($this->config->selectedServer[$feature]) || $this->config->selectedServer[$feature] === $table) { |
1684
|
48 |
|
continue; |
1685
|
|
|
} |
1686
|
|
|
|
1687
|
|
|
// Set the replacement to transform the default table name into a custom name |
1688
|
12 |
|
$tableNameReplacements[$table] = $this->config->selectedServer[$feature]; |
1689
|
|
|
} |
1690
|
|
|
|
1691
|
48 |
|
return $tableNameReplacements; |
1692
|
|
|
} |
1693
|
|
|
} |
1694
|
|
|
|
The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g.
excluded_paths: ["lib/*"]
, you can move it to the dependency path list as follows:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths