1 | <?php |
||
2 | /* vim: set expandtab sw=4 ts=4 sts=4: */ |
||
3 | /** |
||
4 | * Holds the PhpMyAdmin\Controllers\Database\DatabaseStructureController |
||
5 | * |
||
6 | * @package PhpMyAdmin\Controllers |
||
7 | */ |
||
8 | declare(strict_types=1); |
||
9 | |||
10 | namespace PhpMyAdmin\Controllers\Database; |
||
11 | |||
12 | use PhpMyAdmin\Charsets; |
||
13 | use PhpMyAdmin\Config\PageSettings; |
||
14 | use PhpMyAdmin\Controllers\DatabaseController; |
||
15 | use PhpMyAdmin\Core; |
||
16 | use PhpMyAdmin\DatabaseInterface; |
||
17 | use PhpMyAdmin\Display\CreateTable; |
||
18 | use PhpMyAdmin\Message; |
||
19 | use PhpMyAdmin\RecentFavoriteTable; |
||
20 | use PhpMyAdmin\Relation; |
||
21 | use PhpMyAdmin\Replication; |
||
22 | use PhpMyAdmin\Response; |
||
23 | use PhpMyAdmin\Sanitize; |
||
24 | use PhpMyAdmin\Template; |
||
25 | use PhpMyAdmin\Tracker; |
||
26 | use PhpMyAdmin\Util; |
||
27 | use PhpMyAdmin\Url; |
||
28 | |||
29 | /** |
||
30 | * Handles database structure logic |
||
31 | * |
||
32 | * @package PhpMyAdmin\Controllers |
||
33 | */ |
||
34 | class DatabaseStructureController extends DatabaseController |
||
35 | { |
||
36 | /** |
||
37 | * @var int Number of tables |
||
38 | */ |
||
39 | protected $_num_tables; |
||
40 | /** |
||
41 | * @var int Current position in the list |
||
42 | */ |
||
43 | protected $_pos; |
||
44 | /** |
||
45 | * @var bool DB is information_schema |
||
46 | */ |
||
47 | protected $_db_is_system_schema; |
||
48 | /** |
||
49 | * @var int Number of tables |
||
50 | */ |
||
51 | protected $_total_num_tables; |
||
52 | /** |
||
53 | * @var array Tables in the database |
||
54 | */ |
||
55 | protected $_tables; |
||
56 | /** |
||
57 | * @var bool whether stats show or not |
||
58 | */ |
||
59 | protected $_is_show_stats; |
||
60 | |||
61 | /** |
||
62 | * @var Relation |
||
63 | */ |
||
64 | private $relation; |
||
65 | |||
66 | /** |
||
67 | * @var Replication |
||
68 | */ |
||
69 | private $replication; |
||
70 | |||
71 | /** |
||
72 | * Constructor |
||
73 | * |
||
74 | * @param Response $response Response object |
||
75 | * @param DatabaseInterface $dbi DatabaseInterface object |
||
76 | * @param string $db Database name |
||
77 | */ |
||
78 | public function __construct($response, $dbi, $db) |
||
79 | { |
||
80 | parent::__construct($response, $dbi, $db); |
||
81 | $this->relation = new Relation($dbi); |
||
82 | $this->replication = new Replication(); |
||
83 | } |
||
84 | |||
85 | /** |
||
86 | * Retrieves databse information for further use |
||
87 | * |
||
88 | * @param string $sub_part Page part name |
||
89 | * |
||
90 | * @return void |
||
91 | */ |
||
92 | private function _getDbInfo($sub_part) |
||
93 | { |
||
94 | list( |
||
95 | $tables, |
||
96 | $num_tables, |
||
97 | $total_num_tables, |
||
98 | , |
||
99 | $is_show_stats, |
||
100 | $db_is_system_schema, |
||
101 | , |
||
102 | , |
||
103 | $pos |
||
104 | ) = Util::getDbInfo($this->db, $sub_part); |
||
105 | |||
106 | $this->_tables = $tables; |
||
107 | $this->_num_tables = $num_tables; |
||
108 | $this->_pos = $pos; |
||
109 | $this->_db_is_system_schema = $db_is_system_schema; |
||
110 | $this->_total_num_tables = $total_num_tables; |
||
111 | $this->_is_show_stats = $is_show_stats; |
||
112 | } |
||
113 | |||
114 | /** |
||
115 | * Index action |
||
116 | * |
||
117 | * @return void |
||
118 | */ |
||
119 | public function indexAction() |
||
120 | { |
||
121 | $response = Response::getInstance(); |
||
122 | |||
123 | // Add/Remove favorite tables using Ajax request. |
||
124 | if ($response->isAjax() && !empty($_REQUEST['favorite_table'])) { |
||
125 | $this->addRemoveFavoriteTablesAction(); |
||
126 | return; |
||
127 | } |
||
128 | |||
129 | // If there is an Ajax request for real row count of a table. |
||
130 | if ($response->isAjax() |
||
131 | && isset($_REQUEST['real_row_count']) |
||
132 | && $_REQUEST['real_row_count'] == true |
||
133 | ) { |
||
134 | $this->handleRealRowCountRequestAction(); |
||
135 | return; |
||
136 | } |
||
137 | |||
138 | // Drops/deletes/etc. multiple tables if required |
||
139 | if ((! empty($_POST['submit_mult']) && isset($_POST['selected_tbl'])) |
||
140 | || isset($_POST['mult_btn']) |
||
141 | ) { |
||
142 | $this->multiSubmitAction(); |
||
143 | } |
||
144 | |||
145 | $this->response->getHeader()->getScripts()->addFiles( |
||
146 | [ |
||
147 | 'db_structure.js', |
||
148 | 'tbl_change.js', |
||
149 | ] |
||
150 | ); |
||
151 | |||
152 | // Gets the database structure |
||
153 | $this->_getDbInfo('_structure'); |
||
154 | |||
155 | // Checks if there are any tables to be shown on current page. |
||
156 | // If there are no tables, the user is redirected to the last page |
||
157 | // having any. |
||
158 | if ($this->_total_num_tables > 0 && $this->_pos > $this->_total_num_tables) { |
||
159 | $uri = './db_structure.php' . Url::getCommonRaw([ |
||
160 | 'db' => $this->db, |
||
161 | 'pos' => max(0, $this->_total_num_tables - $GLOBALS['cfg']['MaxTableList']), |
||
162 | 'reload' => 1 |
||
163 | ]); |
||
164 | Core::sendHeaderLocation($uri); |
||
165 | } |
||
166 | |||
167 | include_once 'libraries/replication.inc.php'; |
||
168 | |||
169 | PageSettings::showGroup('DbStructure'); |
||
170 | |||
171 | // 1. No tables |
||
172 | if ($this->_num_tables == 0) { |
||
173 | $this->response->addHTML( |
||
174 | Message::notice(__('No tables found in database.')) |
||
175 | ); |
||
176 | if (empty($this->_db_is_system_schema)) { |
||
177 | $this->response->addHTML(CreateTable::getHtml($this->db)); |
||
178 | } |
||
179 | return; |
||
180 | } |
||
181 | |||
182 | // else |
||
183 | // 2. Shows table information |
||
184 | /** |
||
185 | * Displays the tables list |
||
186 | */ |
||
187 | $this->response->addHTML('<div id="tableslistcontainer">'); |
||
188 | $_url_params = [ |
||
189 | 'pos' => $this->_pos, |
||
190 | 'db' => $this->db]; |
||
191 | |||
192 | // Add the sort options if they exists |
||
193 | if (isset($_REQUEST['sort'])) { |
||
194 | $_url_params['sort'] = $_REQUEST['sort']; |
||
195 | } |
||
196 | |||
197 | if (isset($_REQUEST['sort_order'])) { |
||
198 | $_url_params['sort_order'] = $_REQUEST['sort_order']; |
||
199 | } |
||
200 | |||
201 | $this->response->addHTML( |
||
202 | Util::getListNavigator( |
||
203 | $this->_total_num_tables, |
||
204 | $this->_pos, |
||
205 | $_url_params, |
||
206 | 'db_structure.php', |
||
207 | 'frame_content', |
||
208 | $GLOBALS['cfg']['MaxTableList'] |
||
209 | ) |
||
210 | ); |
||
211 | |||
212 | $this->displayTableList(); |
||
213 | |||
214 | // display again the table list navigator |
||
215 | $this->response->addHTML( |
||
216 | Util::getListNavigator( |
||
217 | $this->_total_num_tables, |
||
218 | $this->_pos, |
||
219 | $_url_params, |
||
220 | 'db_structure.php', |
||
221 | 'frame_content', |
||
222 | $GLOBALS['cfg']['MaxTableList'] |
||
223 | ) |
||
224 | ); |
||
225 | |||
226 | $this->response->addHTML('</div><hr />'); |
||
227 | |||
228 | /** |
||
229 | * Work on the database |
||
230 | */ |
||
231 | /* DATABASE WORK */ |
||
232 | /* Printable view of a table */ |
||
233 | $this->response->addHTML( |
||
234 | $this->template->render('database/structure/print_view_data_dictionary_link', [ |
||
235 | 'url_query' => Url::getCommon([ |
||
236 | 'db' => $this->db, |
||
237 | 'goto' => 'db_structure.php', |
||
238 | ]) |
||
239 | ]) |
||
240 | ); |
||
241 | |||
242 | if (empty($this->_db_is_system_schema)) { |
||
243 | $this->response->addHTML(CreateTable::getHtml($this->db)); |
||
244 | } |
||
245 | } |
||
246 | |||
247 | /** |
||
248 | * Add or remove favorite tables |
||
249 | * |
||
250 | * @return void |
||
251 | */ |
||
252 | public function addRemoveFavoriteTablesAction() |
||
253 | { |
||
254 | $fav_instance = RecentFavoriteTable::getInstance('favorite'); |
||
255 | if (isset($_REQUEST['favorite_tables'])) { |
||
256 | $favorite_tables = json_decode($_REQUEST['favorite_tables'], true); |
||
257 | } else { |
||
258 | $favorite_tables = []; |
||
259 | } |
||
260 | // Required to keep each user's preferences separate. |
||
261 | $user = sha1($GLOBALS['cfg']['Server']['user']); |
||
262 | |||
263 | // Request for Synchronization of favorite tables. |
||
264 | if (isset($_REQUEST['sync_favorite_tables'])) { |
||
265 | $cfgRelation = $this->relation->getRelationsParam(); |
||
266 | if ($cfgRelation['favoritework']) { |
||
267 | $this->synchronizeFavoriteTables($fav_instance, $user, $favorite_tables); |
||
268 | } |
||
269 | return; |
||
270 | } |
||
271 | $changes = true; |
||
272 | $titles = Util::buildActionTitles(); |
||
273 | $favorite_table = $_REQUEST['favorite_table']; |
||
274 | $already_favorite = $this->checkFavoriteTable($favorite_table); |
||
275 | |||
276 | if (isset($_REQUEST['remove_favorite'])) { |
||
277 | if ($already_favorite) { |
||
278 | // If already in favorite list, remove it. |
||
279 | $fav_instance->remove($this->db, $favorite_table); |
||
280 | $already_favorite = false; // for favorite_anchor template |
||
281 | } |
||
282 | } elseif (isset($_REQUEST['add_favorite'])) { |
||
283 | if (!$already_favorite) { |
||
284 | $nbTables = count($fav_instance->getTables()); |
||
285 | if ($nbTables == $GLOBALS['cfg']['NumFavoriteTables']) { |
||
286 | $changes = false; |
||
287 | } else { |
||
288 | // Otherwise add to favorite list. |
||
289 | $fav_instance->add($this->db, $favorite_table); |
||
290 | $already_favorite = true; // for favorite_anchor template |
||
291 | } |
||
292 | } |
||
293 | } |
||
294 | |||
295 | $favorite_tables[$user] = $fav_instance->getTables(); |
||
296 | $this->response->addJSON('changes', $changes); |
||
297 | if (!$changes) { |
||
298 | $this->response->addJSON( |
||
299 | 'message', |
||
300 | $this->template->render('components/error_message', [ |
||
301 | 'msg' => __("Favorite List is full!"), |
||
302 | ]) |
||
303 | ); |
||
304 | return; |
||
305 | } |
||
306 | // Check if current table is already in favorite list. |
||
307 | $favParams = ['db' => $this->db, |
||
308 | 'ajax_request' => true, |
||
309 | 'favorite_table' => $favorite_table, |
||
310 | (($already_favorite ? 'remove' : 'add') . '_favorite') => true |
||
311 | ]; |
||
312 | $this->response->addJSON([ |
||
313 | 'user' => $user, |
||
314 | 'favorite_tables' => json_encode($favorite_tables), |
||
315 | 'list' => $fav_instance->getHtmlList(), |
||
316 | 'anchor' => $this->template->render('database/structure/favorite_anchor', [ |
||
317 | 'table_name_hash' => md5($favorite_table), |
||
318 | 'db_table_name_hash' => md5($this->db . "." . $favorite_table), |
||
319 | 'fav_params' => $favParams, |
||
320 | 'already_favorite' => $already_favorite, |
||
321 | 'titles' => $titles, |
||
322 | ]), |
||
323 | ]); |
||
324 | } |
||
325 | |||
326 | /** |
||
327 | * Handles request for real row count on database level view page. |
||
328 | * |
||
329 | * @return boolean true |
||
330 | */ |
||
331 | public function handleRealRowCountRequestAction() |
||
332 | { |
||
333 | $ajax_response = $this->response; |
||
334 | // If there is a request to update all table's row count. |
||
335 | if (!isset($_REQUEST['real_row_count_all'])) { |
||
336 | // Get the real row count for the table. |
||
337 | $real_row_count = $this->dbi |
||
338 | ->getTable($this->db, $_REQUEST['table']) |
||
339 | ->getRealRowCountTable(); |
||
340 | // Format the number. |
||
341 | $real_row_count = Util::formatNumber($real_row_count, 0); |
||
342 | $ajax_response->addJSON('real_row_count', $real_row_count); |
||
343 | return; |
||
344 | } |
||
345 | |||
346 | // Array to store the results. |
||
347 | $real_row_count_all = []; |
||
348 | // Iterate over each table and fetch real row count. |
||
349 | foreach ($this->_tables as $table) { |
||
350 | $row_count = $this->dbi |
||
351 | ->getTable($this->db, $table['TABLE_NAME']) |
||
352 | ->getRealRowCountTable(); |
||
353 | $real_row_count_all[] = [ |
||
354 | 'table' => $table['TABLE_NAME'], |
||
355 | 'row_count' => $row_count |
||
356 | ]; |
||
357 | } |
||
358 | |||
359 | $ajax_response->addJSON( |
||
360 | 'real_row_count_all', |
||
361 | json_encode($real_row_count_all) |
||
362 | ); |
||
363 | } |
||
364 | |||
365 | /** |
||
366 | * Handles actions related to multiple tables |
||
367 | * |
||
368 | * @return void |
||
369 | */ |
||
370 | public function multiSubmitAction() |
||
371 | { |
||
372 | $action = 'db_structure.php'; |
||
373 | $err_url = 'db_structure.php' . Url::getCommon( |
||
374 | ['db' => $this->db] |
||
375 | ); |
||
376 | |||
377 | // see bug #2794840; in this case, code path is: |
||
378 | // db_structure.php -> libraries/mult_submits.inc.php -> sql.php |
||
379 | // -> db_structure.php and if we got an error on the multi submit, |
||
380 | // we must display it here and not call again mult_submits.inc.php |
||
381 | if (! isset($_POST['error']) || false === $_POST['error']) { |
||
382 | include 'libraries/mult_submits.inc.php'; |
||
383 | } |
||
384 | if (empty($_POST['message'])) { |
||
385 | $_POST['message'] = Message::success(); |
||
386 | } |
||
387 | } |
||
388 | |||
389 | /** |
||
390 | * Displays the list of tables |
||
391 | * |
||
392 | * @return void |
||
393 | */ |
||
394 | protected function displayTableList() |
||
395 | { |
||
396 | // filtering |
||
397 | $this->response->addHTML( |
||
398 | $this->template->render('filter', ['filter_value' => '']) |
||
399 | ); |
||
400 | // table form |
||
401 | $this->response->addHTML( |
||
402 | $this->template->render('database/structure/table_header', [ |
||
403 | 'db' => $this->db, |
||
404 | 'db_is_system_schema' => $this->_db_is_system_schema, |
||
405 | 'replication' => $GLOBALS['replication_info']['slave']['status'], |
||
406 | 'properties_num_columns' => $GLOBALS['cfg']['PropertiesNumColumns'], |
||
407 | 'is_show_stats' => $GLOBALS['is_show_stats'], |
||
408 | 'show_charset' => $GLOBALS['cfg']['ShowDbStructureCharset'], |
||
409 | 'show_comment' => $GLOBALS['cfg']['ShowDbStructureComment'], |
||
410 | 'show_creation' => $GLOBALS['cfg']['ShowDbStructureCreation'], |
||
411 | 'show_last_update' => $GLOBALS['cfg']['ShowDbStructureLastUpdate'], |
||
412 | 'show_last_check' => $GLOBALS['cfg']['ShowDbStructureLastCheck'], |
||
413 | 'num_favorite_tables' => $GLOBALS['cfg']['NumFavoriteTables'], |
||
414 | ]) |
||
415 | ); |
||
416 | |||
417 | $i = $sum_entries = 0; |
||
418 | $overhead_check = false; |
||
419 | $create_time_all = ''; |
||
420 | $update_time_all = ''; |
||
421 | $check_time_all = ''; |
||
422 | $num_columns = $GLOBALS['cfg']['PropertiesNumColumns'] > 1 |
||
423 | ? ceil($this->_num_tables / $GLOBALS['cfg']['PropertiesNumColumns']) + 1 |
||
424 | : 0; |
||
425 | $row_count = 0; |
||
426 | $sum_size = 0; |
||
427 | $overhead_size = 0; |
||
428 | |||
429 | $hidden_fields = []; |
||
430 | $overall_approx_rows = false; |
||
431 | foreach ($this->_tables as $keyname => $current_table) { |
||
432 | // Get valid statistics whatever is the table type |
||
433 | |||
434 | $drop_query = ''; |
||
435 | $drop_message = ''; |
||
436 | $overhead = ''; |
||
437 | $input_class = ['checkall']; |
||
438 | |||
439 | $table_is_view = false; |
||
440 | // Sets parameters for links |
||
441 | $tbl_url_query = Url::getCommon( |
||
442 | ['db' => $this->db, 'table' => $current_table['TABLE_NAME']] |
||
443 | ); |
||
444 | // do not list the previous table's size info for a view |
||
445 | |||
446 | list($current_table, $formatted_size, $unit, $formatted_overhead, |
||
447 | $overhead_unit, $overhead_size, $table_is_view, $sum_size) |
||
448 | = $this->getStuffForEngineTypeTable( |
||
449 | $current_table, |
||
450 | $sum_size, |
||
451 | $overhead_size |
||
452 | ); |
||
453 | |||
454 | $curTable = $this->dbi |
||
455 | ->getTable($this->db, $current_table['TABLE_NAME']); |
||
456 | if (!$curTable->isMerge()) { |
||
457 | $sum_entries += $current_table['TABLE_ROWS']; |
||
458 | } |
||
459 | |||
460 | if (isset($current_table['Collation'])) { |
||
461 | $collation = '<dfn title="' |
||
462 | . Charsets::getCollationDescr($current_table['Collation']) . '">' |
||
463 | . $current_table['Collation'] . '</dfn>'; |
||
464 | } else { |
||
465 | $collation = '---'; |
||
466 | } |
||
467 | |||
468 | if ($this->_is_show_stats) { |
||
469 | if ($formatted_overhead != '') { |
||
470 | $overhead = '<a href="tbl_structure.php' |
||
471 | . $tbl_url_query . '#showusage">' |
||
472 | . '<span>' . $formatted_overhead . '</span> ' |
||
473 | . '<span class="unit">' . $overhead_unit . '</span>' |
||
474 | . '</a>' . "\n"; |
||
475 | $overhead_check = true; |
||
476 | $input_class[] = 'tbl-overhead'; |
||
477 | } else { |
||
478 | $overhead = '-'; |
||
479 | } |
||
480 | } // end if |
||
481 | |||
482 | if ($GLOBALS['cfg']['ShowDbStructureCharset']) { |
||
483 | if (isset($current_table['Collation'])) { |
||
484 | $charset = mb_substr($collation, 0, mb_strpos($collation, "_")); |
||
485 | } else { |
||
486 | $charset = ''; |
||
487 | } |
||
488 | } |
||
489 | |||
490 | if ($GLOBALS['cfg']['ShowDbStructureCreation']) { |
||
491 | $create_time = isset($current_table['Create_time']) |
||
492 | ? $current_table['Create_time'] : ''; |
||
493 | if ($create_time |
||
494 | && (!$create_time_all |
||
495 | || $create_time < $create_time_all) |
||
496 | ) { |
||
497 | $create_time_all = $create_time; |
||
498 | } |
||
499 | } |
||
500 | |||
501 | if ($GLOBALS['cfg']['ShowDbStructureLastUpdate']) { |
||
502 | $update_time = isset($current_table['Update_time']) |
||
503 | ? $current_table['Update_time'] : ''; |
||
504 | if ($update_time |
||
505 | && (!$update_time_all |
||
506 | || $update_time < $update_time_all) |
||
507 | ) { |
||
508 | $update_time_all = $update_time; |
||
509 | } |
||
510 | } |
||
511 | |||
512 | if ($GLOBALS['cfg']['ShowDbStructureLastCheck']) { |
||
513 | $check_time = isset($current_table['Check_time']) |
||
514 | ? $current_table['Check_time'] : ''; |
||
515 | if ($check_time |
||
516 | && (!$check_time_all |
||
517 | || $check_time < $check_time_all) |
||
518 | ) { |
||
519 | $check_time_all = $check_time; |
||
520 | } |
||
521 | } |
||
522 | |||
523 | $truename = (!empty($tooltip_truename) |
||
524 | && isset($tooltip_truename[$current_table['TABLE_NAME']])) |
||
525 | ? $tooltip_truename[$current_table['TABLE_NAME']] |
||
526 | : $current_table['TABLE_NAME']; |
||
527 | |||
528 | $i++; |
||
529 | |||
530 | $row_count++; |
||
531 | if ($table_is_view) { |
||
532 | $hidden_fields[] = '<input type="hidden" name="views[]" value="' |
||
533 | . htmlspecialchars($current_table['TABLE_NAME']) . '" />'; |
||
534 | } |
||
535 | |||
536 | /* |
||
537 | * Always activate links for Browse, Search and Empty, even if |
||
538 | * the icons are greyed, because |
||
539 | * 1. for views, we don't know the number of rows at this point |
||
540 | * 2. for tables, another source could have populated them since the |
||
541 | * page was generated |
||
542 | * |
||
543 | * I could have used the PHP ternary conditional operator but I find |
||
544 | * the code easier to read without this operator. |
||
545 | */ |
||
546 | $may_have_rows = $current_table['TABLE_ROWS'] > 0 || $table_is_view; |
||
547 | $titles = Util::buildActionTitles(); |
||
548 | |||
549 | if (!$this->_db_is_system_schema) { |
||
550 | $drop_query = sprintf( |
||
551 | 'DROP %s %s', |
||
552 | ($table_is_view || $current_table['ENGINE'] == null) ? 'VIEW' |
||
553 | : 'TABLE', |
||
554 | Util::backquote( |
||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
555 | $current_table['TABLE_NAME'] |
||
556 | ) |
||
557 | ); |
||
558 | $drop_message = sprintf( |
||
559 | (($table_is_view || $current_table['ENGINE'] == null) |
||
560 | ? __('View %s has been dropped.') |
||
561 | : __('Table %s has been dropped.')), |
||
562 | str_replace( |
||
563 | ' ', |
||
564 | ' ', |
||
565 | htmlspecialchars($current_table['TABLE_NAME']) |
||
566 | ) |
||
567 | ); |
||
568 | } |
||
569 | |||
570 | if ($num_columns > 0 |
||
571 | && $this->_num_tables > $num_columns |
||
572 | && ($row_count % $num_columns) == 0 |
||
573 | ) { |
||
574 | $row_count = 1; |
||
575 | |||
576 | $this->response->addHTML( |
||
577 | '</tr></tbody></table></div></form>' |
||
578 | ); |
||
579 | |||
580 | $this->response->addHTML( |
||
581 | $this->template->render('database/structure/table_header', [ |
||
582 | 'db' => $this->db, |
||
583 | 'db_is_system_schema' => $this->_db_is_system_schema, |
||
584 | 'replication' => $GLOBALS['replication_info']['slave']['status'], |
||
585 | 'properties_num_columns' => $GLOBALS['cfg']['PropertiesNumColumns'], |
||
586 | 'is_show_stats' => $GLOBALS['is_show_stats'], |
||
587 | 'show_charset' => $GLOBALS['cfg']['ShowDbStructureCharset'], |
||
588 | 'show_comment' => $GLOBALS['cfg']['ShowDbStructureComment'], |
||
589 | 'show_creation' => $GLOBALS['cfg']['ShowDbStructureCreation'], |
||
590 | 'show_last_update' => $GLOBALS['cfg']['ShowDbStructureLastUpdate'], |
||
591 | 'show_last_check' => $GLOBALS['cfg']['ShowDbStructureLastCheck'], |
||
592 | 'num_favorite_tables' => $GLOBALS['cfg']['NumFavoriteTables'], |
||
593 | ]) |
||
594 | ); |
||
595 | } |
||
596 | |||
597 | list($approx_rows, $show_superscript) = $this->isRowCountApproximated( |
||
598 | $current_table, |
||
599 | $table_is_view |
||
600 | ); |
||
601 | |||
602 | list($do, $ignored) = $this->getReplicationStatus($truename); |
||
603 | |||
604 | $this->response->addHTML( |
||
605 | $this->template->render('database/structure/structure_table_row', [ |
||
606 | 'table_name_hash' => md5($current_table['TABLE_NAME']), |
||
607 | 'db_table_name_hash' => md5($this->db . '.' . $current_table['TABLE_NAME']), |
||
608 | 'db' => $this->db, |
||
609 | 'curr' => $i, |
||
610 | 'input_class' => implode(' ', $input_class), |
||
611 | 'table_is_view' => $table_is_view, |
||
612 | 'current_table' => $current_table, |
||
613 | 'browse_table_title' => $may_have_rows ? $titles['Browse'] : $titles['NoBrowse'], |
||
614 | 'search_table_title' => $may_have_rows ? $titles['Search'] : $titles['NoSearch'], |
||
615 | 'browse_table_label_title' => htmlspecialchars($current_table['TABLE_COMMENT']), |
||
616 | 'browse_table_label_truename' => $truename, |
||
617 | 'empty_table_sql_query' => urlencode( |
||
618 | 'TRUNCATE ' . Util::backquote( |
||
619 | $current_table['TABLE_NAME'] |
||
620 | ) |
||
621 | ), |
||
622 | 'empty_table_message_to_show' => urlencode( |
||
623 | sprintf( |
||
624 | __('Table %s has been emptied.'), |
||
625 | htmlspecialchars( |
||
626 | $current_table['TABLE_NAME'] |
||
627 | ) |
||
628 | ) |
||
629 | ), |
||
630 | 'empty_table_title' => $may_have_rows ? $titles['Empty'] : $titles['NoEmpty'], |
||
631 | 'tracking_icon' => $this->getTrackingIcon($truename), |
||
632 | 'server_slave_status' => $GLOBALS['replication_info']['slave']['status'], |
||
633 | 'tbl_url_query' => $tbl_url_query, |
||
634 | 'db_is_system_schema' => $this->_db_is_system_schema, |
||
635 | 'titles' => $titles, |
||
636 | 'drop_query' => $drop_query, |
||
637 | 'drop_message' => $drop_message, |
||
638 | 'collation' => $collation, |
||
639 | 'formatted_size' => $formatted_size, |
||
640 | 'unit' => $unit, |
||
641 | 'overhead' => $overhead, |
||
642 | 'create_time' => (isset($create_time) && $create_time |
||
643 | ? Util::localisedDate(strtotime($create_time)) : '-'), |
||
644 | 'update_time' => (isset($update_time) && $update_time |
||
645 | ? Util::localisedDate(strtotime($update_time)) : '-'), |
||
646 | 'check_time' => (isset($check_time) && $check_time |
||
647 | ? Util::localisedDate(strtotime($check_time)) : '-'), |
||
648 | 'charset' => isset($charset) |
||
649 | ? $charset : '', |
||
650 | 'is_show_stats' => $this->_is_show_stats, |
||
651 | 'ignored' => $ignored, |
||
652 | 'do' => $do, |
||
653 | 'approx_rows' => $approx_rows, |
||
654 | 'show_superscript' => $show_superscript, |
||
655 | 'already_favorite' => $this->checkFavoriteTable( |
||
656 | $current_table['TABLE_NAME'] |
||
657 | ), |
||
658 | 'num_favorite_tables' => $GLOBALS['cfg']['NumFavoriteTables'], |
||
659 | 'properties_num_columns' => $GLOBALS['cfg']['PropertiesNumColumns'], |
||
660 | 'limit_chars' => $GLOBALS['cfg']['LimitChars'], |
||
661 | 'show_charset' => $GLOBALS['cfg']['ShowDbStructureCharset'], |
||
662 | 'show_comment' => $GLOBALS['cfg']['ShowDbStructureComment'], |
||
663 | 'show_creation' => $GLOBALS['cfg']['ShowDbStructureCreation'], |
||
664 | 'show_last_update' => $GLOBALS['cfg']['ShowDbStructureLastUpdate'], |
||
665 | 'show_last_check' => $GLOBALS['cfg']['ShowDbStructureLastCheck'], |
||
666 | ]) |
||
667 | ); |
||
668 | |||
669 | $overall_approx_rows = $overall_approx_rows || $approx_rows; |
||
670 | } // end foreach |
||
671 | |||
672 | $this->response->addHTML('</tbody>'); |
||
673 | |||
674 | $db_collation = $this->dbi->getDbCollation($this->db); |
||
675 | $db_charset = mb_substr($db_collation, 0, mb_strpos($db_collation, "_")); |
||
676 | |||
677 | // Show Summary |
||
678 | $this->response->addHTML( |
||
679 | $this->template->render('database/structure/body_for_table_summary', [ |
||
680 | 'num_tables' => $this->_num_tables, |
||
681 | 'server_slave_status' => $GLOBALS['replication_info']['slave']['status'], |
||
682 | 'db_is_system_schema' => $this->_db_is_system_schema, |
||
683 | 'sum_entries' => $sum_entries, |
||
684 | 'db_collation' => $db_collation, |
||
685 | 'is_show_stats' => $this->_is_show_stats, |
||
686 | 'db_charset' => $db_charset, |
||
687 | 'sum_size' => $sum_size, |
||
688 | 'overhead_size' => $overhead_size, |
||
689 | 'create_time_all' => ($create_time_all ? Util::localisedDate(strtotime($create_time_all)) : '-'), |
||
690 | 'update_time_all' => ($update_time_all ? Util::localisedDate(strtotime($update_time_all)) : '-'), |
||
691 | 'check_time_all' => ($check_time_all ? Util::localisedDate(strtotime($check_time_all)) : '-'), |
||
692 | 'approx_rows' => $overall_approx_rows, |
||
693 | 'num_favorite_tables' => $GLOBALS['cfg']['NumFavoriteTables'], |
||
694 | 'db' => $GLOBALS['db'], |
||
695 | 'properties_num_columns' => $GLOBALS['cfg']['PropertiesNumColumns'], |
||
696 | 'dbi' => $this->dbi, |
||
697 | 'show_charset' => $GLOBALS['cfg']['ShowDbStructureCharset'], |
||
698 | 'show_comment' => $GLOBALS['cfg']['ShowDbStructureComment'], |
||
699 | 'show_creation' => $GLOBALS['cfg']['ShowDbStructureCreation'], |
||
700 | 'show_last_update' => $GLOBALS['cfg']['ShowDbStructureLastUpdate'], |
||
701 | 'show_last_check' => $GLOBALS['cfg']['ShowDbStructureLastCheck'], |
||
702 | ]) |
||
703 | ); |
||
704 | $this->response->addHTML('</table>'); |
||
705 | |||
706 | //check all |
||
707 | $this->response->addHTML( |
||
708 | $this->template->render('database/structure/check_all_tables', [ |
||
709 | 'pma_theme_image' => $GLOBALS['pmaThemeImage'], |
||
710 | 'text_dir' => $GLOBALS['text_dir'], |
||
711 | 'overhead_check' => $overhead_check, |
||
712 | 'db_is_system_schema' => $this->_db_is_system_schema, |
||
713 | 'hidden_fields' => $hidden_fields, |
||
714 | 'disable_multi_table' => $GLOBALS['cfg']['DisableMultiTableMaintenance'], |
||
715 | 'central_columns_work' => $GLOBALS['cfgRelation']['centralcolumnswork'], |
||
716 | ]) |
||
717 | ); |
||
718 | $this->response->addHTML('</form>'); //end of form |
||
719 | } |
||
720 | |||
721 | /** |
||
722 | * Returns the tracking icon if the table is tracked |
||
723 | * |
||
724 | * @param string $table table name |
||
725 | * |
||
726 | * @return string HTML for tracking icon |
||
727 | */ |
||
728 | protected function getTrackingIcon($table) |
||
729 | { |
||
730 | $tracking_icon = ''; |
||
731 | if (Tracker::isActive()) { |
||
732 | $is_tracked = Tracker::isTracked($this->db, $table); |
||
733 | if ($is_tracked |
||
734 | || Tracker::getVersion($this->db, $table) > 0 |
||
735 | ) { |
||
736 | $tracking_icon = $this->template->render('database/structure/tracking_icon', [ |
||
737 | 'db' => $this->db, |
||
738 | 'table' => $table, |
||
739 | 'is_tracked' => $is_tracked, |
||
740 | ]); |
||
741 | } |
||
742 | } |
||
743 | return $tracking_icon; |
||
744 | } |
||
745 | |||
746 | /** |
||
747 | * Returns whether the row count is approximated |
||
748 | * |
||
749 | * @param array $current_table array containing details about the table |
||
750 | * @param boolean $table_is_view whether the table is a view |
||
751 | * |
||
752 | * @return array |
||
753 | */ |
||
754 | protected function isRowCountApproximated(array $current_table, $table_is_view) |
||
755 | { |
||
756 | $approx_rows = false; |
||
757 | $show_superscript = ''; |
||
758 | |||
759 | // there is a null value in the ENGINE |
||
760 | // - when the table needs to be repaired, or |
||
761 | // - when it's a view |
||
762 | // so ensure that we'll display "in use" below for a table |
||
763 | // that needs to be repaired |
||
764 | if (isset($current_table['TABLE_ROWS']) |
||
765 | && ($current_table['ENGINE'] != null || $table_is_view) |
||
766 | ) { |
||
767 | // InnoDB/TokuDB table: we did not get an accurate row count |
||
768 | $approx_rows = !$table_is_view |
||
769 | && in_array($current_table['ENGINE'], ['InnoDB', 'TokuDB']) |
||
770 | && !$current_table['COUNTED']; |
||
771 | |||
772 | if ($table_is_view |
||
773 | && $current_table['TABLE_ROWS'] >= $GLOBALS['cfg']['MaxExactCountViews'] |
||
774 | ) { |
||
775 | $approx_rows = true; |
||
776 | $show_superscript = Util::showHint( |
||
777 | Sanitize::sanitize( |
||
778 | sprintf( |
||
779 | __( |
||
780 | 'This view has at least this number of ' |
||
781 | . 'rows. Please refer to %sdocumentation%s.' |
||
782 | ), |
||
783 | '[doc@cfg_MaxExactCountViews]', |
||
784 | '[/doc]' |
||
785 | ) |
||
786 | ) |
||
787 | ); |
||
788 | } |
||
789 | } |
||
790 | |||
791 | return [$approx_rows, $show_superscript]; |
||
792 | } |
||
793 | |||
794 | /** |
||
795 | * Returns the replication status of the table. |
||
796 | * |
||
797 | * @param string $table table name |
||
798 | * |
||
799 | * @return array |
||
800 | */ |
||
801 | protected function getReplicationStatus($table) |
||
802 | { |
||
803 | $do = $ignored = false; |
||
804 | if ($GLOBALS['replication_info']['slave']['status']) { |
||
805 | $nbServSlaveDoDb = count( |
||
806 | $GLOBALS['replication_info']['slave']['Do_DB'] |
||
807 | ); |
||
808 | $nbServSlaveIgnoreDb = count( |
||
809 | $GLOBALS['replication_info']['slave']['Ignore_DB'] |
||
810 | ); |
||
811 | $searchDoDBInTruename = array_search( |
||
812 | $table, |
||
813 | $GLOBALS['replication_info']['slave']['Do_DB'] |
||
814 | ); |
||
815 | $searchDoDBInDB = array_search( |
||
816 | $this->db, |
||
817 | $GLOBALS['replication_info']['slave']['Do_DB'] |
||
818 | ); |
||
819 | |||
820 | $do = strlen($searchDoDBInTruename) > 0 |
||
821 | || strlen($searchDoDBInDB) > 0 |
||
822 | || ($nbServSlaveDoDb == 0 && $nbServSlaveIgnoreDb == 0) |
||
823 | || $this->hasTable( |
||
824 | $GLOBALS['replication_info']['slave']['Wild_Do_Table'], |
||
825 | $table |
||
826 | ); |
||
827 | |||
828 | $searchDb = array_search( |
||
829 | $this->db, |
||
830 | $GLOBALS['replication_info']['slave']['Ignore_DB'] |
||
831 | ); |
||
832 | $searchTable = array_search( |
||
833 | $table, |
||
834 | $GLOBALS['replication_info']['slave']['Ignore_Table'] |
||
835 | ); |
||
836 | $ignored = strlen($searchTable) > 0 |
||
837 | || strlen($searchDb) > 0 |
||
838 | || $this->hasTable( |
||
839 | $GLOBALS['replication_info']['slave']['Wild_Ignore_Table'], |
||
840 | $table |
||
841 | ); |
||
842 | } |
||
843 | |||
844 | return [$do, $ignored]; |
||
845 | } |
||
846 | |||
847 | /** |
||
848 | * Synchronize favorite tables |
||
849 | * |
||
850 | * |
||
851 | * @param RecentFavoriteTable $fav_instance Instance of this class |
||
852 | * @param string $user The user hash |
||
853 | * @param array $favorite_tables Existing favorites |
||
854 | * |
||
855 | * @return void |
||
856 | */ |
||
857 | protected function synchronizeFavoriteTables( |
||
858 | $fav_instance, |
||
859 | $user, |
||
860 | array $favorite_tables |
||
861 | ) { |
||
862 | $fav_instance_tables = $fav_instance->getTables(); |
||
863 | |||
864 | if (empty($fav_instance_tables) |
||
865 | && isset($favorite_tables[$user]) |
||
866 | ) { |
||
867 | foreach ($favorite_tables[$user] as $key => $value) { |
||
868 | $fav_instance->add($value['db'], $value['table']); |
||
869 | } |
||
870 | } |
||
871 | $favorite_tables[$user] = $fav_instance->getTables(); |
||
872 | |||
873 | $this->response->addJSON( |
||
874 | [ |
||
875 | 'favorite_tables' => json_encode($favorite_tables), |
||
876 | 'list' => $fav_instance->getHtmlList() |
||
877 | ] |
||
878 | ); |
||
879 | $server_id = $GLOBALS['server']; |
||
880 | // Set flag when localStorage and pmadb(if present) are in sync. |
||
881 | $_SESSION['tmpval']['favorites_synced'][$server_id] = true; |
||
882 | } |
||
883 | |||
884 | /** |
||
885 | * Function to check if a table is already in favorite list. |
||
886 | * |
||
887 | * @param string $current_table current table |
||
888 | * |
||
889 | * @return true|false |
||
890 | */ |
||
891 | protected function checkFavoriteTable($current_table) |
||
892 | { |
||
893 | // ensure $_SESSION['tmpval']['favorite_tables'] is initialized |
||
894 | RecentFavoriteTable::getInstance('favorite'); |
||
895 | foreach ($_SESSION['tmpval']['favorite_tables'][$GLOBALS['server']] as $value) { |
||
896 | if ($value['db'] == $this->db && $value['table'] == $current_table) { |
||
897 | return true; |
||
898 | } |
||
899 | } |
||
900 | return false; |
||
901 | } |
||
902 | |||
903 | /** |
||
904 | * Find table with truename |
||
905 | * |
||
906 | * @param array $db DB to look into |
||
907 | * @param string $truename Table name |
||
908 | * |
||
909 | * @return bool |
||
910 | */ |
||
911 | protected function hasTable(array $db, $truename) |
||
912 | { |
||
913 | foreach ($db as $db_table) { |
||
914 | if ($this->db == $this->replication->extractDbOrTable($db_table) |
||
915 | && preg_match( |
||
916 | "@^" . |
||
917 | preg_quote(mb_substr($this->replication->extractDbOrTable($db_table, 'table'), 0, -1)) . "@", |
||
918 | $truename |
||
919 | ) |
||
920 | ) { |
||
921 | return true; |
||
922 | } |
||
923 | } |
||
924 | return false; |
||
925 | } |
||
926 | |||
927 | /** |
||
928 | * Get the value set for ENGINE table, |
||
929 | * |
||
930 | * @param array $current_table current table |
||
931 | * @param integer $sum_size total table size |
||
932 | * @param integer $overhead_size overhead size |
||
933 | * |
||
934 | * @return array |
||
935 | * @internal param bool $table_is_view whether table is view or not |
||
936 | */ |
||
937 | protected function getStuffForEngineTypeTable( |
||
938 | array $current_table, |
||
939 | $sum_size, |
||
940 | $overhead_size |
||
941 | ) { |
||
942 | $formatted_size = '-'; |
||
943 | $unit = ''; |
||
944 | $formatted_overhead = ''; |
||
945 | $overhead_unit = ''; |
||
946 | $table_is_view = false; |
||
947 | |||
948 | switch ($current_table['ENGINE']) { |
||
949 | // MyISAM, ISAM or Heap table: Row count, data size and index size |
||
950 | // are accurate; data size is accurate for ARCHIVE |
||
951 | case 'MyISAM': |
||
952 | case 'ISAM': |
||
953 | case 'HEAP': |
||
954 | case 'MEMORY': |
||
955 | case 'ARCHIVE': |
||
956 | case 'Aria': |
||
957 | case 'Maria': |
||
958 | list($current_table, $formatted_size, $unit, $formatted_overhead, |
||
959 | $overhead_unit, $overhead_size, $sum_size) |
||
960 | = $this->getValuesForAriaTable( |
||
961 | $current_table, |
||
962 | $sum_size, |
||
963 | $overhead_size, |
||
964 | $formatted_size, |
||
965 | $unit, |
||
966 | $formatted_overhead, |
||
967 | $overhead_unit |
||
968 | ); |
||
969 | break; |
||
970 | case 'InnoDB': |
||
971 | case 'PBMS': |
||
972 | case 'TokuDB': |
||
973 | // InnoDB table: Row count is not accurate but data and index sizes are. |
||
974 | // PBMS table in Drizzle: TABLE_ROWS is taken from table cache, |
||
975 | // so it may be unavailable |
||
976 | list($current_table, $formatted_size, $unit, $sum_size) |
||
977 | = $this->getValuesForInnodbTable( |
||
978 | $current_table, |
||
979 | $sum_size |
||
980 | ); |
||
981 | break; |
||
982 | // Mysql 5.0.x (and lower) uses MRG_MyISAM |
||
983 | // and MySQL 5.1.x (and higher) uses MRG_MYISAM |
||
984 | // Both are aliases for MERGE |
||
985 | case 'MRG_MyISAM': |
||
986 | case 'MRG_MYISAM': |
||
987 | case 'MERGE': |
||
988 | case 'BerkeleyDB': |
||
989 | // Merge or BerkleyDB table: Only row count is accurate. |
||
990 | if ($this->_is_show_stats) { |
||
991 | $formatted_size = ' - '; |
||
992 | $unit = ''; |
||
993 | } |
||
994 | break; |
||
995 | // for a view, the ENGINE is sometimes reported as null, |
||
996 | // or on some servers it's reported as "SYSTEM VIEW" |
||
997 | case null: |
||
998 | case 'SYSTEM VIEW': |
||
999 | // possibly a view, do nothing |
||
1000 | break; |
||
1001 | default: |
||
1002 | // Unknown table type. |
||
1003 | if ($this->_is_show_stats) { |
||
1004 | $formatted_size = __('unknown'); |
||
1005 | $unit = ''; |
||
1006 | } |
||
1007 | } // end switch |
||
1008 | |||
1009 | if ($current_table['TABLE_TYPE'] == 'VIEW' |
||
1010 | || $current_table['TABLE_TYPE'] == 'SYSTEM VIEW' |
||
1011 | ) { |
||
1012 | // countRecords() takes care of $cfg['MaxExactCountViews'] |
||
1013 | $current_table['TABLE_ROWS'] = $this->dbi |
||
1014 | ->getTable($this->db, $current_table['TABLE_NAME']) |
||
1015 | ->countRecords(true); |
||
1016 | $table_is_view = true; |
||
1017 | } |
||
1018 | |||
1019 | return [$current_table, $formatted_size, $unit, $formatted_overhead, |
||
1020 | $overhead_unit, $overhead_size, $table_is_view, $sum_size |
||
1021 | ]; |
||
1022 | } |
||
1023 | |||
1024 | /** |
||
1025 | * Get values for ARIA/MARIA tables |
||
1026 | * |
||
1027 | * @param array $current_table current table |
||
1028 | * @param integer $sum_size sum size |
||
1029 | * @param integer $overhead_size overhead size |
||
1030 | * @param integer $formatted_size formatted size |
||
1031 | * @param string $unit unit |
||
1032 | * @param integer $formatted_overhead overhead formatted |
||
1033 | * @param string $overhead_unit overhead unit |
||
1034 | * |
||
1035 | * @return array |
||
1036 | */ |
||
1037 | protected function getValuesForAriaTable( |
||
1038 | array $current_table, |
||
1039 | $sum_size, |
||
1040 | $overhead_size, |
||
1041 | $formatted_size, |
||
1042 | $unit, |
||
1043 | $formatted_overhead, |
||
1044 | $overhead_unit |
||
1045 | ) { |
||
1046 | if ($this->_db_is_system_schema) { |
||
1047 | $current_table['Rows'] = $this->dbi |
||
1048 | ->getTable($this->db, $current_table['Name']) |
||
1049 | ->countRecords(); |
||
1050 | } |
||
1051 | |||
1052 | if ($this->_is_show_stats) { |
||
1053 | $tblsize = $current_table['Data_length'] |
||
1054 | + $current_table['Index_length']; |
||
1055 | $sum_size += $tblsize; |
||
1056 | list($formatted_size, $unit) = Util::formatByteDown( |
||
1057 | $tblsize, |
||
1058 | 3, |
||
1059 | ($tblsize > 0) ? 1 : 0 |
||
1060 | ); |
||
1061 | if (isset($current_table['Data_free']) |
||
1062 | && $current_table['Data_free'] > 0 |
||
1063 | ) { |
||
1064 | list($formatted_overhead, $overhead_unit) |
||
1065 | = Util::formatByteDown( |
||
1066 | $current_table['Data_free'], |
||
1067 | 3, |
||
1068 | (($current_table['Data_free'] > 0) ? 1 : 0) |
||
1069 | ); |
||
1070 | $overhead_size += $current_table['Data_free']; |
||
1071 | } |
||
1072 | } |
||
1073 | return [$current_table, $formatted_size, $unit, $formatted_overhead, |
||
1074 | $overhead_unit, $overhead_size, $sum_size |
||
1075 | ]; |
||
1076 | } |
||
1077 | |||
1078 | /** |
||
1079 | * Get values for InnoDB table |
||
1080 | * |
||
1081 | * @param array $current_table current table |
||
1082 | * @param integer $sum_size sum size |
||
1083 | * |
||
1084 | * @return array |
||
1085 | */ |
||
1086 | protected function getValuesForInnodbTable( |
||
1087 | array $current_table, |
||
1088 | $sum_size |
||
1089 | ) { |
||
1090 | $formatted_size = $unit = ''; |
||
1091 | |||
1092 | if ((in_array($current_table['ENGINE'], ['InnoDB', 'TokuDB']) |
||
1093 | && $current_table['TABLE_ROWS'] < $GLOBALS['cfg']['MaxExactCount']) |
||
1094 | || !isset($current_table['TABLE_ROWS']) |
||
1095 | ) { |
||
1096 | $current_table['COUNTED'] = true; |
||
1097 | $current_table['TABLE_ROWS'] = $this->dbi |
||
1098 | ->getTable($this->db, $current_table['TABLE_NAME']) |
||
1099 | ->countRecords(true); |
||
1100 | } else { |
||
1101 | $current_table['COUNTED'] = false; |
||
1102 | } |
||
1103 | |||
1104 | if ($this->_is_show_stats) { |
||
1105 | $tblsize = $current_table['Data_length'] |
||
1106 | + $current_table['Index_length']; |
||
1107 | $sum_size += $tblsize; |
||
1108 | list($formatted_size, $unit) = Util::formatByteDown( |
||
1109 | $tblsize, |
||
1110 | 3, |
||
1111 | (($tblsize > 0) ? 1 : 0) |
||
1112 | ); |
||
1113 | } |
||
1114 | |||
1115 | return [$current_table, $formatted_size, $unit, $sum_size]; |
||
1116 | } |
||
1117 | } |
||
1118 |