1<?php 2/** 3 * set of functions with the insert/edit features in pma 4 */ 5 6declare(strict_types=1); 7 8namespace PhpMyAdmin; 9 10use PhpMyAdmin\Controllers\Table\ChangeController; 11use PhpMyAdmin\Html\Generator; 12use PhpMyAdmin\Plugins\TransformationsPlugin; 13use const ENT_COMPAT; 14use const PASSWORD_DEFAULT; 15use function array_fill; 16use function array_flip; 17use function array_merge; 18use function array_values; 19use function bin2hex; 20use function class_exists; 21use function count; 22use function current; 23use function date; 24use function defined; 25use function explode; 26use function htmlspecialchars; 27use function implode; 28use function in_array; 29use function is_array; 30use function is_file; 31use function is_numeric; 32use function is_string; 33use function max; 34use function mb_stripos; 35use function mb_strlen; 36use function mb_strpos; 37use function mb_strstr; 38use function mb_substr; 39use function md5; 40use function method_exists; 41use function min; 42use function password_hash; 43use function preg_match; 44use function preg_replace; 45use function sprintf; 46use function str_replace; 47use function stripcslashes; 48use function stripslashes; 49use function strlen; 50use function strpos; 51use function substr; 52use function time; 53use function trim; 54 55/** 56 * PhpMyAdmin\InsertEdit class 57 */ 58class InsertEdit 59{ 60 /** 61 * DatabaseInterface instance 62 * 63 * @var DatabaseInterface 64 */ 65 private $dbi; 66 67 /** @var Relation */ 68 private $relation; 69 70 /** @var Transformations */ 71 private $transformations; 72 73 /** @var FileListing */ 74 private $fileListing; 75 76 /** @var Template */ 77 public $template; 78 79 /** 80 * @param DatabaseInterface $dbi DatabaseInterface instance 81 */ 82 public function __construct(DatabaseInterface $dbi) 83 { 84 $this->dbi = $dbi; 85 $this->relation = new Relation($this->dbi); 86 $this->transformations = new Transformations(); 87 $this->fileListing = new FileListing(); 88 $this->template = new Template(); 89 } 90 91 /** 92 * Retrieve form parameters for insert/edit form 93 * 94 * @param string $db name of the database 95 * @param string $table name of the table 96 * @param array|null $where_clauses where clauses 97 * @param array $where_clause_array array of where clauses 98 * @param string $err_url error url 99 * 100 * @return array array of insert/edit form parameters 101 */ 102 public function getFormParametersForInsertForm( 103 $db, 104 $table, 105 ?array $where_clauses, 106 array $where_clause_array, 107 $err_url 108 ) { 109 $_form_params = [ 110 'db' => $db, 111 'table' => $table, 112 'goto' => $GLOBALS['goto'], 113 'err_url' => $err_url, 114 'sql_query' => $_POST['sql_query'], 115 ]; 116 if (isset($where_clauses)) { 117 foreach ($where_clause_array as $key_id => $where_clause) { 118 $_form_params['where_clause[' . $key_id . ']'] = trim($where_clause); 119 } 120 } 121 if (isset($_POST['clause_is_unique'])) { 122 $_form_params['clause_is_unique'] = $_POST['clause_is_unique']; 123 } 124 125 return $_form_params; 126 } 127 128 /** 129 * Creates array of where clauses 130 * 131 * @param array|string|null $where_clause where clause 132 * 133 * @return array whereClauseArray array of where clauses 134 */ 135 private function getWhereClauseArray($where_clause) 136 { 137 if (! isset($where_clause)) { 138 return []; 139 } 140 141 if (is_array($where_clause)) { 142 return $where_clause; 143 } 144 145 return [0 => $where_clause]; 146 } 147 148 /** 149 * Analysing where clauses array 150 * 151 * @param array $where_clause_array array of where clauses 152 * @param string $table name of the table 153 * @param string $db name of the database 154 * 155 * @return array $where_clauses, $result, $rows, $found_unique_key 156 */ 157 private function analyzeWhereClauses( 158 array $where_clause_array, 159 $table, 160 $db 161 ) { 162 $rows = []; 163 $result = []; 164 $where_clauses = []; 165 $found_unique_key = false; 166 foreach ($where_clause_array as $key_id => $where_clause) { 167 $local_query = 'SELECT * FROM ' 168 . Util::backquote($db) . '.' 169 . Util::backquote($table) 170 . ' WHERE ' . $where_clause . ';'; 171 $result[$key_id] = $this->dbi->query( 172 $local_query, 173 DatabaseInterface::CONNECT_USER, 174 DatabaseInterface::QUERY_STORE 175 ); 176 $rows[$key_id] = $this->dbi->fetchAssoc($result[$key_id]); 177 178 $where_clauses[$key_id] = str_replace('\\', '\\\\', $where_clause); 179 $has_unique_condition = $this->showEmptyResultMessageOrSetUniqueCondition( 180 $rows, 181 $key_id, 182 $where_clause_array, 183 $local_query, 184 $result 185 ); 186 if (! $has_unique_condition) { 187 continue; 188 } 189 190 $found_unique_key = true; 191 } 192 193 return [ 194 $where_clauses, 195 $result, 196 $rows, 197 $found_unique_key, 198 ]; 199 } 200 201 /** 202 * Show message for empty result or set the unique_condition 203 * 204 * @param array $rows MySQL returned rows 205 * @param string $key_id ID in current key 206 * @param array $where_clause_array array of where clauses 207 * @param string $local_query query performed 208 * @param array $result MySQL result handle 209 * 210 * @return bool 211 */ 212 private function showEmptyResultMessageOrSetUniqueCondition( 213 array $rows, 214 $key_id, 215 array $where_clause_array, 216 $local_query, 217 array $result 218 ) { 219 $has_unique_condition = false; 220 221 // No row returned 222 if (! $rows[$key_id]) { 223 unset($rows[$key_id], $where_clause_array[$key_id]); 224 Response::getInstance()->addHTML( 225 Generator::getMessage( 226 __('MySQL returned an empty result set (i.e. zero rows).'), 227 $local_query 228 ) 229 ); 230 /** 231 * @todo not sure what should be done at this point, but we must not 232 * exit if we want the message to be displayed 233 */ 234 } else {// end if (no row returned) 235 $meta = $this->dbi->getFieldsMeta($result[$key_id]); 236 237 [$unique_condition, $tmp_clause_is_unique] = Util::getUniqueCondition( 238 $result[$key_id], 239 count($meta), 240 $meta, 241 $rows[$key_id], 242 true 243 ); 244 245 if (! empty($unique_condition)) { 246 $has_unique_condition = true; 247 } 248 unset($unique_condition, $tmp_clause_is_unique); 249 } 250 251 return $has_unique_condition; 252 } 253 254 /** 255 * No primary key given, just load first row 256 * 257 * @param string $table name of the table 258 * @param string $db name of the database 259 * 260 * @return array containing $result and $rows arrays 261 */ 262 private function loadFirstRow($table, $db) 263 { 264 $result = $this->dbi->query( 265 'SELECT * FROM ' . Util::backquote($db) 266 . '.' . Util::backquote($table) . ' LIMIT 1;', 267 DatabaseInterface::CONNECT_USER, 268 DatabaseInterface::QUERY_STORE 269 ); 270 // Can be a string on some old configuration storage settings 271 $rows = array_fill(0, (int) $GLOBALS['cfg']['InsertRows'], false); 272 273 return [ 274 $result, 275 $rows, 276 ]; 277 } 278 279 /** 280 * Add some url parameters 281 * 282 * @param array $url_params containing $db and $table as url parameters 283 * @param array $where_clause_array where clauses array 284 * 285 * @return array Add some url parameters to $url_params array and return it 286 */ 287 public function urlParamsInEditMode( 288 array $url_params, 289 array $where_clause_array 290 ): array { 291 foreach ($where_clause_array as $where_clause) { 292 $url_params['where_clause'] = trim($where_clause); 293 } 294 if (! empty($_POST['sql_query'])) { 295 $url_params['sql_query'] = $_POST['sql_query']; 296 } 297 298 return $url_params; 299 } 300 301 /** 302 * Show type information or function selectors in Insert/Edit 303 * 304 * @param string $which function|type 305 * @param array $url_params containing url parameters 306 * @param bool $is_show whether to show the element in $which 307 * 308 * @return string an HTML snippet 309 */ 310 public function showTypeOrFunction($which, array $url_params, $is_show) 311 { 312 $params = []; 313 314 switch ($which) { 315 case 'function': 316 $params['ShowFunctionFields'] = ($is_show ? 0 : 1); 317 $params['ShowFieldTypesInDataEditView'] 318 = $GLOBALS['cfg']['ShowFieldTypesInDataEditView']; 319 break; 320 case 'type': 321 $params['ShowFieldTypesInDataEditView'] = ($is_show ? 0 : 1); 322 $params['ShowFunctionFields'] 323 = $GLOBALS['cfg']['ShowFunctionFields']; 324 break; 325 } 326 327 $params['goto'] = Url::getFromRoute('/sql'); 328 $this_url_params = array_merge($url_params, $params); 329 330 if (! $is_show) { 331 return ' : <a href="' . Url::getFromRoute('/table/change') . '" data-post="' 332 . Url::getCommon($this_url_params, '', false) . '">' 333 . $this->showTypeOrFunctionLabel($which) 334 . '</a>'; 335 } 336 337 return '<th><a href="' . Url::getFromRoute('/table/change') . '" data-post="' 338 . Url::getCommon($this_url_params, '', false) 339 . '" title="' . __('Hide') . '">' 340 . $this->showTypeOrFunctionLabel($which) 341 . '</a></th>'; 342 } 343 344 /** 345 * Show type information or function selectors labels in Insert/Edit 346 * 347 * @param string $which function|type 348 * 349 * @return string|null an HTML snippet 350 */ 351 private function showTypeOrFunctionLabel($which) 352 { 353 switch ($which) { 354 case 'function': 355 return __('Function'); 356 case 'type': 357 return __('Type'); 358 } 359 360 return null; 361 } 362 363 /** 364 * Analyze the table column array 365 * 366 * @param array $column description of column in given table 367 * @param array $comments_map comments for every column that has a comment 368 * @param bool $timestamp_seen whether a timestamp has been seen 369 * 370 * @return array description of column in given table 371 */ 372 private function analyzeTableColumnsArray( 373 array $column, 374 array $comments_map, 375 $timestamp_seen 376 ) { 377 $column['Field_html'] = htmlspecialchars($column['Field']); 378 $column['Field_md5'] = md5($column['Field']); 379 // True_Type contains only the type (stops at first bracket) 380 $column['True_Type'] = preg_replace('@\(.*@s', '', $column['Type']); 381 $column['len'] = preg_match('@float|double@', $column['Type']) ? 100 : -1; 382 $column['Field_title'] = $this->getColumnTitle($column, $comments_map); 383 $column['is_binary'] = $this->isColumn( 384 $column, 385 [ 386 'binary', 387 'varbinary', 388 ] 389 ); 390 $column['is_blob'] = $this->isColumn( 391 $column, 392 [ 393 'blob', 394 'tinyblob', 395 'mediumblob', 396 'longblob', 397 ] 398 ); 399 $column['is_char'] = $this->isColumn( 400 $column, 401 [ 402 'char', 403 'varchar', 404 ] 405 ); 406 407 [$column['pma_type'], $column['wrap'], $column['first_timestamp']] 408 = $this->getEnumSetAndTimestampColumns($column, $timestamp_seen); 409 410 return $column; 411 } 412 413 /** 414 * Retrieve the column title 415 * 416 * @param array $column description of column in given table 417 * @param array $comments_map comments for every column that has a comment 418 * 419 * @return string column title 420 */ 421 private function getColumnTitle(array $column, array $comments_map) 422 { 423 if (isset($comments_map[$column['Field']])) { 424 return '<span style="border-bottom: 1px dashed black;" title="' 425 . htmlspecialchars($comments_map[$column['Field']]) . '">' 426 . $column['Field_html'] . '</span>'; 427 } 428 429 return $column['Field_html']; 430 } 431 432 /** 433 * check whether the column is of a certain type 434 * the goal is to ensure that types such as "enum('one','two','binary',..)" 435 * or "enum('one','two','varbinary',..)" are not categorized as binary 436 * 437 * @param array $column description of column in given table 438 * @param array $types the types to verify 439 * 440 * @return bool whether the column's type if one of the $types 441 */ 442 public function isColumn(array $column, array $types) 443 { 444 foreach ($types as $one_type) { 445 if (mb_stripos($column['Type'], $one_type) === 0) { 446 return true; 447 } 448 } 449 450 return false; 451 } 452 453 /** 454 * Retrieve set, enum, timestamp table columns 455 * 456 * @param array $column description of column in given table 457 * @param bool $timestamp_seen whether a timestamp has been seen 458 * 459 * @return array $column['pma_type'], $column['wrap'], $column['first_timestamp'] 460 */ 461 private function getEnumSetAndTimestampColumns(array $column, $timestamp_seen) 462 { 463 $column['first_timestamp'] = false; 464 switch ($column['True_Type']) { 465 case 'set': 466 $column['pma_type'] = 'set'; 467 $column['wrap'] = ''; 468 break; 469 case 'enum': 470 $column['pma_type'] = 'enum'; 471 $column['wrap'] = ''; 472 break; 473 case 'timestamp': 474 if (! $timestamp_seen) { // can only occur once per table 475 $column['first_timestamp'] = true; 476 } 477 $column['pma_type'] = $column['Type']; 478 $column['wrap'] = ' nowrap'; 479 break; 480 481 default: 482 $column['pma_type'] = $column['Type']; 483 $column['wrap'] = ' nowrap'; 484 break; 485 } 486 487 return [ 488 $column['pma_type'], 489 $column['wrap'], 490 $column['first_timestamp'], 491 ]; 492 } 493 494 /** 495 * The function column 496 * We don't want binary data to be destroyed 497 * Note: from the MySQL manual: "BINARY doesn't affect how the column is 498 * stored or retrieved" so it does not mean that the contents is binary 499 * 500 * @param array $column description of column in given table 501 * @param bool $is_upload upload or no 502 * @param string $column_name_appendix the name attribute 503 * @param string $onChangeClause onchange clause for fields 504 * @param array $no_support_types list of datatypes that are not (yet) 505 * handled by PMA 506 * @param int $tabindex_for_function +3000 507 * @param int $tabindex tab index 508 * @param int $idindex id index 509 * @param bool $insert_mode insert mode or edit mode 510 * @param bool $readOnly is column read only or not 511 * @param array $foreignData foreign key data 512 * 513 * @return string an html snippet 514 */ 515 private function getFunctionColumn( 516 array $column, 517 $is_upload, 518 $column_name_appendix, 519 $onChangeClause, 520 array $no_support_types, 521 $tabindex_for_function, 522 $tabindex, 523 $idindex, 524 $insert_mode, 525 $readOnly, 526 array $foreignData 527 ): string { 528 $html_output = ''; 529 if (($GLOBALS['cfg']['ProtectBinary'] === 'blob' 530 && $column['is_blob'] && ! $is_upload) 531 || ($GLOBALS['cfg']['ProtectBinary'] === 'all' 532 && $column['is_binary']) 533 || ($GLOBALS['cfg']['ProtectBinary'] === 'noblob' 534 && $column['is_binary']) 535 ) { 536 $html_output .= '<td class="text-center">' . __('Binary') . '</td>' . "\n"; 537 } elseif ($readOnly 538 || mb_strstr($column['True_Type'], 'enum') 539 || mb_strstr($column['True_Type'], 'set') 540 || in_array($column['pma_type'], $no_support_types) 541 ) { 542 $html_output .= '<td class="text-center">--</td>' . "\n"; 543 } else { 544 $html_output .= '<td>' . "\n"; 545 546 $html_output .= '<select name="funcs' . $column_name_appendix . '"' 547 . ' ' . $onChangeClause 548 . ' tabindex="' . ($tabindex + $tabindex_for_function) . '"' 549 . ' id="field_' . $idindex . '_1">'; 550 $html_output .= Generator::getFunctionsForField( 551 $column, 552 $insert_mode, 553 $foreignData 554 ) . "\n"; 555 556 $html_output .= '</select>' . "\n"; 557 $html_output .= '</td>' . "\n"; 558 } 559 560 return $html_output; 561 } 562 563 /** 564 * The null column 565 * 566 * @param array $column description of column in given table 567 * @param string $column_name_appendix the name attribute 568 * @param bool $real_null_value is column value null or not null 569 * @param int $tabindex tab index 570 * @param int $tabindex_for_null +6000 571 * @param int $idindex id index 572 * @param string $vkey [multi_edit]['row_id'] 573 * @param array $foreigners keys into foreign fields 574 * @param array $foreignData data about the foreign keys 575 * @param bool $readOnly is column read only or not 576 * 577 * @return string an html snippet 578 */ 579 private function getNullColumn( 580 array $column, 581 $column_name_appendix, 582 $real_null_value, 583 $tabindex, 584 $tabindex_for_null, 585 $idindex, 586 $vkey, 587 array $foreigners, 588 array $foreignData, 589 $readOnly 590 ) { 591 if ($column['Null'] !== 'YES' || $readOnly) { 592 return "<td></td>\n"; 593 } 594 $html_output = ''; 595 $html_output .= '<td>' . "\n"; 596 $html_output .= '<input type="hidden" name="fields_null_prev' 597 . $column_name_appendix . '"'; 598 if ($real_null_value && ! $column['first_timestamp']) { 599 $html_output .= ' value="on"'; 600 } 601 $html_output .= '>' . "\n"; 602 603 $html_output .= '<input type="checkbox" class="checkbox_null" tabindex="' 604 . ($tabindex + $tabindex_for_null) . '"' 605 . ' name="fields_null' . $column_name_appendix . '"'; 606 if ($real_null_value) { 607 $html_output .= ' checked="checked"'; 608 } 609 $html_output .= ' id="field_' . $idindex . '_2">'; 610 611 // nullify_code is needed by the js nullify() function 612 $nullify_code = $this->getNullifyCodeForNullColumn( 613 $column, 614 $foreigners, 615 $foreignData 616 ); 617 // to be able to generate calls to nullify() in jQuery 618 $html_output .= '<input type="hidden" class="nullify_code" name="nullify_code' 619 . $column_name_appendix . '" value="' . $nullify_code . '">'; 620 $html_output .= '<input type="hidden" class="hashed_field" name="hashed_field' 621 . $column_name_appendix . '" value="' . $column['Field_md5'] . '">'; 622 $html_output .= '<input type="hidden" class="multi_edit" name="multi_edit' 623 . $column_name_appendix . '" value="' . Sanitize::escapeJsString($vkey) . '">'; 624 $html_output .= '</td>' . "\n"; 625 626 return $html_output; 627 } 628 629 /** 630 * Retrieve the nullify code for the null column 631 * 632 * @param array $column description of column in given table 633 * @param array $foreigners keys into foreign fields 634 * @param array $foreignData data about the foreign keys 635 */ 636 private function getNullifyCodeForNullColumn( 637 array $column, 638 array $foreigners, 639 array $foreignData 640 ): string { 641 $foreigner = $this->relation->searchColumnInForeigners($foreigners, $column['Field']); 642 if (mb_strstr($column['True_Type'], 'enum')) { 643 if (mb_strlen((string) $column['Type']) > 20) { 644 $nullify_code = '1'; 645 } else { 646 $nullify_code = '2'; 647 } 648 } elseif (mb_strstr($column['True_Type'], 'set')) { 649 $nullify_code = '3'; 650 } elseif (! empty($foreigners) 651 && ! empty($foreigner) 652 && $foreignData['foreign_link'] == false 653 ) { 654 // foreign key in a drop-down 655 $nullify_code = '4'; 656 } elseif (! empty($foreigners) 657 && ! empty($foreigner) 658 && $foreignData['foreign_link'] == true 659 ) { 660 // foreign key with a browsing icon 661 $nullify_code = '6'; 662 } else { 663 $nullify_code = '5'; 664 } 665 666 return $nullify_code; 667 } 668 669 /** 670 * Get the HTML elements for value column in insert form 671 * (here, "column" is used in the sense of HTML column in HTML table) 672 * 673 * @param array $column description of column in given table 674 * @param string $backup_field hidden input field 675 * @param string $column_name_appendix the name attribute 676 * @param string $onChangeClause onchange clause for fields 677 * @param int $tabindex tab index 678 * @param int $tabindex_for_value offset for the values tabindex 679 * @param int $idindex id index 680 * @param string $data description of the column field 681 * @param string $special_chars special characters 682 * @param array $foreignData data about the foreign keys 683 * @param array $paramTableDbArray array containing $table and $db 684 * @param int $rownumber the row number 685 * @param string $text_dir text direction 686 * @param string $special_chars_encoded replaced char if the string starts 687 * with a \r\n pair (0x0d0a) add an extra \n 688 * @param string $vkey [multi_edit]['row_id'] 689 * @param bool $is_upload is upload or not 690 * @param int $biggest_max_file_size 0 integer 691 * @param string $default_char_editing default char editing mode which is stored 692 * in the config.inc.php script 693 * @param array $no_support_types list of datatypes that are not (yet) 694 * handled by PMA 695 * @param array $gis_data_types list of GIS data types 696 * @param array $extracted_columnspec associative array containing type, 697 * spec_in_brackets and possibly 698 * enum_set_values (another array) 699 * @param bool $readOnly is column read only or not 700 * 701 * @return string an html snippet 702 */ 703 private function getValueColumn( 704 array $column, 705 $backup_field, 706 $column_name_appendix, 707 $onChangeClause, 708 $tabindex, 709 $tabindex_for_value, 710 $idindex, 711 $data, 712 $special_chars, 713 array $foreignData, 714 array $paramTableDbArray, 715 $rownumber, 716 $text_dir, 717 $special_chars_encoded, 718 $vkey, 719 $is_upload, 720 $biggest_max_file_size, 721 $default_char_editing, 722 array $no_support_types, 723 array $gis_data_types, 724 array $extracted_columnspec, 725 $readOnly 726 ) { 727 // HTML5 data-* attribute data-type 728 $data_type = $this->dbi->types->getTypeClass($column['True_Type']); 729 $html_output = ''; 730 731 if ($foreignData['foreign_link'] == true) { 732 $html_output .= $this->getForeignLink( 733 $column, 734 $backup_field, 735 $column_name_appendix, 736 $onChangeClause, 737 $tabindex, 738 $tabindex_for_value, 739 $idindex, 740 $data, 741 $paramTableDbArray, 742 $rownumber, 743 $readOnly 744 ); 745 } elseif (is_array($foreignData['disp_row'])) { 746 $html_output .= $this->dispRowForeignData( 747 $column, 748 $backup_field, 749 $column_name_appendix, 750 $onChangeClause, 751 $tabindex, 752 $tabindex_for_value, 753 $idindex, 754 $data, 755 $foreignData, 756 $readOnly 757 ); 758 } elseif (( 759 $GLOBALS['cfg']['LongtextDoubleTextarea'] 760 && mb_strstr($column['pma_type'], 'longtext')) 761 || mb_strstr($column['pma_type'], 'json') 762 ) { 763 $html_output .= $this->getTextarea( 764 $column, 765 $backup_field, 766 $column_name_appendix, 767 $onChangeClause, 768 $tabindex, 769 $tabindex_for_value, 770 $idindex, 771 $text_dir, 772 $special_chars_encoded, 773 $data_type, 774 $readOnly 775 ); 776 } elseif (mb_strstr($column['pma_type'], 'text')) { 777 $html_output .= $this->getTextarea( 778 $column, 779 $backup_field, 780 $column_name_appendix, 781 $onChangeClause, 782 $tabindex, 783 $tabindex_for_value, 784 $idindex, 785 $text_dir, 786 $special_chars_encoded, 787 $data_type, 788 $readOnly 789 ); 790 $html_output .= "\n"; 791 if (mb_strlen($special_chars) > 32000) { 792 $html_output .= "</td>\n"; 793 $html_output .= '<td>' . __( 794 'Because of its length,<br> this column might not be editable.' 795 ); 796 } 797 } elseif ($column['pma_type'] === 'enum') { 798 $html_output .= $this->getPmaTypeEnum( 799 $column, 800 $backup_field, 801 $column_name_appendix, 802 $extracted_columnspec, 803 $onChangeClause, 804 $tabindex, 805 $tabindex_for_value, 806 $idindex, 807 $data, 808 $readOnly 809 ); 810 } elseif ($column['pma_type'] === 'set') { 811 $html_output .= $this->getPmaTypeSet( 812 $column, 813 $extracted_columnspec, 814 $backup_field, 815 $column_name_appendix, 816 $onChangeClause, 817 $tabindex, 818 $tabindex_for_value, 819 $idindex, 820 $data, 821 $readOnly 822 ); 823 } elseif ($column['is_binary'] || $column['is_blob']) { 824 $html_output .= $this->getBinaryAndBlobColumn( 825 $column, 826 $data, 827 $special_chars, 828 $biggest_max_file_size, 829 $backup_field, 830 $column_name_appendix, 831 $onChangeClause, 832 $tabindex, 833 $tabindex_for_value, 834 $idindex, 835 $text_dir, 836 $special_chars_encoded, 837 $vkey, 838 $is_upload, 839 $readOnly 840 ); 841 } elseif (! in_array($column['pma_type'], $no_support_types)) { 842 $html_output .= $this->getValueColumnForOtherDatatypes( 843 $column, 844 $default_char_editing, 845 $backup_field, 846 $column_name_appendix, 847 $onChangeClause, 848 $tabindex, 849 $special_chars, 850 $tabindex_for_value, 851 $idindex, 852 $text_dir, 853 $special_chars_encoded, 854 $data, 855 $extracted_columnspec, 856 $readOnly 857 ); 858 } 859 860 if (in_array($column['pma_type'], $gis_data_types)) { 861 $html_output .= $this->getHtmlForGisDataTypes((int) $rownumber); 862 } 863 864 return $html_output; 865 } 866 867 /** 868 * Get HTML for foreign link in insert form 869 * 870 * @param array $column description of column in given table 871 * @param string $backup_field hidden input field 872 * @param string $column_name_appendix the name attribute 873 * @param string $onChangeClause onchange clause for fields 874 * @param int $tabindex tab index 875 * @param int $tabindex_for_value offset for the values tabindex 876 * @param int $idindex id index 877 * @param string $data data to edit 878 * @param array $paramTableDbArray array containing $table and $db 879 * @param int $rownumber the row number 880 * @param bool $readOnly is column read only or not 881 * 882 * @return string an html snippet 883 */ 884 private function getForeignLink( 885 array $column, 886 $backup_field, 887 $column_name_appendix, 888 $onChangeClause, 889 $tabindex, 890 $tabindex_for_value, 891 $idindex, 892 $data, 893 array $paramTableDbArray, 894 $rownumber, 895 $readOnly 896 ) { 897 [$table, $db] = $paramTableDbArray; 898 $html_output = ''; 899 $html_output .= $backup_field . "\n"; 900 901 $html_output .= '<input type="hidden" name="fields_type' 902 . $column_name_appendix . '" value="foreign">'; 903 904 $html_output .= '<input type="text" name="fields' . $column_name_appendix . '" ' 905 . 'class="textfield" ' 906 . $onChangeClause . ' ' 907 . ($readOnly ? 'readonly="readonly" ' : '') 908 . 'tabindex="' . ($tabindex + $tabindex_for_value) . '" ' 909 . 'id="field_' . $idindex . '_3" ' 910 . 'value="' . htmlspecialchars($data) . '">'; 911 912 $html_output .= '<a class="ajax browse_foreign" href="' 913 . Url::getFromRoute('/browse-foreigners') 914 . '" data-post="' 915 . Url::getCommon( 916 [ 917 'db' => $db, 918 'table' => $table, 919 'field' => $column['Field'], 920 'rownumber' => $rownumber, 921 'data' => $data, 922 ], 923 '', 924 false 925 ) . '">' 926 . Generator::getIcon('b_browse', __('Browse foreign values')) . '</a>'; 927 928 return $html_output; 929 } 930 931 /** 932 * Get HTML to display foreign data 933 * 934 * @param array $column description of column in given table 935 * @param string $backup_field hidden input field 936 * @param string $column_name_appendix the name attribute 937 * @param string $onChangeClause onchange clause for fields 938 * @param int $tabindex tab index 939 * @param int $tabindex_for_value offset for the values tabindex 940 * @param int $idindex id index 941 * @param string $data data to edit 942 * @param array $foreignData data about the foreign keys 943 * @param bool $readOnly is display read only or not 944 * 945 * @return string an html snippet 946 */ 947 private function dispRowForeignData( 948 $column, 949 $backup_field, 950 $column_name_appendix, 951 $onChangeClause, 952 $tabindex, 953 $tabindex_for_value, 954 $idindex, 955 $data, 956 array $foreignData, 957 $readOnly 958 ) { 959 $html_output = ''; 960 $html_output .= $backup_field . "\n"; 961 $html_output .= '<input type="hidden"' 962 . ' name="fields_type' . $column_name_appendix . '"'; 963 if ($column['is_binary']) { 964 $html_output .= ' value="hex">'; 965 } else { 966 $html_output .= ' value="foreign">'; 967 } 968 969 $html_output .= '<select name="fields' . $column_name_appendix . '"' 970 . ' ' . $onChangeClause 971 . ' class="textfield"' 972 . ($readOnly ? ' disabled' : '') 973 . ' tabindex="' . ($tabindex + $tabindex_for_value) . '"' 974 . ' id="field_' . $idindex . '_3">'; 975 $html_output .= $this->relation->foreignDropdown( 976 $foreignData['disp_row'], 977 $foreignData['foreign_field'], 978 $foreignData['foreign_display'], 979 $data, 980 $GLOBALS['cfg']['ForeignKeyMaxLimit'] 981 ); 982 $html_output .= '</select>'; 983 984 //Add hidden input, as disabled <select> input does not included in POST. 985 if ($readOnly) { 986 $html_output .= '<input name="fields' . $column_name_appendix . '"' 987 . ' type="hidden" value="' . htmlspecialchars($data) . '">'; 988 } 989 990 return $html_output; 991 } 992 993 /** 994 * Get HTML textarea for insert form 995 * 996 * @param array $column column information 997 * @param string $backup_field hidden input field 998 * @param string $column_name_appendix the name attribute 999 * @param string $onChangeClause onchange clause for fields 1000 * @param int $tabindex tab index 1001 * @param int $tabindex_for_value offset for the values tabindex 1002 * @param int $idindex id index 1003 * @param string $text_dir text direction 1004 * @param string $special_chars_encoded replaced char if the string starts 1005 * with a \r\n pair (0x0d0a) add an extra \n 1006 * @param string $data_type the html5 data-* attribute type 1007 * @param bool $readOnly is column read only or not 1008 * 1009 * @return string an html snippet 1010 */ 1011 private function getTextarea( 1012 array $column, 1013 $backup_field, 1014 $column_name_appendix, 1015 $onChangeClause, 1016 $tabindex, 1017 $tabindex_for_value, 1018 $idindex, 1019 $text_dir, 1020 $special_chars_encoded, 1021 $data_type, 1022 $readOnly 1023 ) { 1024 $the_class = ''; 1025 $textAreaRows = $GLOBALS['cfg']['TextareaRows']; 1026 $textareaCols = $GLOBALS['cfg']['TextareaCols']; 1027 1028 if ($column['is_char']) { 1029 /** 1030 * @todo clarify the meaning of the "textfield" class and explain 1031 * why character columns have the "char" class instead 1032 */ 1033 $the_class = 'char charField'; 1034 $textAreaRows = $GLOBALS['cfg']['CharTextareaRows']; 1035 $textareaCols = $GLOBALS['cfg']['CharTextareaCols']; 1036 $extracted_columnspec = Util::extractColumnSpec( 1037 $column['Type'] 1038 ); 1039 $maxlength = $extracted_columnspec['spec_in_brackets']; 1040 } elseif ($GLOBALS['cfg']['LongtextDoubleTextarea'] 1041 && mb_strstr($column['pma_type'], 'longtext') 1042 ) { 1043 $textAreaRows = $GLOBALS['cfg']['TextareaRows'] * 2; 1044 $textareaCols = $GLOBALS['cfg']['TextareaCols'] * 2; 1045 } 1046 1047 return $backup_field . "\n" 1048 . '<textarea name="fields' . $column_name_appendix . '"' 1049 . ' class="' . $the_class . '"' 1050 . ($readOnly ? ' readonly="readonly"' : '') 1051 . (isset($maxlength) ? ' data-maxlength="' . $maxlength . '"' : '') 1052 . ' rows="' . $textAreaRows . '"' 1053 . ' cols="' . $textareaCols . '"' 1054 . ' dir="' . $text_dir . '"' 1055 . ' id="field_' . $idindex . '_3"' 1056 . (! empty($onChangeClause) ? ' ' . $onChangeClause : '') 1057 . ' tabindex="' . ($tabindex + $tabindex_for_value) . '"' 1058 . ' data-type="' . $data_type . '">' 1059 . $special_chars_encoded 1060 . '</textarea>'; 1061 } 1062 1063 /** 1064 * Get HTML for enum type 1065 * 1066 * @param array $column description of column in given table 1067 * @param string $backup_field hidden input field 1068 * @param string $column_name_appendix the name attribute 1069 * @param array $extracted_columnspec associative array containing type, 1070 * spec_in_brackets and possibly 1071 * enum_set_values (another array) 1072 * @param string $onChangeClause onchange clause for fields 1073 * @param int $tabindex tab index 1074 * @param int $tabindex_for_value offset for the values tabindex 1075 * @param int $idindex id index 1076 * @param mixed $data data to edit 1077 * @param bool $readOnly is column read only or not 1078 * 1079 * @return string an html snippet 1080 */ 1081 private function getPmaTypeEnum( 1082 array $column, 1083 $backup_field, 1084 $column_name_appendix, 1085 array $extracted_columnspec, 1086 $onChangeClause, 1087 $tabindex, 1088 $tabindex_for_value, 1089 $idindex, 1090 $data, 1091 $readOnly 1092 ) { 1093 $html_output = ''; 1094 if (! isset($column['values'])) { 1095 $column['values'] = $this->getColumnEnumValues( 1096 $column, 1097 $extracted_columnspec 1098 ); 1099 } 1100 $column_enum_values = $column['values']; 1101 $html_output .= '<input type="hidden" name="fields_type' 1102 . $column_name_appendix . '" value="enum">'; 1103 $html_output .= "\n" . ' ' . $backup_field . "\n"; 1104 if (mb_strlen($column['Type']) > 20) { 1105 $html_output .= $this->getDropDownDependingOnLength( 1106 $column, 1107 $column_name_appendix, 1108 $onChangeClause, 1109 $tabindex, 1110 $tabindex_for_value, 1111 $idindex, 1112 $data, 1113 $column_enum_values, 1114 $readOnly 1115 ); 1116 } else { 1117 $html_output .= $this->getRadioButtonDependingOnLength( 1118 $column_name_appendix, 1119 $onChangeClause, 1120 $tabindex, 1121 $column, 1122 $tabindex_for_value, 1123 $idindex, 1124 $data, 1125 $column_enum_values, 1126 $readOnly 1127 ); 1128 } 1129 1130 return $html_output; 1131 } 1132 1133 /** 1134 * Get column values 1135 * 1136 * @param array $column description of column in given table 1137 * @param array $extracted_columnspec associative array containing type, 1138 * spec_in_brackets and possibly enum_set_values 1139 * (another array) 1140 * 1141 * @return array column values as an associative array 1142 */ 1143 private function getColumnEnumValues(array $column, array $extracted_columnspec) 1144 { 1145 $column['values'] = []; 1146 foreach ($extracted_columnspec['enum_set_values'] as $val) { 1147 $column['values'][] = [ 1148 'plain' => $val, 1149 'html' => htmlspecialchars($val), 1150 ]; 1151 } 1152 1153 return $column['values']; 1154 } 1155 1156 /** 1157 * Get HTML drop down for more than 20 string length 1158 * 1159 * @param array $column description of column in given table 1160 * @param string $column_name_appendix the name attribute 1161 * @param string $onChangeClause onchange clause for fields 1162 * @param int $tabindex tab index 1163 * @param int $tabindex_for_value offset for the values tabindex 1164 * @param int $idindex id index 1165 * @param string $data data to edit 1166 * @param array $column_enum_values $column['values'] 1167 * @param bool $readOnly is column read only or not 1168 * 1169 * @return string an html snippet 1170 */ 1171 private function getDropDownDependingOnLength( 1172 array $column, 1173 $column_name_appendix, 1174 $onChangeClause, 1175 $tabindex, 1176 $tabindex_for_value, 1177 $idindex, 1178 $data, 1179 array $column_enum_values, 1180 $readOnly 1181 ) { 1182 $html_output = '<select name="fields' . $column_name_appendix . '"' 1183 . ' ' . $onChangeClause 1184 . ' class="textfield"' 1185 . ' tabindex="' . ($tabindex + $tabindex_for_value) . '"' 1186 . ($readOnly ? ' disabled' : '') 1187 . ' id="field_' . $idindex . '_3">'; 1188 $html_output .= '<option value=""> </option>' . "\n"; 1189 1190 $selected_html = ''; 1191 foreach ($column_enum_values as $enum_value) { 1192 $html_output .= '<option value="' . $enum_value['html'] . '"'; 1193 if ($data == $enum_value['plain'] 1194 || ($data == '' 1195 && (! isset($_POST['where_clause']) || $column['Null'] !== 'YES') 1196 && isset($column['Default']) 1197 && $enum_value['plain'] == $column['Default']) 1198 ) { 1199 $html_output .= ' selected="selected"'; 1200 $selected_html = $enum_value['html']; 1201 } 1202 $html_output .= '>' . $enum_value['html'] . '</option>' . "\n"; 1203 } 1204 $html_output .= '</select>'; 1205 1206 //Add hidden input, as disabled <select> input does not included in POST. 1207 if ($readOnly) { 1208 $html_output .= '<input name="fields' . $column_name_appendix . '"' 1209 . ' type="hidden" value="' . $selected_html . '">'; 1210 } 1211 1212 return $html_output; 1213 } 1214 1215 /** 1216 * Get HTML radio button for less than 20 string length 1217 * 1218 * @param string $column_name_appendix the name attribute 1219 * @param string $onChangeClause onchange clause for fields 1220 * @param int $tabindex tab index 1221 * @param array $column description of column in given table 1222 * @param int $tabindex_for_value offset for the values tabindex 1223 * @param int $idindex id index 1224 * @param string $data data to edit 1225 * @param array $column_enum_values $column['values'] 1226 * @param bool $readOnly is column read only or not 1227 * 1228 * @return string an html snippet 1229 */ 1230 private function getRadioButtonDependingOnLength( 1231 $column_name_appendix, 1232 $onChangeClause, 1233 $tabindex, 1234 array $column, 1235 $tabindex_for_value, 1236 $idindex, 1237 $data, 1238 array $column_enum_values, 1239 $readOnly 1240 ) { 1241 $j = 0; 1242 $html_output = ''; 1243 foreach ($column_enum_values as $enum_value) { 1244 $html_output .= ' ' 1245 . '<input type="radio" name="fields' . $column_name_appendix . '"' 1246 . ' class="textfield"' 1247 . ' value="' . $enum_value['html'] . '"' 1248 . ' id="field_' . $idindex . '_3_' . $j . '"' 1249 . ' ' . $onChangeClause; 1250 if ($data == $enum_value['plain'] 1251 || ($data == '' 1252 && (! isset($_POST['where_clause']) || $column['Null'] !== 'YES') 1253 && isset($column['Default']) 1254 && $enum_value['plain'] == $column['Default']) 1255 ) { 1256 $html_output .= ' checked="checked"'; 1257 } elseif ($readOnly) { 1258 $html_output .= ' disabled'; 1259 } 1260 $html_output .= ' tabindex="' . ($tabindex + $tabindex_for_value) . '">'; 1261 $html_output .= '<label for="field_' . $idindex . '_3_' . $j . '">' 1262 . $enum_value['html'] . '</label>' . "\n"; 1263 $j++; 1264 } 1265 1266 return $html_output; 1267 } 1268 1269 /** 1270 * Get the HTML for 'set' pma type 1271 * 1272 * @param array $column description of column in given table 1273 * @param array $extracted_columnspec associative array containing type, 1274 * spec_in_brackets and possibly 1275 * enum_set_values (another array) 1276 * @param string $backup_field hidden input field 1277 * @param string $column_name_appendix the name attribute 1278 * @param string $onChangeClause onchange clause for fields 1279 * @param int $tabindex tab index 1280 * @param int $tabindex_for_value offset for the values tabindex 1281 * @param int $idindex id index 1282 * @param string $data description of the column field 1283 * @param bool $readOnly is column read only or not 1284 * 1285 * @return string an html snippet 1286 */ 1287 private function getPmaTypeSet( 1288 array $column, 1289 array $extracted_columnspec, 1290 $backup_field, 1291 $column_name_appendix, 1292 $onChangeClause, 1293 $tabindex, 1294 $tabindex_for_value, 1295 $idindex, 1296 $data, 1297 $readOnly 1298 ) { 1299 [$column_set_values, $select_size] = $this->getColumnSetValueAndSelectSize( 1300 $column, 1301 $extracted_columnspec 1302 ); 1303 $vset = array_flip(explode(',', $data)); 1304 $html_output = $backup_field . "\n"; 1305 $html_output .= '<input type="hidden" name="fields_type' 1306 . $column_name_appendix . '" value="set">'; 1307 $html_output .= '<select name="fields' . $column_name_appendix . '[]"' 1308 . ' class="textfield"' 1309 . ($readOnly ? ' disabled' : '') 1310 . ' size="' . $select_size . '"' 1311 . ' multiple="multiple"' 1312 . ' ' . $onChangeClause 1313 . ' tabindex="' . ($tabindex + $tabindex_for_value) . '"' 1314 . ' id="field_' . $idindex . '_3">'; 1315 1316 $selected_html = ''; 1317 foreach ($column_set_values as $column_set_value) { 1318 $html_output .= '<option value="' . $column_set_value['html'] . '"'; 1319 if (isset($vset[$column_set_value['plain']])) { 1320 $html_output .= ' selected="selected"'; 1321 $selected_html = $column_set_value['html']; 1322 } 1323 $html_output .= '>' . $column_set_value['html'] . '</option>' . "\n"; 1324 } 1325 $html_output .= '</select>'; 1326 1327 //Add hidden input, as disabled <select> input does not included in POST. 1328 if ($readOnly) { 1329 $html_output .= '<input name="fields' . $column_name_appendix . '[]"' 1330 . ' type="hidden" value="' . $selected_html . '">'; 1331 } 1332 1333 return $html_output; 1334 } 1335 1336 /** 1337 * Retrieve column 'set' value and select size 1338 * 1339 * @param array $column description of column in given table 1340 * @param array $extracted_columnspec associative array containing type, 1341 * spec_in_brackets and possibly enum_set_values 1342 * (another array) 1343 * 1344 * @return array $column['values'], $column['select_size'] 1345 */ 1346 private function getColumnSetValueAndSelectSize( 1347 array $column, 1348 array $extracted_columnspec 1349 ) { 1350 if (! isset($column['values'])) { 1351 $column['values'] = []; 1352 foreach ($extracted_columnspec['enum_set_values'] as $val) { 1353 $column['values'][] = [ 1354 'plain' => $val, 1355 'html' => htmlspecialchars($val), 1356 ]; 1357 } 1358 $column['select_size'] = min(4, count($column['values'])); 1359 } 1360 1361 return [ 1362 $column['values'], 1363 $column['select_size'], 1364 ]; 1365 } 1366 1367 /** 1368 * Get HTML for binary and blob column 1369 * 1370 * @param array $column description of column in given table 1371 * @param string|null $data data to edit 1372 * @param string $special_chars special characters 1373 * @param int $biggest_max_file_size biggest max file size for uploading 1374 * @param string $backup_field hidden input field 1375 * @param string $column_name_appendix the name attribute 1376 * @param string $onChangeClause onchange clause for fields 1377 * @param int $tabindex tab index 1378 * @param int $tabindex_for_value offset for the values tabindex 1379 * @param int $idindex id index 1380 * @param string $text_dir text direction 1381 * @param string $special_chars_encoded replaced char if the string starts 1382 * with a \r\n pair (0x0d0a) add an 1383 * extra \n 1384 * @param string $vkey [multi_edit]['row_id'] 1385 * @param bool $is_upload is upload or not 1386 * @param bool $readOnly is column read only or not 1387 * 1388 * @return string an html snippet 1389 */ 1390 private function getBinaryAndBlobColumn( 1391 array $column, 1392 ?string $data, 1393 $special_chars, 1394 $biggest_max_file_size, 1395 $backup_field, 1396 $column_name_appendix, 1397 $onChangeClause, 1398 $tabindex, 1399 $tabindex_for_value, 1400 $idindex, 1401 $text_dir, 1402 $special_chars_encoded, 1403 $vkey, 1404 $is_upload, 1405 $readOnly 1406 ) { 1407 $html_output = ''; 1408 // Add field type : Protected or Hexadecimal 1409 $fields_type_html = '<input type="hidden" name="fields_type' 1410 . $column_name_appendix . '" value="%s">'; 1411 // Default value : hex 1412 $fields_type_val = 'hex'; 1413 if (($GLOBALS['cfg']['ProtectBinary'] === 'blob' && $column['is_blob']) 1414 || ($GLOBALS['cfg']['ProtectBinary'] === 'all') 1415 || ($GLOBALS['cfg']['ProtectBinary'] === 'noblob' && ! $column['is_blob']) 1416 ) { 1417 $html_output .= __('Binary - do not edit'); 1418 if (isset($data)) { 1419 $data_size = Util::formatByteDown( 1420 mb_strlen(stripslashes($data)), 1421 3, 1422 1 1423 ); 1424 $html_output .= ' (' . $data_size[0] . ' ' . $data_size[1] . ')'; 1425 unset($data_size); 1426 } 1427 $fields_type_val = 'protected'; 1428 $html_output .= '<input type="hidden" name="fields' 1429 . $column_name_appendix . '" value="">'; 1430 } elseif ($column['is_blob'] 1431 || ($column['len'] > $GLOBALS['cfg']['LimitChars']) 1432 ) { 1433 $html_output .= "\n" . $this->getTextarea( 1434 $column, 1435 $backup_field, 1436 $column_name_appendix, 1437 $onChangeClause, 1438 $tabindex, 1439 $tabindex_for_value, 1440 $idindex, 1441 $text_dir, 1442 $special_chars_encoded, 1443 'HEX', 1444 $readOnly 1445 ); 1446 } else { 1447 // field size should be at least 4 and max $GLOBALS['cfg']['LimitChars'] 1448 $fieldsize = min(max($column['len'], 4), $GLOBALS['cfg']['LimitChars']); 1449 $html_output .= "\n" . $backup_field . "\n" . $this->getHtmlInput( 1450 $column, 1451 $column_name_appendix, 1452 $special_chars, 1453 $fieldsize, 1454 $onChangeClause, 1455 $tabindex, 1456 $tabindex_for_value, 1457 $idindex, 1458 'HEX', 1459 $readOnly 1460 ); 1461 } 1462 $html_output .= sprintf($fields_type_html, $fields_type_val); 1463 1464 if ($is_upload && $column['is_blob'] && ! $readOnly) { 1465 // We don't want to prevent users from using 1466 // browser's default drag-drop feature on some page(s), 1467 // so we add noDragDrop class to the input 1468 $html_output .= '<br>' 1469 . '<input type="file"' 1470 . ' name="fields_upload' . $vkey . '[' . $column['Field_md5'] . ']"' 1471 . ' class="textfield noDragDrop" id="field_' . $idindex . '_3" size="10"' 1472 . ' ' . $onChangeClause . '> '; 1473 [$html_out] = $this->getMaxUploadSize( 1474 $column, 1475 $biggest_max_file_size 1476 ); 1477 $html_output .= $html_out; 1478 } 1479 1480 if (! empty($GLOBALS['cfg']['UploadDir']) && ! $readOnly) { 1481 $html_output .= $this->getSelectOptionForUpload($vkey, $column); 1482 } 1483 1484 return $html_output; 1485 } 1486 1487 /** 1488 * Get HTML input type 1489 * 1490 * @param array $column description of column in given table 1491 * @param string $column_name_appendix the name attribute 1492 * @param string $special_chars special characters 1493 * @param int $fieldsize html field size 1494 * @param string $onChangeClause onchange clause for fields 1495 * @param int $tabindex tab index 1496 * @param int $tabindex_for_value offset for the values tabindex 1497 * @param int $idindex id index 1498 * @param string $data_type the html5 data-* attribute type 1499 * @param bool $readOnly is column read only or not 1500 * 1501 * @return string an html snippet 1502 */ 1503 private function getHtmlInput( 1504 array $column, 1505 $column_name_appendix, 1506 $special_chars, 1507 $fieldsize, 1508 $onChangeClause, 1509 $tabindex, 1510 $tabindex_for_value, 1511 $idindex, 1512 $data_type, 1513 $readOnly 1514 ) { 1515 $input_type = 'text'; 1516 // do not use the 'date' or 'time' types here; they have no effect on some 1517 // browsers and create side effects (see bug #4218) 1518 1519 $the_class = 'textfield'; 1520 // verify True_Type which does not contain the parentheses and length 1521 if (! $readOnly) { 1522 if ($column['True_Type'] === 'date') { 1523 $the_class .= ' datefield'; 1524 } elseif ($column['True_Type'] === 'time') { 1525 $the_class .= ' timefield'; 1526 } elseif ($column['True_Type'] === 'datetime' 1527 || $column['True_Type'] === 'timestamp' 1528 ) { 1529 $the_class .= ' datetimefield'; 1530 } 1531 } 1532 $input_min_max = false; 1533 if (in_array($column['True_Type'], $this->dbi->types->getIntegerTypes())) { 1534 $extracted_columnspec = Util::extractColumnSpec( 1535 $column['Type'] 1536 ); 1537 $is_unsigned = $extracted_columnspec['unsigned']; 1538 $min_max_values = $this->dbi->types->getIntegerRange( 1539 $column['True_Type'], 1540 ! $is_unsigned 1541 ); 1542 $input_min_max = 'min="' . $min_max_values[0] . '" ' 1543 . 'max="' . $min_max_values[1] . '"'; 1544 $data_type = 'INT'; 1545 } 1546 1547 return '<input type="' . $input_type . '"' 1548 . ' name="fields' . $column_name_appendix . '"' 1549 . ' value="' . $special_chars . '" size="' . $fieldsize . '"' 1550 . (isset($column['is_char']) && $column['is_char'] 1551 ? ' data-maxlength="' . $fieldsize . '"' 1552 : '') 1553 . ($readOnly ? ' readonly="readonly"' : '') 1554 . ($input_min_max !== false ? ' ' . $input_min_max : '') 1555 . ' data-type="' . $data_type . '"' 1556 . ($input_type === 'time' ? ' step="1"' : '') 1557 . ' class="' . $the_class . '" ' . $onChangeClause 1558 . ' tabindex="' . ($tabindex + $tabindex_for_value) . '"' 1559 . ' id="field_' . $idindex . '_3">'; 1560 } 1561 1562 /** 1563 * Get HTML select option for upload 1564 * 1565 * @param string $vkey [multi_edit]['row_id'] 1566 * @param array $column description of column in given table 1567 * 1568 * @return string|null an html snippet 1569 */ 1570 private function getSelectOptionForUpload($vkey, array $column) 1571 { 1572 $files = $this->fileListing->getFileSelectOptions( 1573 Util::userDir((string) ($GLOBALS['cfg']['UploadDir'] ?? '')) 1574 ); 1575 1576 if ($files === false) { 1577 return '<span style="color:red">' . __('Error') . '</span><br>' . "\n" 1578 . __('The directory you set for upload work cannot be reached.') . "\n"; 1579 } 1580 1581 if (! empty($files)) { 1582 return "<br>\n" 1583 . '<i>' . __('Or') . '</i> ' 1584 . __('web server upload directory:') . '<br>' . "\n" 1585 . '<select size="1" name="fields_uploadlocal' 1586 . $vkey . '[' . $column['Field_md5'] . ']">' . "\n" 1587 . '<option value="" selected="selected"></option>' . "\n" 1588 . $files 1589 . '</select>' . "\n"; 1590 } 1591 1592 return null; 1593 } 1594 1595 /** 1596 * Retrieve the maximum upload file size 1597 * 1598 * @param array $column description of column in given table 1599 * @param int $biggest_max_file_size biggest max file size for uploading 1600 * 1601 * @return array an html snippet and $biggest_max_file_size 1602 */ 1603 private function getMaxUploadSize(array $column, $biggest_max_file_size) 1604 { 1605 // find maximum upload size, based on field type 1606 /** 1607 * @todo with functions this is not so easy, as you can basically 1608 * process any data with function like MD5 1609 */ 1610 global $max_upload_size; 1611 $max_field_sizes = [ 1612 'tinyblob' => '256', 1613 'blob' => '65536', 1614 'mediumblob' => '16777216', 1615 'longblob' => '4294967296',// yeah, really 1616 ]; 1617 1618 $this_field_max_size = $max_upload_size; // from PHP max 1619 if ($this_field_max_size > $max_field_sizes[$column['pma_type']]) { 1620 $this_field_max_size = $max_field_sizes[$column['pma_type']]; 1621 } 1622 $html_output 1623 = Util::getFormattedMaximumUploadSize( 1624 $this_field_max_size 1625 ) . "\n"; 1626 // do not generate here the MAX_FILE_SIZE, because we should 1627 // put only one in the form to accommodate the biggest field 1628 if ($this_field_max_size > $biggest_max_file_size) { 1629 $biggest_max_file_size = $this_field_max_size; 1630 } 1631 1632 return [ 1633 $html_output, 1634 $biggest_max_file_size, 1635 ]; 1636 } 1637 1638 /** 1639 * Get HTML for the Value column of other datatypes 1640 * (here, "column" is used in the sense of HTML column in HTML table) 1641 * 1642 * @param array $column description of column in given table 1643 * @param string $default_char_editing default char editing mode which is stored 1644 * in the config.inc.php script 1645 * @param string $backup_field hidden input field 1646 * @param string $column_name_appendix the name attribute 1647 * @param string $onChangeClause onchange clause for fields 1648 * @param int $tabindex tab index 1649 * @param string $special_chars special characters 1650 * @param int $tabindex_for_value offset for the values tabindex 1651 * @param int $idindex id index 1652 * @param string $text_dir text direction 1653 * @param string $special_chars_encoded replaced char if the string starts 1654 * with a \r\n pair (0x0d0a) add an extra \n 1655 * @param string $data data to edit 1656 * @param array $extracted_columnspec associative array containing type, 1657 * spec_in_brackets and possibly 1658 * enum_set_values (another array) 1659 * @param bool $readOnly is column read only or not 1660 * 1661 * @return string an html snippet 1662 */ 1663 private function getValueColumnForOtherDatatypes( 1664 array $column, 1665 $default_char_editing, 1666 $backup_field, 1667 $column_name_appendix, 1668 $onChangeClause, 1669 $tabindex, 1670 $special_chars, 1671 $tabindex_for_value, 1672 $idindex, 1673 $text_dir, 1674 $special_chars_encoded, 1675 $data, 1676 array $extracted_columnspec, 1677 $readOnly 1678 ) { 1679 // HTML5 data-* attribute data-type 1680 $data_type = $this->dbi->types->getTypeClass($column['True_Type']); 1681 $fieldsize = $this->getColumnSize($column, $extracted_columnspec); 1682 $html_output = $backup_field . "\n"; 1683 if ($column['is_char'] 1684 && ($GLOBALS['cfg']['CharEditing'] === 'textarea' 1685 || mb_strpos($data, "\n") !== false) 1686 ) { 1687 $html_output .= "\n"; 1688 $GLOBALS['cfg']['CharEditing'] = $default_char_editing; 1689 $html_output .= $this->getTextarea( 1690 $column, 1691 $backup_field, 1692 $column_name_appendix, 1693 $onChangeClause, 1694 $tabindex, 1695 $tabindex_for_value, 1696 $idindex, 1697 $text_dir, 1698 $special_chars_encoded, 1699 $data_type, 1700 $readOnly 1701 ); 1702 } else { 1703 $html_output .= $this->getHtmlInput( 1704 $column, 1705 $column_name_appendix, 1706 $special_chars, 1707 $fieldsize, 1708 $onChangeClause, 1709 $tabindex, 1710 $tabindex_for_value, 1711 $idindex, 1712 $data_type, 1713 $readOnly 1714 ); 1715 1716 if (preg_match('/(VIRTUAL|PERSISTENT|GENERATED)/', $column['Extra']) 1717 && strpos($column['Extra'], 'DEFAULT_GENERATED') === false 1718 ) { 1719 $html_output .= '<input type="hidden" name="virtual' 1720 . $column_name_appendix . '" value="1">'; 1721 } 1722 if ($column['Extra'] === 'auto_increment') { 1723 $html_output .= '<input type="hidden" name="auto_increment' 1724 . $column_name_appendix . '" value="1">'; 1725 } 1726 if (substr($column['pma_type'], 0, 9) === 'timestamp') { 1727 $html_output .= '<input type="hidden" name="fields_type' 1728 . $column_name_appendix . '" value="timestamp">'; 1729 } 1730 if (substr($column['pma_type'], 0, 4) === 'date') { 1731 $type = substr($column['pma_type'], 0, 8) === 'datetime' ? 'datetime' : 'date'; 1732 $html_output .= '<input type="hidden" name="fields_type' 1733 . $column_name_appendix . '" value="' . $type . '">'; 1734 } 1735 if ($column['True_Type'] === 'bit') { 1736 $html_output .= '<input type="hidden" name="fields_type' 1737 . $column_name_appendix . '" value="bit">'; 1738 } 1739 } 1740 1741 return $html_output; 1742 } 1743 1744 /** 1745 * Get the field size 1746 * 1747 * @param array $column description of column in given table 1748 * @param array $extracted_columnspec associative array containing type, 1749 * spec_in_brackets and possibly enum_set_values 1750 * (another array) 1751 * 1752 * @return int field size 1753 */ 1754 private function getColumnSize(array $column, array $extracted_columnspec) 1755 { 1756 if ($column['is_char']) { 1757 $fieldsize = $extracted_columnspec['spec_in_brackets']; 1758 if ($fieldsize > $GLOBALS['cfg']['MaxSizeForInputField']) { 1759 /** 1760 * This case happens for CHAR or VARCHAR columns which have 1761 * a size larger than the maximum size for input field. 1762 */ 1763 $GLOBALS['cfg']['CharEditing'] = 'textarea'; 1764 } 1765 } else { 1766 /** 1767 * This case happens for example for INT or DATE columns; 1768 * in these situations, the value returned in $column['len'] 1769 * seems appropriate. 1770 */ 1771 $fieldsize = $column['len']; 1772 } 1773 1774 return min( 1775 max($fieldsize, $GLOBALS['cfg']['MinSizeForInputField']), 1776 $GLOBALS['cfg']['MaxSizeForInputField'] 1777 ); 1778 } 1779 1780 /** 1781 * Get HTML for gis data types 1782 * 1783 * @return string an html snippet 1784 */ 1785 private function getHtmlForGisDataTypes(int $rowId): string 1786 { 1787 $edit_str = Generator::getIcon('b_edit', __('Edit/Insert')); 1788 1789 return '<span class="open_gis_editor" data-row-id="' . $rowId . '">' 1790 . Generator::linkOrButton( 1791 '#', 1792 null, 1793 $edit_str, 1794 [], 1795 '_blank' 1796 ) 1797 . '</span>'; 1798 } 1799 1800 /** 1801 * get html for continue insertion form 1802 * 1803 * @param string $table name of the table 1804 * @param string $db name of the database 1805 * @param array $where_clause_array array of where clauses 1806 * @param string $err_url error url 1807 * 1808 * @return string an html snippet 1809 */ 1810 public function getContinueInsertionForm( 1811 $table, 1812 $db, 1813 array $where_clause_array, 1814 $err_url 1815 ) { 1816 return $this->template->render('table/insert/continue_insertion_form', [ 1817 'db' => $db, 1818 'table' => $table, 1819 'where_clause_array' => $where_clause_array, 1820 'err_url' => $err_url, 1821 'goto' => $GLOBALS['goto'], 1822 'sql_query' => $_POST['sql_query'] ?? null, 1823 'has_where_clause' => isset($_POST['where_clause']), 1824 'insert_rows_default' => $GLOBALS['cfg']['InsertRows'], 1825 ]); 1826 } 1827 1828 /** 1829 * Get action panel 1830 * 1831 * @param array|null $where_clause where clause 1832 * @param string $after_insert insert mode, e.g. new_insert, same_insert 1833 * @param int $tabindex tab index 1834 * @param int $tabindex_for_value offset for the values tabindex 1835 * @param bool $found_unique_key boolean variable for unique key 1836 * 1837 * @return string an html snippet 1838 */ 1839 public function getActionsPanel( 1840 $where_clause, 1841 $after_insert, 1842 $tabindex, 1843 $tabindex_for_value, 1844 $found_unique_key 1845 ) { 1846 $html_output = '<fieldset id="actions_panel">' 1847 . '<table cellpadding="5" cellspacing="0" class="pma-table tdblock w-100">' 1848 . '<tr>' 1849 . '<td class="nowrap vmiddle">' 1850 . $this->getSubmitTypeDropDown($where_clause, $tabindex, $tabindex_for_value) 1851 . "\n"; 1852 1853 $html_output .= '</td>' 1854 . '<td class="vmiddle">' 1855 . ' <strong>' 1856 . __('and then') . '</strong> ' 1857 . '</td>' 1858 . '<td class="nowrap vmiddle">' 1859 . $this->getAfterInsertDropDown( 1860 $where_clause, 1861 $after_insert, 1862 $found_unique_key 1863 ) 1864 . '</td>' 1865 . '</tr>'; 1866 $html_output .= '<tr>' 1867 . $this->getSubmitAndResetButtonForActionsPanel($tabindex, $tabindex_for_value) 1868 . '</tr>' 1869 . '</table>' 1870 . '</fieldset>'; 1871 1872 return $html_output; 1873 } 1874 1875 /** 1876 * Get a HTML drop down for submit types 1877 * 1878 * @param array|null $where_clause where clause 1879 * @param int $tabindex tab index 1880 * @param int $tabindex_for_value offset for the values tabindex 1881 * 1882 * @return string an html snippet 1883 */ 1884 private function getSubmitTypeDropDown( 1885 $where_clause, 1886 $tabindex, 1887 $tabindex_for_value 1888 ) { 1889 $html_output = '<select name="submit_type" class="control_at_footer" tabindex="' 1890 . ($tabindex + $tabindex_for_value + 1) . '">'; 1891 if (isset($where_clause)) { 1892 $html_output .= '<option value="save">' . __('Save') . '</option>'; 1893 } 1894 $html_output .= '<option value="insert">' 1895 . __('Insert as new row') 1896 . '</option>' 1897 . '<option value="insertignore">' 1898 . __('Insert as new row and ignore errors') 1899 . '</option>' 1900 . '<option value="showinsert">' 1901 . __('Show insert query') 1902 . '</option>' 1903 . '</select>'; 1904 1905 return $html_output; 1906 } 1907 1908 /** 1909 * Get HTML drop down for after insert 1910 * 1911 * @param array|null $where_clause where clause 1912 * @param string $after_insert insert mode, e.g. new_insert, same_insert 1913 * @param bool $found_unique_key boolean variable for unique key 1914 * 1915 * @return string an html snippet 1916 */ 1917 private function getAfterInsertDropDown($where_clause, $after_insert, $found_unique_key) 1918 { 1919 $html_output = '<select name="after_insert" class="control_at_footer">' 1920 . '<option value="back" ' 1921 . ($after_insert === 'back' ? 'selected="selected"' : '') . '>' 1922 . __('Go back to previous page') . '</option>' 1923 . '<option value="new_insert" ' 1924 . ($after_insert === 'new_insert' ? 'selected="selected"' : '') . '>' 1925 . __('Insert another new row') . '</option>'; 1926 1927 if (isset($where_clause)) { 1928 $html_output .= '<option value="same_insert" ' 1929 . ($after_insert === 'same_insert' ? 'selected="selected"' : '') . '>' 1930 . __('Go back to this page') . '</option>'; 1931 1932 // If we have just numeric primary key, we can also edit next 1933 // in 2.8.2, we were looking for `field_name` = numeric_value 1934 //if (preg_match('@^[\s]*`[^`]*` = [0-9]+@', $where_clause)) { 1935 // in 2.9.0, we are looking for `table_name`.`field_name` = numeric_value 1936 $is_numeric = false; 1937 if (! is_array($where_clause)) { 1938 $where_clause = [$where_clause]; 1939 } 1940 for ($i = 0, $nb = count($where_clause); $i < $nb; $i++) { 1941 // preg_match() returns 1 if there is a match 1942 $is_numeric = (preg_match( 1943 '@^[\s]*`[^`]*`[\.]`[^`]*` = [0-9]+@', 1944 $where_clause[$i] 1945 ) == 1); 1946 if ($is_numeric === true) { 1947 break; 1948 } 1949 } 1950 if ($found_unique_key && $is_numeric) { 1951 $html_output .= '<option value="edit_next" ' 1952 . ($after_insert === 'edit_next' ? 'selected="selected"' : '') . '>' 1953 . __('Edit next row') . '</option>'; 1954 } 1955 } 1956 1957 return $html_output . '</select>'; 1958 } 1959 1960 /** 1961 * get Submit button and Reset button for action panel 1962 * 1963 * @param int $tabindex tab index 1964 * @param int $tabindex_for_value offset for the values tabindex 1965 * 1966 * @return string an html snippet 1967 */ 1968 private function getSubmitAndResetButtonForActionsPanel($tabindex, $tabindex_for_value) 1969 { 1970 return '<td>' 1971 . Generator::showHint( 1972 __( 1973 'Use TAB key to move from value to value,' 1974 . ' or CTRL+arrows to move anywhere.' 1975 ) 1976 ) 1977 . '</td>' 1978 . '<td colspan="3" class="right vmiddle">' 1979 . '<input type="button" class="btn btn-secondary preview_sql" value="' . __('Preview SQL') . '"' 1980 . ' tabindex="' . ($tabindex + $tabindex_for_value + 6) . '">' 1981 . '<input type="reset" class="btn btn-secondary control_at_footer" value="' . __('Reset') . '"' 1982 . ' tabindex="' . ($tabindex + $tabindex_for_value + 7) . '">' 1983 . '<input type="submit" class="btn btn-primary control_at_footer" value="' . __('Go') . '"' 1984 . ' tabindex="' . ($tabindex + $tabindex_for_value + 8) . '" id="buttonYes">' 1985 . '</td>'; 1986 } 1987 1988 /** 1989 * Get table head and table foot for insert row table 1990 * 1991 * @param array $url_params url parameters 1992 * 1993 * @return string an html snippet 1994 */ 1995 private function getHeadAndFootOfInsertRowTable(array $url_params) 1996 { 1997 $html_output = '<div class="responsivetable">' 1998 . '<table class="pma-table insertRowTable topmargin">' 1999 . '<thead>' 2000 . '<tr>' 2001 . '<th>' . __('Column') . '</th>'; 2002 2003 if ($GLOBALS['cfg']['ShowFieldTypesInDataEditView']) { 2004 $html_output .= $this->showTypeOrFunction('type', $url_params, true); 2005 } 2006 if ($GLOBALS['cfg']['ShowFunctionFields']) { 2007 $html_output .= $this->showTypeOrFunction('function', $url_params, true); 2008 } 2009 2010 $html_output .= '<th>' . __('Null') . '</th>' 2011 . '<th class="fillPage">' . __('Value') . '</th>' 2012 . '</tr>' 2013 . '</thead>' 2014 . ' <tfoot>' 2015 . '<tr>' 2016 . '<th colspan="5" class="tblFooters right">' 2017 . '<input class="btn btn-primary" type="submit" value="' . __('Go') . '">' 2018 . '</th>' 2019 . '</tr>' 2020 . '</tfoot>'; 2021 2022 return $html_output; 2023 } 2024 2025 /** 2026 * Prepares the field value and retrieve special chars, backup field and data array 2027 * 2028 * @param array $current_row a row of the table 2029 * @param array $column description of column in given table 2030 * @param array $extracted_columnspec associative array containing type, 2031 * spec_in_brackets and possibly 2032 * enum_set_values (another array) 2033 * @param bool $real_null_value whether column value null or not null 2034 * @param array $gis_data_types list of GIS data types 2035 * @param string $column_name_appendix string to append to column name in input 2036 * @param bool $as_is use the data as is, used in repopulating 2037 * 2038 * @return array $real_null_value, $data, $special_chars, $backup_field, 2039 * $special_chars_encoded 2040 */ 2041 private function getSpecialCharsAndBackupFieldForExistingRow( 2042 array $current_row, 2043 array $column, 2044 array $extracted_columnspec, 2045 $real_null_value, 2046 array $gis_data_types, 2047 $column_name_appendix, 2048 $as_is 2049 ) { 2050 $special_chars_encoded = ''; 2051 $data = null; 2052 // (we are editing) 2053 if (! isset($current_row[$column['Field']])) { 2054 $real_null_value = true; 2055 $current_row[$column['Field']] = ''; 2056 $special_chars = ''; 2057 $data = $current_row[$column['Field']]; 2058 } elseif ($column['True_Type'] === 'bit') { 2059 $special_chars = $as_is 2060 ? $current_row[$column['Field']] 2061 : Util::printableBitValue( 2062 (int) $current_row[$column['Field']], 2063 (int) $extracted_columnspec['spec_in_brackets'] 2064 ); 2065 } elseif ((substr($column['True_Type'], 0, 9) === 'timestamp' 2066 || $column['True_Type'] === 'datetime' 2067 || $column['True_Type'] === 'time') 2068 && (mb_strpos($current_row[$column['Field']], '.') !== false) 2069 ) { 2070 $current_row[$column['Field']] = $as_is 2071 ? $current_row[$column['Field']] 2072 : Util::addMicroseconds( 2073 $current_row[$column['Field']] 2074 ); 2075 $special_chars = htmlspecialchars($current_row[$column['Field']], ENT_COMPAT); 2076 } elseif (in_array($column['True_Type'], $gis_data_types)) { 2077 // Convert gis data to Well Know Text format 2078 $current_row[$column['Field']] = $as_is 2079 ? $current_row[$column['Field']] 2080 : Util::asWKT( 2081 $current_row[$column['Field']], 2082 true 2083 ); 2084 $special_chars = htmlspecialchars($current_row[$column['Field']], ENT_COMPAT); 2085 } else { 2086 // special binary "characters" 2087 if ($column['is_binary'] 2088 || ($column['is_blob'] && $GLOBALS['cfg']['ProtectBinary'] !== 'all') 2089 ) { 2090 $current_row[$column['Field']] = $as_is 2091 ? $current_row[$column['Field']] 2092 : bin2hex( 2093 $current_row[$column['Field']] 2094 ); 2095 } 2096 $special_chars = htmlspecialchars($current_row[$column['Field']], ENT_COMPAT); 2097 2098 //We need to duplicate the first \n or otherwise we will lose 2099 //the first newline entered in a VARCHAR or TEXT column 2100 $special_chars_encoded 2101 = Util::duplicateFirstNewline($special_chars); 2102 2103 $data = $current_row[$column['Field']]; 2104 } 2105 2106 //when copying row, it is useful to empty auto-increment column 2107 // to prevent duplicate key error 2108 if (isset($_POST['default_action']) 2109 && $_POST['default_action'] === 'insert' 2110 ) { 2111 if ($column['Key'] === 'PRI' 2112 && mb_strpos($column['Extra'], 'auto_increment') !== false 2113 ) { 2114 $data = $special_chars_encoded = $special_chars = null; 2115 } 2116 } 2117 // If a timestamp field value is not included in an update 2118 // statement MySQL auto-update it to the current timestamp; 2119 // however, things have changed since MySQL 4.1, so 2120 // it's better to set a fields_prev in this situation 2121 $backup_field = '<input type="hidden" name="fields_prev' 2122 . $column_name_appendix . '" value="' 2123 . htmlspecialchars($current_row[$column['Field']], ENT_COMPAT) . '">'; 2124 2125 return [ 2126 $real_null_value, 2127 $special_chars_encoded, 2128 $special_chars, 2129 $data, 2130 $backup_field, 2131 ]; 2132 } 2133 2134 /** 2135 * display default values 2136 * 2137 * @param array $column description of column in given table 2138 * @param bool $real_null_value whether column value null or not null 2139 * 2140 * @return array $real_null_value, $data, $special_chars, 2141 * $backup_field, $special_chars_encoded 2142 */ 2143 private function getSpecialCharsAndBackupFieldForInsertingMode( 2144 array $column, 2145 $real_null_value 2146 ) { 2147 if (! isset($column['Default'])) { 2148 $column['Default'] = ''; 2149 $real_null_value = true; 2150 $data = ''; 2151 } else { 2152 $data = $column['Default']; 2153 } 2154 2155 $trueType = $column['True_Type']; 2156 2157 if ($trueType === 'bit') { 2158 $special_chars = Util::convertBitDefaultValue( 2159 $column['Default'] 2160 ); 2161 } elseif (substr($trueType, 0, 9) === 'timestamp' 2162 || $trueType === 'datetime' 2163 || $trueType === 'time' 2164 ) { 2165 $special_chars = Util::addMicroseconds($column['Default']); 2166 } elseif ($trueType === 'binary' || $trueType === 'varbinary') { 2167 $special_chars = bin2hex($column['Default']); 2168 } elseif (substr($trueType, -4) === 'text') { 2169 $textDefault = substr($column['Default'], 1, -1); 2170 $special_chars = stripcslashes($textDefault !== false ? $textDefault : $column['Default']); 2171 } else { 2172 $special_chars = htmlspecialchars($column['Default']); 2173 } 2174 $backup_field = ''; 2175 $special_chars_encoded = Util::duplicateFirstNewline( 2176 $special_chars 2177 ); 2178 2179 return [ 2180 $real_null_value, 2181 $data, 2182 $special_chars, 2183 $backup_field, 2184 $special_chars_encoded, 2185 ]; 2186 } 2187 2188 /** 2189 * Prepares the update/insert of a row 2190 * 2191 * @return array $loop_array, $using_key, $is_insert, $is_insertignore 2192 */ 2193 public function getParamsForUpdateOrInsert() 2194 { 2195 if (isset($_POST['where_clause'])) { 2196 // we were editing something => use the WHERE clause 2197 $loop_array = is_array($_POST['where_clause']) 2198 ? $_POST['where_clause'] 2199 : [$_POST['where_clause']]; 2200 $using_key = true; 2201 $is_insert = isset($_POST['submit_type']) 2202 && ($_POST['submit_type'] === 'insert' 2203 || $_POST['submit_type'] === 'showinsert' 2204 || $_POST['submit_type'] === 'insertignore'); 2205 } else { 2206 // new row => use indexes 2207 $loop_array = []; 2208 if (! empty($_POST['fields'])) { 2209 foreach ($_POST['fields']['multi_edit'] as $key => $dummy) { 2210 $loop_array[] = $key; 2211 } 2212 } 2213 $using_key = false; 2214 $is_insert = true; 2215 } 2216 $is_insertignore = isset($_POST['submit_type']) 2217 && $_POST['submit_type'] === 'insertignore'; 2218 2219 return [ 2220 $loop_array, 2221 $using_key, 2222 $is_insert, 2223 $is_insertignore, 2224 ]; 2225 } 2226 2227 /** 2228 * Check wether insert row mode and if so include tbl_changen script and set 2229 * global variables. 2230 * 2231 * @return void 2232 */ 2233 public function isInsertRow() 2234 { 2235 global $containerBuilder; 2236 2237 if (! isset($_POST['insert_rows']) 2238 || ! is_numeric($_POST['insert_rows']) 2239 || $_POST['insert_rows'] == $GLOBALS['cfg']['InsertRows'] 2240 ) { 2241 return; 2242 } 2243 2244 $GLOBALS['cfg']['InsertRows'] = $_POST['insert_rows']; 2245 $response = Response::getInstance(); 2246 $header = $response->getHeader(); 2247 $scripts = $header->getScripts(); 2248 $scripts->addFile('vendor/jquery/additional-methods.js'); 2249 $scripts->addFile('table/change.js'); 2250 if (! defined('TESTSUITE')) { 2251 /** @var ChangeController $controller */ 2252 $controller = $containerBuilder->get(ChangeController::class); 2253 $controller->index(); 2254 exit; 2255 } 2256 } 2257 2258 /** 2259 * set $_SESSION for edit_next 2260 * 2261 * @param string $one_where_clause one where clause from where clauses array 2262 * 2263 * @return void 2264 */ 2265 public function setSessionForEditNext($one_where_clause) 2266 { 2267 $local_query = 'SELECT * FROM ' . Util::backquote($GLOBALS['db']) 2268 . '.' . Util::backquote($GLOBALS['table']) . ' WHERE ' 2269 . str_replace('` =', '` >', $one_where_clause) . ' LIMIT 1;'; 2270 2271 $res = $this->dbi->query($local_query); 2272 $row = $this->dbi->fetchRow($res); 2273 $meta = $this->dbi->getFieldsMeta($res); 2274 // must find a unique condition based on unique key, 2275 // not a combination of all fields 2276 [$unique_condition, $clause_is_unique] = Util::getUniqueCondition( 2277 $res, 2278 count($meta), 2279 $meta, 2280 $row ?? [], 2281 true 2282 ); 2283 if (! empty($unique_condition)) { 2284 $_SESSION['edit_next'] = $unique_condition; 2285 } 2286 unset($unique_condition, $clause_is_unique); 2287 } 2288 2289 /** 2290 * set $goto_include variable for different cases and retrieve like, 2291 * if $GLOBALS['goto'] empty, if $goto_include previously not defined 2292 * and new_insert, same_insert, edit_next 2293 * 2294 * @param string|false $goto_include store some script for include, otherwise it is 2295 * boolean false 2296 * 2297 * @return string|false 2298 */ 2299 public function getGotoInclude($goto_include) 2300 { 2301 $valid_options = [ 2302 'new_insert', 2303 'same_insert', 2304 'edit_next', 2305 ]; 2306 if (isset($_POST['after_insert']) 2307 && in_array($_POST['after_insert'], $valid_options) 2308 ) { 2309 $goto_include = '/table/change'; 2310 } elseif (! empty($GLOBALS['goto'])) { 2311 if (! preg_match('@^[a-z_]+\.php$@', $GLOBALS['goto'])) { 2312 // this should NOT happen 2313 //$GLOBALS['goto'] = false; 2314 if ($GLOBALS['goto'] === 'index.php?route=/sql') { 2315 $goto_include = '/sql'; 2316 } else { 2317 $goto_include = false; 2318 } 2319 } else { 2320 $goto_include = $GLOBALS['goto']; 2321 } 2322 if ($GLOBALS['goto'] === 'index.php?route=/database/sql' && strlen($GLOBALS['table']) > 0) { 2323 $GLOBALS['table'] = ''; 2324 } 2325 } 2326 if (! $goto_include) { 2327 if (strlen($GLOBALS['table']) === 0) { 2328 $goto_include = '/database/sql'; 2329 } else { 2330 $goto_include = '/table/sql'; 2331 } 2332 } 2333 2334 return $goto_include; 2335 } 2336 2337 /** 2338 * Defines the url to return in case of failure of the query 2339 * 2340 * @param array $url_params url parameters 2341 * 2342 * @return string error url for query failure 2343 */ 2344 public function getErrorUrl(array $url_params) 2345 { 2346 if (isset($_POST['err_url'])) { 2347 return $_POST['err_url']; 2348 } 2349 2350 return Url::getFromRoute('/table/change', $url_params); 2351 } 2352 2353 /** 2354 * Builds the sql query 2355 * 2356 * @param bool $is_insertignore $_POST['submit_type'] === 'insertignore' 2357 * @param array $query_fields column names array 2358 * @param array $value_sets array of query values 2359 * 2360 * @return array of query 2361 */ 2362 public function buildSqlQuery($is_insertignore, array $query_fields, array $value_sets) 2363 { 2364 if ($is_insertignore) { 2365 $insert_command = 'INSERT IGNORE '; 2366 } else { 2367 $insert_command = 'INSERT '; 2368 } 2369 2370 return [ 2371 $insert_command . 'INTO ' 2372 . Util::backquote($GLOBALS['table']) 2373 . ' (' . implode(', ', $query_fields) . ') VALUES (' 2374 . implode('), (', $value_sets) . ')', 2375 ]; 2376 } 2377 2378 /** 2379 * Executes the sql query and get the result, then move back to the calling page 2380 * 2381 * @param array $url_params url parameters array 2382 * @param array $query built query from buildSqlQuery() 2383 * 2384 * @return array $url_params, $total_affected_rows, $last_messages 2385 * $warning_messages, $error_messages, $return_to_sql_query 2386 */ 2387 public function executeSqlQuery(array $url_params, array $query) 2388 { 2389 $return_to_sql_query = ''; 2390 if (! empty($GLOBALS['sql_query'])) { 2391 $url_params['sql_query'] = $GLOBALS['sql_query']; 2392 $return_to_sql_query = $GLOBALS['sql_query']; 2393 } 2394 $GLOBALS['sql_query'] = implode('; ', $query) . ';'; 2395 // to ensure that the query is displayed in case of 2396 // "insert as new row" and then "insert another new row" 2397 $GLOBALS['display_query'] = $GLOBALS['sql_query']; 2398 2399 $total_affected_rows = 0; 2400 $last_messages = []; 2401 $warning_messages = []; 2402 $error_messages = []; 2403 2404 foreach ($query as $single_query) { 2405 if (isset($_POST['submit_type']) && $_POST['submit_type'] === 'showinsert') { 2406 $last_messages[] = Message::notice(__('Showing SQL query')); 2407 continue; 2408 } 2409 if ($GLOBALS['cfg']['IgnoreMultiSubmitErrors']) { 2410 $result = $this->dbi->tryQuery($single_query); 2411 } else { 2412 $result = $this->dbi->query($single_query); 2413 } 2414 if (! $result) { 2415 $error_messages[] = $this->dbi->getError(); 2416 } else { 2417 $tmp = @$this->dbi->affectedRows(); 2418 2419 if ($tmp) { 2420 $total_affected_rows += $tmp; 2421 } 2422 unset($tmp); 2423 2424 $insert_id = $this->dbi->insertId(); 2425 if ($insert_id != 0) { 2426 // insert_id is id of FIRST record inserted in one insert, so if we 2427 // inserted multiple rows, we had to increment this 2428 2429 if ($total_affected_rows > 0) { 2430 $insert_id += $total_affected_rows - 1; 2431 } 2432 $last_message = Message::notice(__('Inserted row id: %1$d')); 2433 $last_message->addParam($insert_id); 2434 $last_messages[] = $last_message; 2435 } 2436 $this->dbi->freeResult($result); 2437 } 2438 $warning_messages = $this->getWarningMessages(); 2439 } 2440 2441 return [ 2442 $url_params, 2443 $total_affected_rows, 2444 $last_messages, 2445 $warning_messages, 2446 $error_messages, 2447 $return_to_sql_query, 2448 ]; 2449 } 2450 2451 /** 2452 * get the warning messages array 2453 * 2454 * @return array 2455 */ 2456 private function getWarningMessages() 2457 { 2458 $warning_essages = []; 2459 foreach ($this->dbi->getWarnings() as $warning) { 2460 $warning_essages[] = Message::sanitize( 2461 $warning['Level'] . ': #' . $warning['Code'] . ' ' . $warning['Message'] 2462 ); 2463 } 2464 2465 return $warning_essages; 2466 } 2467 2468 /** 2469 * Column to display from the foreign table? 2470 * 2471 * @param string $where_comparison string that contain relation field value 2472 * @param array $map all Relations to foreign tables for a given 2473 * table or optionally a given column in a table 2474 * @param string $relation_field relation field 2475 * 2476 * @return string display value from the foreign table 2477 */ 2478 public function getDisplayValueForForeignTableColumn( 2479 $where_comparison, 2480 array $map, 2481 $relation_field 2482 ) { 2483 $foreigner = $this->relation->searchColumnInForeigners($map, $relation_field); 2484 2485 if (! is_array($foreigner)) { 2486 return ''; 2487 } 2488 2489 $display_field = $this->relation->getDisplayField( 2490 $foreigner['foreign_db'], 2491 $foreigner['foreign_table'] 2492 ); 2493 // Field to display from the foreign table? 2494 if (is_string($display_field) && strlen($display_field) > 0) { 2495 $dispsql = 'SELECT ' . Util::backquote($display_field) 2496 . ' FROM ' . Util::backquote($foreigner['foreign_db']) 2497 . '.' . Util::backquote($foreigner['foreign_table']) 2498 . ' WHERE ' . Util::backquote($foreigner['foreign_field']) 2499 . $where_comparison; 2500 $dispresult = $this->dbi->tryQuery( 2501 $dispsql, 2502 DatabaseInterface::CONNECT_USER, 2503 DatabaseInterface::QUERY_STORE 2504 ); 2505 if ($dispresult && $this->dbi->numRows($dispresult) > 0) { 2506 [$dispval] = $this->dbi->fetchRow($dispresult); 2507 } else { 2508 $dispval = ''; 2509 } 2510 if ($dispresult) { 2511 $this->dbi->freeResult($dispresult); 2512 } 2513 2514 return $dispval; 2515 } 2516 2517 return ''; 2518 } 2519 2520 /** 2521 * Display option in the cell according to user choices 2522 * 2523 * @param array $map all Relations to foreign tables for a given 2524 * table or optionally a given column in a table 2525 * @param string $relation_field relation field 2526 * @param string $where_comparison string that contain relation field value 2527 * @param string $dispval display value from the foreign table 2528 * @param string $relation_field_value relation field value 2529 * 2530 * @return string HTML <a> tag 2531 */ 2532 public function getLinkForRelationalDisplayField( 2533 array $map, 2534 $relation_field, 2535 $where_comparison, 2536 $dispval, 2537 $relation_field_value 2538 ) { 2539 $foreigner = $this->relation->searchColumnInForeigners($map, $relation_field); 2540 2541 if (! is_array($foreigner)) { 2542 return ''; 2543 } 2544 2545 if ($_SESSION['tmpval']['relational_display'] === 'K') { 2546 // user chose "relational key" in the display options, so 2547 // the title contains the display field 2548 $title = ! empty($dispval) 2549 ? ' title="' . htmlspecialchars($dispval) . '"' 2550 : ''; 2551 } else { 2552 $title = ' title="' . htmlspecialchars($relation_field_value) . '"'; 2553 } 2554 $sqlQuery = 'SELECT * FROM ' 2555 . Util::backquote($foreigner['foreign_db']) 2556 . '.' . Util::backquote($foreigner['foreign_table']) 2557 . ' WHERE ' . Util::backquote($foreigner['foreign_field']) 2558 . $where_comparison; 2559 $_url_params = [ 2560 'db' => $foreigner['foreign_db'], 2561 'table' => $foreigner['foreign_table'], 2562 'pos' => '0', 2563 'sql_signature' => Core::signSqlQuery($sqlQuery), 2564 'sql_query' => $sqlQuery, 2565 ]; 2566 $output = '<a href="' . Url::getFromRoute('/sql', $_url_params) . '"' . $title . '>'; 2567 2568 if ($_SESSION['tmpval']['relational_display'] === 'D') { 2569 // user chose "relational display field" in the 2570 // display options, so show display field in the cell 2571 $output .= ! empty($dispval) ? htmlspecialchars($dispval) : ''; 2572 } else { 2573 // otherwise display data in the cell 2574 $output .= htmlspecialchars($relation_field_value); 2575 } 2576 $output .= '</a>'; 2577 2578 return $output; 2579 } 2580 2581 /** 2582 * Transform edited values 2583 * 2584 * @param string $db db name 2585 * @param string $table table name 2586 * @param array $transformation mimetypes for all columns of a table 2587 * [field_name][field_key] 2588 * @param array $edited_values transform columns list and new values 2589 * @param string $file file containing the transformation plugin 2590 * @param string $column_name column name 2591 * @param array $extra_data extra data array 2592 * @param string $type the type of transformation 2593 * 2594 * @return array 2595 */ 2596 public function transformEditedValues( 2597 $db, 2598 $table, 2599 array $transformation, 2600 array &$edited_values, 2601 $file, 2602 $column_name, 2603 array $extra_data, 2604 $type 2605 ) { 2606 $include_file = 'libraries/classes/Plugins/Transformations/' . $file; 2607 if (is_file(ROOT_PATH . $include_file)) { 2608 // $cfg['SaveCellsAtOnce'] = true; JS code sends an array 2609 $whereClause = is_array($_POST['where_clause']) ? $_POST['where_clause'][0] : $_POST['where_clause']; 2610 $_url_params = [ 2611 'db' => $db, 2612 'table' => $table, 2613 'where_clause_sign' => Core::signSqlQuery($whereClause), 2614 'where_clause' => $whereClause, 2615 'transform_key' => $column_name, 2616 ]; 2617 $transform_options = $this->transformations->getOptions( 2618 $transformation[$type . '_options'] ?? '' 2619 ); 2620 $transform_options['wrapper_link'] = Url::getCommon($_url_params); 2621 $transform_options['wrapper_params'] = $_url_params; 2622 $class_name = $this->transformations->getClassName($include_file); 2623 if (class_exists($class_name)) { 2624 /** @var TransformationsPlugin $transformation_plugin */ 2625 $transformation_plugin = new $class_name(); 2626 2627 foreach ($edited_values as $cell_index => $curr_cell_edited_values) { 2628 if (! isset($curr_cell_edited_values[$column_name])) { 2629 continue; 2630 } 2631 2632 $edited_values[$cell_index][$column_name] 2633 = $extra_data['transformations'][$cell_index] 2634 = $transformation_plugin->applyTransformation( 2635 $curr_cell_edited_values[$column_name], 2636 $transform_options 2637 ); 2638 } 2639 } 2640 } 2641 2642 return $extra_data; 2643 } 2644 2645 /** 2646 * Get current value in multi edit mode 2647 * 2648 * @param array $multi_edit_funcs multiple edit functions array 2649 * @param array $multi_edit_salt multiple edit array with encryption salt 2650 * @param array $gis_from_text_functions array that contains gis from text functions 2651 * @param string $current_value current value in the column 2652 * @param array $gis_from_wkb_functions initially $val is $multi_edit_columns[$key] 2653 * @param array $func_optional_param array('RAND','UNIX_TIMESTAMP') 2654 * @param array $func_no_param array of set of string 2655 * @param string $key an md5 of the column name 2656 * 2657 * @return string 2658 */ 2659 public function getCurrentValueAsAnArrayForMultipleEdit( 2660 $multi_edit_funcs, 2661 $multi_edit_salt, 2662 $gis_from_text_functions, 2663 $current_value, 2664 $gis_from_wkb_functions, 2665 $func_optional_param, 2666 $func_no_param, 2667 $key 2668 ) { 2669 if (empty($multi_edit_funcs[$key])) { 2670 return $current_value; 2671 } 2672 2673 if ($multi_edit_funcs[$key] === 'PHP_PASSWORD_HASH') { 2674 /** 2675 * @see https://github.com/vimeo/psalm/issues/3350 2676 * 2677 * @psalm-suppress InvalidArgument 2678 */ 2679 $hash = password_hash($current_value, PASSWORD_DEFAULT); 2680 2681 return "'" . $hash . "'"; 2682 } 2683 2684 if ($multi_edit_funcs[$key] === 'UUID') { 2685 /* This way user will know what UUID new row has */ 2686 $uuid = $this->dbi->fetchValue('SELECT UUID()'); 2687 2688 return "'" . $uuid . "'"; 2689 } 2690 2691 if ( 2692 in_array($multi_edit_funcs[$key], $gis_from_text_functions) || 2693 in_array($multi_edit_funcs[$key], $gis_from_wkb_functions) 2694 ) { 2695 // Remove enclosing apostrophes 2696 $current_value = mb_substr($current_value, 1, -1); 2697 // Remove escaping apostrophes 2698 $current_value = str_replace("''", "'", $current_value); 2699 // Remove backslash-escaped apostrophes 2700 $current_value = str_replace("\'", "'", $current_value); 2701 2702 return $multi_edit_funcs[$key] . '(' . $current_value . ')'; 2703 } 2704 2705 if (! in_array($multi_edit_funcs[$key], $func_no_param) 2706 || ($current_value != "''" 2707 && in_array($multi_edit_funcs[$key], $func_optional_param)) 2708 ) { 2709 if ((isset($multi_edit_salt[$key]) 2710 && ($multi_edit_funcs[$key] === 'AES_ENCRYPT' 2711 || $multi_edit_funcs[$key] === 'AES_DECRYPT')) 2712 || (! empty($multi_edit_salt[$key]) 2713 && ($multi_edit_funcs[$key] === 'DES_ENCRYPT' 2714 || $multi_edit_funcs[$key] === 'DES_DECRYPT' 2715 || $multi_edit_funcs[$key] === 'ENCRYPT')) 2716 ) { 2717 return $multi_edit_funcs[$key] . '(' . $current_value . ",'" 2718 . $this->dbi->escapeString($multi_edit_salt[$key]) . "')"; 2719 } 2720 2721 return $multi_edit_funcs[$key] . '(' . $current_value . ')'; 2722 } 2723 2724 return $multi_edit_funcs[$key] . '()'; 2725 } 2726 2727 /** 2728 * Get query values array and query fields array for insert and update in multi edit 2729 * 2730 * @param array $multi_edit_columns_name multiple edit columns name array 2731 * @param array $multi_edit_columns_null multiple edit columns null array 2732 * @param string $current_value current value in the column in loop 2733 * @param array $multi_edit_columns_prev multiple edit previous columns array 2734 * @param array $multi_edit_funcs multiple edit functions array 2735 * @param bool $is_insert boolean value whether insert or not 2736 * @param array $query_values SET part of the sql query 2737 * @param array $query_fields array of query fields 2738 * @param string $current_value_as_an_array current value in the column 2739 * as an array 2740 * @param array $value_sets array of valu sets 2741 * @param string $key an md5 of the column name 2742 * @param array $multi_edit_columns_null_prev array of multiple edit columns 2743 * null previous 2744 * 2745 * @return array ($query_values, $query_fields) 2746 */ 2747 public function getQueryValuesForInsertAndUpdateInMultipleEdit( 2748 $multi_edit_columns_name, 2749 $multi_edit_columns_null, 2750 $current_value, 2751 $multi_edit_columns_prev, 2752 $multi_edit_funcs, 2753 $is_insert, 2754 $query_values, 2755 $query_fields, 2756 $current_value_as_an_array, 2757 $value_sets, 2758 $key, 2759 $multi_edit_columns_null_prev 2760 ) { 2761 // i n s e r t 2762 if ($is_insert) { 2763 // no need to add column into the valuelist 2764 if (strlen($current_value_as_an_array) > 0) { 2765 $query_values[] = $current_value_as_an_array; 2766 // first inserted row so prepare the list of fields 2767 if (empty($value_sets)) { 2768 $query_fields[] = Util::backquote( 2769 $multi_edit_columns_name[$key] 2770 ); 2771 } 2772 } 2773 } elseif (! empty($multi_edit_columns_null_prev[$key]) 2774 && ! isset($multi_edit_columns_null[$key]) 2775 ) { 2776 // u p d a t e 2777 2778 // field had the null checkbox before the update 2779 // field no longer has the null checkbox 2780 $query_values[] 2781 = Util::backquote($multi_edit_columns_name[$key]) 2782 . ' = ' . $current_value_as_an_array; 2783 } elseif (! (empty($multi_edit_funcs[$key]) 2784 && isset($multi_edit_columns_prev[$key]) 2785 && (($current_value === "'" . $this->dbi->escapeString($multi_edit_columns_prev[$key]) . "'") 2786 || ($current_value === '0x' . $multi_edit_columns_prev[$key]))) 2787 && ! empty($current_value) 2788 ) { 2789 // avoid setting a field to NULL when it's already NULL 2790 // (field had the null checkbox before the update 2791 // field still has the null checkbox) 2792 if (empty($multi_edit_columns_null_prev[$key]) 2793 || empty($multi_edit_columns_null[$key]) 2794 ) { 2795 $query_values[] 2796 = Util::backquote($multi_edit_columns_name[$key]) 2797 . ' = ' . $current_value_as_an_array; 2798 } 2799 } 2800 2801 return [ 2802 $query_values, 2803 $query_fields, 2804 ]; 2805 } 2806 2807 /** 2808 * Get the current column value in the form for different data types 2809 * 2810 * @param string|false $possibly_uploaded_val uploaded file content 2811 * @param string $key an md5 of the column name 2812 * @param array|null $multi_edit_columns_type array of multi edit column types 2813 * @param string $current_value current column value in the form 2814 * @param array|null $multi_edit_auto_increment multi edit auto increment 2815 * @param int $rownumber index of where clause array 2816 * @param array $multi_edit_columns_name multi edit column names array 2817 * @param array $multi_edit_columns_null multi edit columns null array 2818 * @param array $multi_edit_columns_null_prev multi edit columns previous null 2819 * @param bool $is_insert whether insert or not 2820 * @param bool $using_key whether editing or new row 2821 * @param string $where_clause where clause 2822 * @param string $table table name 2823 * @param array $multi_edit_funcs multiple edit functions array 2824 * 2825 * @return string current column value in the form 2826 */ 2827 public function getCurrentValueForDifferentTypes( 2828 $possibly_uploaded_val, 2829 $key, 2830 ?array $multi_edit_columns_type, 2831 $current_value, 2832 ?array $multi_edit_auto_increment, 2833 $rownumber, 2834 $multi_edit_columns_name, 2835 $multi_edit_columns_null, 2836 $multi_edit_columns_null_prev, 2837 $is_insert, 2838 $using_key, 2839 $where_clause, 2840 $table, 2841 $multi_edit_funcs 2842 ) { 2843 // Fetch the current values of a row to use in case we have a protected field 2844 if ($is_insert 2845 && $using_key && isset($multi_edit_columns_type) 2846 && is_array($multi_edit_columns_type) && ! empty($where_clause) 2847 ) { 2848 $protected_row = $this->dbi->fetchSingleRow( 2849 'SELECT * FROM ' . Util::backquote($table) 2850 . ' WHERE ' . $where_clause . ';' 2851 ); 2852 } 2853 2854 if ($possibly_uploaded_val !== false) { 2855 $current_value = $possibly_uploaded_val; 2856 } elseif (! empty($multi_edit_funcs[$key])) { 2857 $current_value = "'" . $this->dbi->escapeString($current_value) 2858 . "'"; 2859 } else { 2860 // c o l u m n v a l u e i n t h e f o r m 2861 if (isset($multi_edit_columns_type[$key])) { 2862 $type = $multi_edit_columns_type[$key]; 2863 } else { 2864 $type = ''; 2865 } 2866 2867 if ($type !== 'protected' && $type !== 'set' && strlen($current_value) === 0) { 2868 // best way to avoid problems in strict mode 2869 // (works also in non-strict mode) 2870 if (isset($multi_edit_auto_increment, $multi_edit_auto_increment[$key])) { 2871 $current_value = 'NULL'; 2872 } else { 2873 $current_value = "''"; 2874 } 2875 } elseif ($type === 'set') { 2876 if (! empty($_POST['fields']['multi_edit'][$rownumber][$key])) { 2877 $current_value = implode( 2878 ',', 2879 $_POST['fields']['multi_edit'][$rownumber][$key] 2880 ); 2881 $current_value = "'" 2882 . $this->dbi->escapeString($current_value) . "'"; 2883 } else { 2884 $current_value = "''"; 2885 } 2886 } elseif ($type === 'protected') { 2887 // here we are in protected mode (asked in the config) 2888 // so tbl_change has put this special value in the 2889 // columns array, so we do not change the column value 2890 // but we can still handle column upload 2891 2892 // when in UPDATE mode, do not alter field's contents. When in INSERT 2893 // mode, insert empty field because no values were submitted. 2894 // If protected blobs where set, insert original fields content. 2895 if (! empty($protected_row[$multi_edit_columns_name[$key]])) { 2896 $current_value = '0x' 2897 . bin2hex($protected_row[$multi_edit_columns_name[$key]]); 2898 } else { 2899 $current_value = ''; 2900 } 2901 } elseif ($type === 'hex') { 2902 if (substr($current_value, 0, 2) != '0x') { 2903 $current_value = '0x' . $current_value; 2904 } 2905 } elseif ($type === 'bit') { 2906 $current_value = preg_replace('/[^01]/', '0', $current_value); 2907 $current_value = "b'" . $this->dbi->escapeString($current_value) 2908 . "'"; 2909 } elseif (! ($type === 'datetime' || $type === 'timestamp' || $type === 'date') 2910 || ($current_value !== 'CURRENT_TIMESTAMP' 2911 && $current_value !== 'current_timestamp()') 2912 ) { 2913 $current_value = "'" . $this->dbi->escapeString($current_value) 2914 . "'"; 2915 } 2916 2917 // Was the Null checkbox checked for this field? 2918 // (if there is a value, we ignore the Null checkbox: this could 2919 // be possible if Javascript is disabled in the browser) 2920 if (! empty($multi_edit_columns_null[$key]) 2921 && ($current_value == "''" || $current_value == '') 2922 ) { 2923 $current_value = 'NULL'; 2924 } 2925 2926 // The Null checkbox was unchecked for this field 2927 if (empty($current_value) 2928 && ! empty($multi_edit_columns_null_prev[$key]) 2929 && ! isset($multi_edit_columns_null[$key]) 2930 ) { 2931 $current_value = "''"; 2932 } 2933 } 2934 2935 return $current_value; 2936 } 2937 2938 /** 2939 * Check whether inline edited value can be truncated or not, 2940 * and add additional parameters for extra_data array if needed 2941 * 2942 * @param string $db Database name 2943 * @param string $table Table name 2944 * @param string $column_name Column name 2945 * @param array $extra_data Extra data for ajax response 2946 * 2947 * @return void 2948 */ 2949 public function verifyWhetherValueCanBeTruncatedAndAppendExtraData( 2950 $db, 2951 $table, 2952 $column_name, 2953 array &$extra_data 2954 ) { 2955 $extra_data['isNeedToRecheck'] = false; 2956 2957 $sql_for_real_value = 'SELECT ' . Util::backquote($table) . '.' 2958 . Util::backquote($column_name) 2959 . ' FROM ' . Util::backquote($db) . '.' 2960 . Util::backquote($table) 2961 . ' WHERE ' . $_POST['where_clause'][0]; 2962 2963 $result = $this->dbi->tryQuery($sql_for_real_value); 2964 $fields_meta = $this->dbi->getFieldsMeta($result); 2965 $meta = $fields_meta[0]; 2966 $row = $this->dbi->fetchRow($result); 2967 2968 if ($row) { 2969 $new_value = $row[0]; 2970 if ((substr($meta->type, 0, 9) === 'timestamp') 2971 || ($meta->type === 'datetime') 2972 || ($meta->type === 'time') 2973 ) { 2974 $new_value = Util::addMicroseconds($new_value); 2975 } elseif (mb_strpos($meta->flags, 'binary') !== false) { 2976 $new_value = '0x' . bin2hex($new_value); 2977 } 2978 $extra_data['isNeedToRecheck'] = true; 2979 $extra_data['truncatableFieldValue'] = $new_value; 2980 } 2981 $this->dbi->freeResult($result); 2982 } 2983 2984 /** 2985 * Function to get the columns of a table 2986 * 2987 * @param string $db current db 2988 * @param string $table current table 2989 * 2990 * @return array 2991 */ 2992 public function getTableColumns($db, $table) 2993 { 2994 $this->dbi->selectDb($db); 2995 2996 return array_values($this->dbi->getColumns($db, $table, null, true)); 2997 } 2998 2999 /** 3000 * Function to determine Insert/Edit rows 3001 * 3002 * @param string $where_clause where clause 3003 * @param string $db current database 3004 * @param string $table current table 3005 * 3006 * @return array 3007 */ 3008 public function determineInsertOrEdit($where_clause, $db, $table): array 3009 { 3010 if (isset($_POST['where_clause'])) { 3011 $where_clause = $_POST['where_clause']; 3012 } 3013 if (isset($_SESSION['edit_next'])) { 3014 $where_clause = $_SESSION['edit_next']; 3015 unset($_SESSION['edit_next']); 3016 $after_insert = 'edit_next'; 3017 } 3018 if (isset($_POST['ShowFunctionFields'])) { 3019 $GLOBALS['cfg']['ShowFunctionFields'] = $_POST['ShowFunctionFields']; 3020 } 3021 if (isset($_POST['ShowFieldTypesInDataEditView'])) { 3022 $GLOBALS['cfg']['ShowFieldTypesInDataEditView'] 3023 = $_POST['ShowFieldTypesInDataEditView']; 3024 } 3025 if (isset($_POST['after_insert'])) { 3026 $after_insert = $_POST['after_insert']; 3027 } 3028 3029 if (isset($where_clause)) { 3030 // we are editing 3031 $insert_mode = false; 3032 $where_clause_array = $this->getWhereClauseArray($where_clause); 3033 [$where_clauses, $result, $rows, $found_unique_key] 3034 = $this->analyzeWhereClauses( 3035 $where_clause_array, 3036 $table, 3037 $db 3038 ); 3039 } else { 3040 // we are inserting 3041 $insert_mode = true; 3042 $where_clause = null; 3043 [$result, $rows] = $this->loadFirstRow($table, $db); 3044 $where_clauses = null; 3045 $where_clause_array = []; 3046 $found_unique_key = false; 3047 } 3048 3049 // Copying a row - fetched data will be inserted as a new row, 3050 // therefore the where clause is needless. 3051 if (isset($_POST['default_action']) 3052 && $_POST['default_action'] === 'insert' 3053 ) { 3054 $where_clause = $where_clauses = null; 3055 } 3056 3057 return [ 3058 $insert_mode, 3059 $where_clause, 3060 $where_clause_array, 3061 $where_clauses, 3062 $result, 3063 $rows, 3064 $found_unique_key, 3065 $after_insert ?? null, 3066 ]; 3067 } 3068 3069 /** 3070 * Function to get comments for the table columns 3071 * 3072 * @param string $db current database 3073 * @param string $table current table 3074 * 3075 * @return array comments for columns 3076 */ 3077 public function getCommentsMap($db, $table) 3078 { 3079 $comments_map = []; 3080 3081 if ($GLOBALS['cfg']['ShowPropertyComments']) { 3082 $comments_map = $this->relation->getComments($db, $table); 3083 } 3084 3085 return $comments_map; 3086 } 3087 3088 /** 3089 * Function to get URL parameters 3090 * 3091 * @param string $db current database 3092 * @param string $table current table 3093 * 3094 * @return array url parameters 3095 */ 3096 public function getUrlParameters($db, $table) 3097 { 3098 global $goto; 3099 /** 3100 * @todo check if we could replace by "db_|tbl_" - please clarify!? 3101 */ 3102 $url_params = [ 3103 'db' => $db, 3104 'sql_query' => $_POST['sql_query'] ?? '', 3105 ]; 3106 3107 if (strpos($goto, 'tbl_') === 0 || strpos($goto, 'index.php?route=/table') === 0) { 3108 $url_params['table'] = $table; 3109 } 3110 3111 return $url_params; 3112 } 3113 3114 /** 3115 * Function to get html for the gis editor div 3116 * 3117 * @return string 3118 */ 3119 public function getHtmlForGisEditor() 3120 { 3121 return '<div id="gis_editor"></div>' 3122 . '<div id="popup_background"></div>' 3123 . '<br>'; 3124 } 3125 3126 /** 3127 * Function to get html for the ignore option in insert mode 3128 * 3129 * @param int $row_id row id 3130 * @param bool $checked ignore option is checked or not 3131 * 3132 * @return string 3133 */ 3134 public function getHtmlForIgnoreOption($row_id, $checked = true) 3135 { 3136 return '<input type="checkbox"' 3137 . ($checked ? ' checked="checked"' : '') 3138 . ' name="insert_ignore_' . $row_id . '"' 3139 . ' id="insert_ignore_' . $row_id . '">' 3140 . '<label for="insert_ignore_' . $row_id . '">' 3141 . __('Ignore') 3142 . '</label><br>' . "\n"; 3143 } 3144 3145 /** 3146 * Function to get html for the function option 3147 * 3148 * @param array $column column 3149 * @param string $column_name_appendix column name appendix 3150 */ 3151 private function getHtmlForFunctionOption(array $column, $column_name_appendix): string 3152 { 3153 return '<tr class="noclick">' 3154 . '<td ' 3155 . 'class="text-center">' 3156 . $column['Field_title'] 3157 . '<input type="hidden" name="fields_name' . $column_name_appendix 3158 . '" value="' . $column['Field_html'] . '">' 3159 . '</td>'; 3160 } 3161 3162 /** 3163 * Function to get html for the column type 3164 * 3165 * @param array $column column 3166 * 3167 * @return string 3168 */ 3169 private function getHtmlForInsertEditColumnType(array $column) 3170 { 3171 return '<td class="text-center' . $column['wrap'] . '">' 3172 . '<span class="column_type" dir="ltr">' . $column['pma_type'] . '</span>' 3173 . '</td>'; 3174 } 3175 3176 /** 3177 * Function to get html for the insert edit form header 3178 * 3179 * @param bool $has_blob_field whether has blob field 3180 * @param bool $is_upload whether is upload 3181 * 3182 * @return string 3183 */ 3184 public function getHtmlForInsertEditFormHeader($has_blob_field, $is_upload) 3185 { 3186 $html_output = '<form id="insertForm" class="lock-page '; 3187 if ($has_blob_field && $is_upload) { 3188 $html_output .= 'disableAjax'; 3189 } 3190 $html_output .= '" method="post" action="' . Url::getFromRoute('/table/replace') . '" name="insertForm" '; 3191 if ($is_upload) { 3192 $html_output .= ' enctype="multipart/form-data"'; 3193 } 3194 $html_output .= '>'; 3195 3196 return $html_output; 3197 } 3198 3199 /** 3200 * Function to get html for each insert/edit column 3201 * 3202 * @param array $table_columns table columns 3203 * @param int $column_number column index in table_columns 3204 * @param array $comments_map comments map 3205 * @param bool $timestamp_seen whether timestamp seen 3206 * @param array $current_result current result 3207 * @param string $chg_evt_handler javascript change event handler 3208 * @param string $jsvkey javascript validation key 3209 * @param string $vkey validation key 3210 * @param bool $insert_mode whether insert mode 3211 * @param array $current_row current row 3212 * @param int $o_rows row offset 3213 * @param int $tabindex tab index 3214 * @param int $columns_cnt columns count 3215 * @param bool $is_upload whether upload 3216 * @param int $tabindex_for_function tab index offset for function 3217 * @param array $foreigners foreigners 3218 * @param int $tabindex_for_null tab index offset for null 3219 * @param int $tabindex_for_value tab index offset for value 3220 * @param string $table table 3221 * @param string $db database 3222 * @param int $row_id row id 3223 * @param int $biggest_max_file_size biggest max file size 3224 * @param string $default_char_editing default char editing mode which is stored 3225 * in the config.inc.php script 3226 * @param string $text_dir text direction 3227 * @param array $repopulate the data to be repopulated 3228 * @param array $column_mime the mime information of column 3229 * @param string $where_clause the where clause 3230 * 3231 * @return string 3232 */ 3233 private function getHtmlForInsertEditFormColumn( 3234 array $table_columns, 3235 $column_number, 3236 array $comments_map, 3237 $timestamp_seen, 3238 $current_result, 3239 $chg_evt_handler, 3240 $jsvkey, 3241 $vkey, 3242 $insert_mode, 3243 array $current_row, 3244 &$o_rows, 3245 &$tabindex, 3246 $columns_cnt, 3247 $is_upload, 3248 $tabindex_for_function, 3249 array $foreigners, 3250 $tabindex_for_null, 3251 $tabindex_for_value, 3252 $table, 3253 $db, 3254 $row_id, 3255 $biggest_max_file_size, 3256 $default_char_editing, 3257 $text_dir, 3258 array $repopulate, 3259 array $column_mime, 3260 $where_clause 3261 ) { 3262 $column = $table_columns[$column_number]; 3263 $readOnly = false; 3264 3265 if (! isset($column['processed'])) { 3266 $column = $this->analyzeTableColumnsArray( 3267 $column, 3268 $comments_map, 3269 $timestamp_seen 3270 ); 3271 } 3272 $as_is = false; 3273 if (! empty($repopulate) && ! empty($current_row)) { 3274 $current_row[$column['Field']] = $repopulate[$column['Field_md5']]; 3275 $as_is = true; 3276 } 3277 3278 $extracted_columnspec 3279 = Util::extractColumnSpec($column['Type']); 3280 3281 if ($column['len'] === -1) { 3282 $column['len'] = $this->dbi->fieldLen( 3283 $current_result, 3284 $column_number 3285 ); 3286 // length is unknown for geometry fields, 3287 // make enough space to edit very simple WKTs 3288 if ($column['len'] === -1) { 3289 $column['len'] = 30; 3290 } 3291 } 3292 //Call validation when the form submitted... 3293 $onChangeClause = $chg_evt_handler 3294 . "=\"return verificationsAfterFieldChange('" 3295 . Sanitize::escapeJsString($column['Field_md5']) . "', '" 3296 . Sanitize::escapeJsString($jsvkey) . "','" . $column['pma_type'] . "')\""; 3297 3298 // Use an MD5 as an array index to avoid having special characters 3299 // in the name attribute (see bug #1746964 ) 3300 $column_name_appendix = $vkey . '[' . $column['Field_md5'] . ']'; 3301 3302 if ($column['Type'] === 'datetime' && $column['Null'] !== 'YES' 3303 && ! isset($column['Default']) && $insert_mode) { 3304 $column['Default'] = date('Y-m-d H:i:s', time()); 3305 } 3306 3307 $html_output = $this->getHtmlForFunctionOption( 3308 $column, 3309 $column_name_appendix 3310 ); 3311 3312 if ($GLOBALS['cfg']['ShowFieldTypesInDataEditView']) { 3313 $html_output .= $this->getHtmlForInsertEditColumnType($column); 3314 } 3315 3316 // Get a list of GIS data types. 3317 $gis_data_types = Util::getGISDatatypes(); 3318 3319 // Prepares the field value 3320 $real_null_value = false; 3321 $special_chars_encoded = ''; 3322 if (! empty($current_row)) { 3323 // (we are editing) 3324 [ 3325 $real_null_value, 3326 $special_chars_encoded, 3327 $special_chars, 3328 $data, 3329 $backup_field, 3330 ] 3331 = $this->getSpecialCharsAndBackupFieldForExistingRow( 3332 $current_row, 3333 $column, 3334 $extracted_columnspec, 3335 $real_null_value, 3336 $gis_data_types, 3337 $column_name_appendix, 3338 $as_is 3339 ); 3340 } else { 3341 // (we are inserting) 3342 // display default values 3343 $tmp = $column; 3344 if (isset($repopulate[$column['Field_md5']])) { 3345 $tmp['Default'] = $repopulate[$column['Field_md5']]; 3346 } 3347 [ 3348 $real_null_value, 3349 $data, 3350 $special_chars, 3351 $backup_field, 3352 $special_chars_encoded, 3353 ] 3354 = $this->getSpecialCharsAndBackupFieldForInsertingMode( 3355 $tmp, 3356 $real_null_value 3357 ); 3358 unset($tmp); 3359 } 3360 3361 $idindex = ($o_rows * $columns_cnt) + $column_number + 1; 3362 $tabindex = $idindex; 3363 3364 // Get a list of data types that are not yet supported. 3365 $no_support_types = Util::unsupportedDatatypes(); 3366 3367 // The function column 3368 // ------------------- 3369 $foreignData = $this->relation->getForeignData( 3370 $foreigners, 3371 $column['Field'], 3372 false, 3373 '', 3374 '' 3375 ); 3376 if ($GLOBALS['cfg']['ShowFunctionFields']) { 3377 $html_output .= $this->getFunctionColumn( 3378 $column, 3379 $is_upload, 3380 $column_name_appendix, 3381 $onChangeClause, 3382 $no_support_types, 3383 $tabindex_for_function, 3384 $tabindex, 3385 $idindex, 3386 $insert_mode, 3387 $readOnly, 3388 $foreignData 3389 ); 3390 } 3391 3392 // The null column 3393 // --------------- 3394 $html_output .= $this->getNullColumn( 3395 $column, 3396 $column_name_appendix, 3397 $real_null_value, 3398 $tabindex, 3399 $tabindex_for_null, 3400 $idindex, 3401 $vkey, 3402 $foreigners, 3403 $foreignData, 3404 $readOnly 3405 ); 3406 3407 // The value column (depends on type) 3408 // ---------------- 3409 // See bug #1667887 for the reason why we don't use the maxlength 3410 // HTML attribute 3411 3412 //add data attributes "no of decimals" and "data type" 3413 $no_decimals = 0; 3414 $type = current(explode('(', $column['pma_type'])); 3415 if (preg_match('/\(([^()]+)\)/', $column['pma_type'], $match)) { 3416 $match[0] = trim($match[0], '()'); 3417 $no_decimals = $match[0]; 3418 } 3419 $html_output .= '<td data-type="' . $type . '" data-decimals="' 3420 . $no_decimals . '">' . "\n"; 3421 // Will be used by js/table/change.js to set the default value 3422 // for the "Continue insertion" feature 3423 $html_output .= '<span class="default_value hide">' 3424 . $special_chars . '</span>'; 3425 3426 // Check input transformation of column 3427 $transformed_html = ''; 3428 if (! empty($column_mime['input_transformation'])) { 3429 $file = $column_mime['input_transformation']; 3430 $include_file = 'libraries/classes/Plugins/Transformations/' . $file; 3431 if (is_file(ROOT_PATH . $include_file)) { 3432 $class_name = $this->transformations->getClassName($include_file); 3433 if (class_exists($class_name)) { 3434 $transformation_plugin = new $class_name(); 3435 $transformation_options = $this->transformations->getOptions( 3436 $column_mime['input_transformation_options'] 3437 ); 3438 $_url_params = [ 3439 'db' => $db, 3440 'table' => $table, 3441 'transform_key' => $column['Field'], 3442 'where_clause_sign' => Core::signSqlQuery($where_clause), 3443 'where_clause' => $where_clause, 3444 ]; 3445 $transformation_options['wrapper_link'] = Url::getCommon($_url_params); 3446 $transformation_options['wrapper_params'] = $_url_params; 3447 $current_value = ''; 3448 if (isset($current_row[$column['Field']])) { 3449 $current_value = $current_row[$column['Field']]; 3450 } 3451 if (method_exists($transformation_plugin, 'getInputHtml')) { 3452 $transformed_html = $transformation_plugin->getInputHtml( 3453 $column, 3454 $row_id, 3455 $column_name_appendix, 3456 $transformation_options, 3457 $current_value, 3458 $text_dir, 3459 $tabindex, 3460 $tabindex_for_value, 3461 $idindex 3462 ); 3463 } 3464 if (method_exists($transformation_plugin, 'getScripts')) { 3465 $GLOBALS['plugin_scripts'] = array_merge( 3466 $GLOBALS['plugin_scripts'], 3467 $transformation_plugin->getScripts() 3468 ); 3469 } 3470 } 3471 } 3472 } 3473 if (! empty($transformed_html)) { 3474 $html_output .= $transformed_html; 3475 } else { 3476 $html_output .= $this->getValueColumn( 3477 $column, 3478 $backup_field, 3479 $column_name_appendix, 3480 $onChangeClause, 3481 $tabindex, 3482 $tabindex_for_value, 3483 $idindex, 3484 $data, 3485 $special_chars, 3486 $foreignData, 3487 [ 3488 $table, 3489 $db, 3490 ], 3491 $row_id, 3492 $text_dir, 3493 $special_chars_encoded, 3494 $vkey, 3495 $is_upload, 3496 $biggest_max_file_size, 3497 $default_char_editing, 3498 $no_support_types, 3499 $gis_data_types, 3500 $extracted_columnspec, 3501 $readOnly 3502 ); 3503 } 3504 3505 return $html_output; 3506 } 3507 3508 /** 3509 * Function to get html for each insert/edit row 3510 * 3511 * @param array $url_params url parameters 3512 * @param array $table_columns table columns 3513 * @param array $comments_map comments map 3514 * @param bool $timestamp_seen whether timestamp seen 3515 * @param array $current_result current result 3516 * @param string $chg_evt_handler javascript change event handler 3517 * @param string $jsvkey javascript validation key 3518 * @param string $vkey validation key 3519 * @param bool $insert_mode whether insert mode 3520 * @param array $current_row current row 3521 * @param int $o_rows row offset 3522 * @param int $tabindex tab index 3523 * @param int $columns_cnt columns count 3524 * @param bool $is_upload whether upload 3525 * @param int $tabindex_for_function tab index offset for function 3526 * @param array $foreigners foreigners 3527 * @param int $tabindex_for_null tab index offset for null 3528 * @param int $tabindex_for_value tab index offset for value 3529 * @param string $table table 3530 * @param string $db database 3531 * @param int $row_id row id 3532 * @param int $biggest_max_file_size biggest max file size 3533 * @param string $text_dir text direction 3534 * @param array $repopulate the data to be repopulated 3535 * @param array $where_clause_array the array of where clauses 3536 * 3537 * @return string 3538 */ 3539 public function getHtmlForInsertEditRow( 3540 array $url_params, 3541 array $table_columns, 3542 array $comments_map, 3543 $timestamp_seen, 3544 $current_result, 3545 $chg_evt_handler, 3546 $jsvkey, 3547 $vkey, 3548 $insert_mode, 3549 array $current_row, 3550 &$o_rows, 3551 &$tabindex, 3552 $columns_cnt, 3553 $is_upload, 3554 $tabindex_for_function, 3555 array $foreigners, 3556 $tabindex_for_null, 3557 $tabindex_for_value, 3558 $table, 3559 $db, 3560 $row_id, 3561 $biggest_max_file_size, 3562 $text_dir, 3563 array $repopulate, 3564 array $where_clause_array 3565 ) { 3566 $html_output = $this->getHeadAndFootOfInsertRowTable($url_params) 3567 . '<tbody>'; 3568 3569 //store the default value for CharEditing 3570 $default_char_editing = $GLOBALS['cfg']['CharEditing']; 3571 $mime_map = $this->transformations->getMime($db, $table); 3572 $where_clause = ''; 3573 if (isset($where_clause_array[$row_id])) { 3574 $where_clause = $where_clause_array[$row_id]; 3575 } 3576 for ($column_number = 0; $column_number < $columns_cnt; $column_number++) { 3577 $table_column = $table_columns[$column_number]; 3578 $column_mime = []; 3579 if (isset($mime_map[$table_column['Field']])) { 3580 $column_mime = $mime_map[$table_column['Field']]; 3581 } 3582 3583 $virtual = [ 3584 'VIRTUAL', 3585 'PERSISTENT', 3586 'VIRTUAL GENERATED', 3587 'STORED GENERATED', 3588 ]; 3589 if (in_array($table_column['Extra'], $virtual)) { 3590 continue; 3591 } 3592 3593 $html_output .= $this->getHtmlForInsertEditFormColumn( 3594 $table_columns, 3595 $column_number, 3596 $comments_map, 3597 $timestamp_seen, 3598 $current_result, 3599 $chg_evt_handler, 3600 $jsvkey, 3601 $vkey, 3602 $insert_mode, 3603 $current_row, 3604 $o_rows, 3605 $tabindex, 3606 $columns_cnt, 3607 $is_upload, 3608 $tabindex_for_function, 3609 $foreigners, 3610 $tabindex_for_null, 3611 $tabindex_for_value, 3612 $table, 3613 $db, 3614 $row_id, 3615 $biggest_max_file_size, 3616 $default_char_editing, 3617 $text_dir, 3618 $repopulate, 3619 $column_mime, 3620 $where_clause 3621 ); 3622 } 3623 $o_rows++; 3624 3625 return $html_output . ' </tbody>' 3626 . '</table></div><br>' 3627 . '<div class="clearfloat"></div>'; 3628 } 3629} 3630