1<?php 2/** 3 * Generic plugin interface. 4 */ 5 6declare(strict_types=1); 7 8namespace PhpMyAdmin; 9 10use PhpMyAdmin\Html\MySQLDocumentation; 11use PhpMyAdmin\Plugins\AuthenticationPlugin; 12use PhpMyAdmin\Plugins\ExportPlugin; 13use PhpMyAdmin\Plugins\ImportPlugin; 14use PhpMyAdmin\Plugins\SchemaPlugin; 15use PhpMyAdmin\Properties\Options\Groups\OptionsPropertySubgroup; 16use PhpMyAdmin\Properties\Options\Items\BoolPropertyItem; 17use PhpMyAdmin\Properties\Options\Items\DocPropertyItem; 18use PhpMyAdmin\Properties\Options\Items\HiddenPropertyItem; 19use PhpMyAdmin\Properties\Options\Items\MessageOnlyPropertyItem; 20use PhpMyAdmin\Properties\Options\Items\NumberPropertyItem; 21use PhpMyAdmin\Properties\Options\Items\RadioPropertyItem; 22use PhpMyAdmin\Properties\Options\Items\SelectPropertyItem; 23use PhpMyAdmin\Properties\Options\Items\TextPropertyItem; 24use PhpMyAdmin\Properties\Options\OptionsPropertyItem; 25use PhpMyAdmin\Properties\Plugins\ExportPluginProperties; 26use PhpMyAdmin\Properties\Plugins\PluginPropertyItem; 27use PhpMyAdmin\Properties\Plugins\SchemaPluginProperties; 28use function array_pop; 29use function class_exists; 30use function count; 31use function explode; 32use function get_class; 33use function htmlspecialchars; 34use function is_file; 35use function mb_strlen; 36use function mb_strpos; 37use function mb_strtolower; 38use function mb_strtoupper; 39use function mb_substr; 40use function method_exists; 41use function opendir; 42use function preg_match; 43use function preg_match_all; 44use function readdir; 45use function str_replace; 46use function strcasecmp; 47use function strcmp; 48use function strtolower; 49use function ucfirst; 50use function usort; 51 52/** 53 * PhpMyAdmin\Plugins class 54 */ 55class Plugins 56{ 57 /** 58 * Includes and instantiates the specified plugin type for a certain format 59 * 60 * @param string $plugin_type the type of the plugin (import, export, etc) 61 * @param string $plugin_format the format of the plugin (sql, xml, et ) 62 * @param string $plugins_dir directory with plugins 63 * @param mixed $plugin_param parameter to plugin by which they can 64 * decide whether they can work 65 * 66 * @return object|null new plugin instance 67 */ 68 public static function getPlugin( 69 $plugin_type, 70 $plugin_format, 71 $plugins_dir, 72 $plugin_param = false 73 ) { 74 $GLOBALS['plugin_param'] = $plugin_param; 75 $class_name = mb_strtoupper($plugin_type[0]) 76 . mb_strtolower(mb_substr($plugin_type, 1)) 77 . mb_strtoupper($plugin_format[0]) 78 . mb_strtolower(mb_substr($plugin_format, 1)); 79 $file = $class_name . '.php'; 80 81 $fullFsPathPluginDir = ROOT_PATH . $plugins_dir; 82 83 if (is_file($fullFsPathPluginDir . $file)) { 84 //include_once $fullFsPathPluginDir . $file; 85 $fqnClass = 'PhpMyAdmin\\' . str_replace('/', '\\', mb_substr($plugins_dir, 18)) . $class_name; 86 // check if class exists, could be caused by skip_import 87 if (class_exists($fqnClass)) { 88 return new $fqnClass(); 89 } 90 } 91 92 return null; 93 } 94 95 /** 96 * @param string $type server|database|table|raw 97 * 98 * @return ExportPlugin[] 99 */ 100 public static function getExport(string $type, bool $singleTable): array 101 { 102 return self::getPlugins('export', 'libraries/classes/Plugins/Export/', [ 103 'export_type' => $type, 104 'single_table' => $singleTable, 105 ]); 106 } 107 108 /** 109 * @param string $type server|database|table 110 * 111 * @return ImportPlugin[] 112 */ 113 public static function getImport(string $type): array 114 { 115 return self::getPlugins('import', 'libraries/classes/Plugins/Import/', $type); 116 } 117 118 /** 119 * @return SchemaPlugin[] 120 */ 121 public static function getSchema(): array 122 { 123 return self::getPlugins('schema', 'libraries/classes/Plugins/Schema/', null); 124 } 125 126 /** 127 * Reads all plugin information from directory $plugins_dir 128 * 129 * @param string $plugin_type the type of the plugin (import, export, etc) 130 * @param string $plugins_dir directory with plugins 131 * @param array|string|null $plugin_param parameter to plugin by which they can 132 * decide whether they can work 133 * 134 * @return array list of plugin instances 135 */ 136 private static function getPlugins(string $plugin_type, string $plugins_dir, $plugin_param): array 137 { 138 global $skip_import; 139 140 $GLOBALS['plugin_param'] = $plugin_param; 141 142 $fullFsPathPluginDir = ROOT_PATH . $plugins_dir; 143 144 $handle = @opendir($fullFsPathPluginDir); 145 if (! $handle) { 146 return []; 147 } 148 149 $plugin_list = []; 150 151 $namespace = 'PhpMyAdmin\\' . str_replace('/', '\\', mb_substr($plugins_dir, 18)); 152 $class_type = mb_strtoupper($plugin_type[0], 'UTF-8') 153 . mb_strtolower(mb_substr($plugin_type, 1), 'UTF-8'); 154 155 $prefix_class_name = $namespace . $class_type; 156 157 while ($file = @readdir($handle)) { 158 // In some situations, Mac OS creates a new file for each file 159 // (for example ._csv.php) so the following regexp 160 // matches a file which does not start with a dot but ends 161 // with ".php" 162 if (! is_file($fullFsPathPluginDir . $file) 163 || ! preg_match( 164 '@^' . $class_type . '([^\.]+)\.php$@i', 165 $file, 166 $matches 167 ) 168 ) { 169 continue; 170 } 171 172 /** @var bool $skip_import */ 173 $skip_import = false; 174 175 include_once $fullFsPathPluginDir . $file; 176 177 if ($skip_import) { 178 continue; 179 } 180 181 $class_name = $prefix_class_name . $matches[1]; 182 $plugin = new $class_name(); 183 if ($plugin->getProperties() === null) { 184 continue; 185 } 186 187 $plugin_list[] = $plugin; 188 } 189 190 usort( 191 $plugin_list, 192 /** 193 * @param mixed $cmp_name_1 194 * @param mixed $cmp_name_2 195 */ 196 static function ($cmp_name_1, $cmp_name_2) { 197 return strcasecmp( 198 $cmp_name_1->getProperties()->getText(), 199 $cmp_name_2->getProperties()->getText() 200 ); 201 } 202 ); 203 204 return $plugin_list; 205 } 206 207 /** 208 * Returns locale string for $name or $name if no locale is found 209 * 210 * @param string $name for local string 211 * 212 * @return string locale string for $name 213 */ 214 public static function getString($name) 215 { 216 return $GLOBALS[$name] ?? $name; 217 } 218 219 /** 220 * Returns html input tag option 'checked' if plugin $opt 221 * should be set by config or request 222 * 223 * @param string $section name of config section in 224 * $GLOBALS['cfg'][$section] for plugin 225 * @param string $opt name of option 226 * 227 * @return string html input tag option 'checked' 228 */ 229 public static function checkboxCheck($section, $opt) 230 { 231 // If the form is being repopulated using $_GET data, that is priority 232 if (isset($_GET[$opt]) 233 || ! isset($_GET['repopulate']) 234 && ((! empty($GLOBALS['timeout_passed']) && isset($_REQUEST[$opt])) 235 || ! empty($GLOBALS['cfg'][$section][$opt])) 236 ) { 237 return ' checked="checked"'; 238 } 239 240 return ''; 241 } 242 243 /** 244 * Returns default value for option $opt 245 * 246 * @param string $section name of config section in 247 * $GLOBALS['cfg'][$section] for plugin 248 * @param string $opt name of option 249 * 250 * @return string default value for option $opt 251 */ 252 public static function getDefault($section, $opt) 253 { 254 if (isset($_GET[$opt])) { 255 // If the form is being repopulated using $_GET data, that is priority 256 return htmlspecialchars($_GET[$opt]); 257 } 258 259 if (isset($GLOBALS['timeout_passed'], $_REQUEST[$opt]) && $GLOBALS['timeout_passed']) { 260 return htmlspecialchars($_REQUEST[$opt]); 261 } 262 263 if (! isset($GLOBALS['cfg'][$section][$opt])) { 264 return ''; 265 } 266 267 $matches = []; 268 /* Possibly replace localised texts */ 269 if (! preg_match_all( 270 '/(str[A-Z][A-Za-z0-9]*)/', 271 (string) $GLOBALS['cfg'][$section][$opt], 272 $matches 273 )) { 274 return htmlspecialchars((string) $GLOBALS['cfg'][$section][$opt]); 275 } 276 277 $val = $GLOBALS['cfg'][$section][$opt]; 278 foreach ($matches[0] as $match) { 279 if (! isset($GLOBALS[$match])) { 280 continue; 281 } 282 283 $val = str_replace($match, $GLOBALS[$match], $val); 284 } 285 286 return htmlspecialchars($val); 287 } 288 289 /** 290 * Returns html select form element for plugin choice 291 * and hidden fields denoting whether each plugin must be exported as a file 292 * 293 * @param string $section name of config section in 294 * $GLOBALS['cfg'][$section] for plugin 295 * @param string $name name of select element 296 * @param array $list array with plugin instances 297 * @param string $cfgname name of config value, if none same as $name 298 * 299 * @return string html select tag 300 */ 301 public static function getChoice($section, $name, array $list, $cfgname = null) 302 { 303 if (! isset($cfgname)) { 304 $cfgname = $name; 305 } 306 $ret = '<select id="plugins" name="' . $name . '">'; 307 $default = self::getDefault($section, $cfgname); 308 $hidden = null; 309 foreach ($list as $plugin) { 310 $elem = explode('\\', get_class($plugin)); 311 $plugin_name = (string) array_pop($elem); 312 unset($elem); 313 $plugin_name = mb_strtolower( 314 mb_substr( 315 $plugin_name, 316 mb_strlen($section) 317 ) 318 ); 319 $ret .= '<option'; 320 // If the form is being repopulated using $_GET data, that is priority 321 if (isset($_GET[$name]) 322 && $plugin_name == $_GET[$name] 323 || ! isset($_GET[$name]) 324 && $plugin_name == $default 325 ) { 326 $ret .= ' selected="selected"'; 327 } 328 329 /** @var PluginPropertyItem $properties */ 330 $properties = $plugin->getProperties(); 331 $text = null; 332 if ($properties != null) { 333 $text = $properties->getText(); 334 } 335 $ret .= ' value="' . $plugin_name . '">' 336 . self::getString($text) 337 . '</option>' . "\n"; 338 339 // Whether each plugin has to be saved as a file 340 $hidden .= '<input type="hidden" id="force_file_' . $plugin_name 341 . '" value="'; 342 /** @var ExportPluginProperties|SchemaPluginProperties $properties */ 343 $properties = $plugin->getProperties(); 344 if (! strcmp($section, 'Import') 345 || ($properties != null && $properties->getForceFile() != null) 346 ) { 347 $hidden .= 'true'; 348 } else { 349 $hidden .= 'false'; 350 } 351 $hidden .= '">' . "\n"; 352 } 353 $ret .= '</select>' . "\n" . $hidden; 354 355 return $ret; 356 } 357 358 /** 359 * Returns single option in a list element 360 * 361 * @param string $section name of config section in $GLOBALS['cfg'][$section] for plugin 362 * @param string $plugin_name unique plugin name 363 * @param OptionsPropertyItem $propertyGroup options property main group instance 364 * @param bool $is_subgroup if this group is a subgroup 365 * 366 * @return string table row with option 367 */ 368 public static function getOneOption( 369 $section, 370 $plugin_name, 371 &$propertyGroup, 372 $is_subgroup = false 373 ) { 374 $ret = "\n"; 375 376 $properties = null; 377 if (! $is_subgroup) { 378 // for subgroup headers 379 if (mb_strpos(get_class($propertyGroup), 'PropertyItem')) { 380 $properties = [$propertyGroup]; 381 } else { 382 // for main groups 383 $ret .= '<div class="export_sub_options" id="' . $plugin_name . '_' 384 . $propertyGroup->getName() . '">'; 385 386 $text = null; 387 if (method_exists($propertyGroup, 'getText')) { 388 $text = $propertyGroup->getText(); 389 } 390 391 if ($text != null) { 392 $ret .= '<h4>' . self::getString($text) . '</h4>'; 393 } 394 $ret .= '<ul>'; 395 } 396 } 397 398 if (! isset($properties)) { 399 $not_subgroup_header = true; 400 if (method_exists($propertyGroup, 'getProperties')) { 401 $properties = $propertyGroup->getProperties(); 402 } 403 } 404 405 $property_class = null; 406 if (isset($properties)) { 407 /** @var OptionsPropertySubgroup $propertyItem */ 408 foreach ($properties as $propertyItem) { 409 $property_class = get_class($propertyItem); 410 // if the property is a subgroup, we deal with it recursively 411 if (mb_strpos($property_class, 'Subgroup')) { 412 // for subgroups 413 // each subgroup can have a header, which may also be a form element 414 /** @var OptionsPropertyItem $subgroup_header */ 415 $subgroup_header = $propertyItem->getSubgroupHeader(); 416 if ($subgroup_header !== null) { 417 $ret .= self::getOneOption( 418 $section, 419 $plugin_name, 420 $subgroup_header 421 ); 422 } 423 424 $ret .= '<li class="subgroup"><ul'; 425 if ($subgroup_header !== null) { 426 $ret .= ' id="ul_' . $subgroup_header->getName() . '">'; 427 } else { 428 $ret .= '>'; 429 } 430 431 $ret .= self::getOneOption( 432 $section, 433 $plugin_name, 434 $propertyItem, 435 true 436 ); 437 continue; 438 } 439 440 // single property item 441 $ret .= self::getHtmlForProperty( 442 $section, 443 $plugin_name, 444 $propertyItem 445 ); 446 } 447 } 448 449 if ($is_subgroup) { 450 // end subgroup 451 $ret .= '</ul></li>'; 452 } else { 453 // end main group 454 if (! empty($not_subgroup_header)) { 455 $ret .= '</ul></div>'; 456 } 457 } 458 459 if (method_exists($propertyGroup, 'getDoc')) { 460 $doc = $propertyGroup->getDoc(); 461 if ($doc != null) { 462 if (count($doc) === 3) { 463 $ret .= MySQLDocumentation::show( 464 $doc[1], 465 false, 466 null, 467 null, 468 $doc[2] 469 ); 470 } elseif (count($doc) === 1) { 471 $ret .= MySQLDocumentation::showDocumentation('faq', $doc[0]); 472 } else { 473 $ret .= MySQLDocumentation::show( 474 $doc[1] 475 ); 476 } 477 } 478 } 479 480 // Close the list element after $doc link is displayed 481 if ($property_class !== null) { 482 if ($property_class == BoolPropertyItem::class 483 || $property_class == MessageOnlyPropertyItem::class 484 || $property_class == SelectPropertyItem::class 485 || $property_class == TextPropertyItem::class 486 ) { 487 $ret .= '</li>'; 488 } 489 } 490 491 return $ret . "\n"; 492 } 493 494 /** 495 * Get HTML for properties items 496 * 497 * @param string $section name of config section in 498 * $GLOBALS['cfg'][$section] for plugin 499 * @param string $plugin_name unique plugin name 500 * @param OptionsPropertyItem $propertyItem Property item 501 * 502 * @return string 503 */ 504 public static function getHtmlForProperty( 505 $section, 506 $plugin_name, 507 $propertyItem 508 ) { 509 $ret = null; 510 $property_class = get_class($propertyItem); 511 switch ($property_class) { 512 case BoolPropertyItem::class: 513 $ret .= '<li>' . "\n"; 514 $ret .= '<input type="checkbox" name="' . $plugin_name . '_' 515 . $propertyItem->getName() . '"' 516 . ' value="something" id="checkbox_' . $plugin_name . '_' 517 . $propertyItem->getName() . '"' 518 . ' ' 519 . self::checkboxCheck( 520 $section, 521 $plugin_name . '_' . $propertyItem->getName() 522 ); 523 524 if ($propertyItem->getForce() != null) { 525 // Same code is also few lines lower, update both if needed 526 $ret .= ' onclick="if (!this.checked && ' 527 . '(!document.getElementById(\'checkbox_' . $plugin_name 528 . '_' . $propertyItem->getForce() . '\') ' 529 . '|| !document.getElementById(\'checkbox_' 530 . $plugin_name . '_' . $propertyItem->getForce() 531 . '\').checked)) ' 532 . 'return false; else return true;"'; 533 } 534 $ret .= '>'; 535 $ret .= '<label for="checkbox_' . $plugin_name . '_' 536 . $propertyItem->getName() . '">' 537 . self::getString($propertyItem->getText()) . '</label>'; 538 break; 539 case DocPropertyItem::class: 540 echo DocPropertyItem::class; 541 break; 542 case HiddenPropertyItem::class: 543 $ret .= '<li><input type="hidden" name="' . $plugin_name . '_' 544 . $propertyItem->getName() . '"' 545 . ' value="' . self::getDefault( 546 $section, 547 $plugin_name . '_' . $propertyItem->getName() 548 ) 549 . '"></li>'; 550 break; 551 case MessageOnlyPropertyItem::class: 552 $ret .= '<li>' . "\n"; 553 $ret .= '<p>' . self::getString($propertyItem->getText()) . '</p>'; 554 break; 555 case RadioPropertyItem::class: 556 /** 557 * @var RadioPropertyItem $pitem 558 */ 559 $pitem = $propertyItem; 560 561 $default = self::getDefault( 562 $section, 563 $plugin_name . '_' . $pitem->getName() 564 ); 565 566 foreach ($pitem->getValues() as $key => $val) { 567 $ret .= '<li><input type="radio" name="' . $plugin_name 568 . '_' . $pitem->getName() . '" value="' . $key 569 . '" id="radio_' . $plugin_name . '_' 570 . $pitem->getName() . '_' . $key . '"'; 571 if ($key == $default) { 572 $ret .= ' checked="checked"'; 573 } 574 $ret .= '><label for="radio_' . $plugin_name . '_' 575 . $pitem->getName() . '_' . $key . '">' 576 . self::getString($val) . '</label></li>'; 577 } 578 break; 579 case SelectPropertyItem::class: 580 /** 581 * @var SelectPropertyItem $pitem 582 */ 583 $pitem = $propertyItem; 584 $ret .= '<li>' . "\n"; 585 $ret .= '<label for="select_' . $plugin_name . '_' 586 . $pitem->getName() . '" class="desc">' 587 . self::getString($pitem->getText()) . '</label>'; 588 $ret .= '<select name="' . $plugin_name . '_' 589 . $pitem->getName() . '"' 590 . ' id="select_' . $plugin_name . '_' 591 . $pitem->getName() . '">'; 592 $default = self::getDefault( 593 $section, 594 $plugin_name . '_' . $pitem->getName() 595 ); 596 foreach ($pitem->getValues() as $key => $val) { 597 $ret .= '<option value="' . $key . '"'; 598 if ($key == $default) { 599 $ret .= ' selected="selected"'; 600 } 601 $ret .= '>' . self::getString($val) . '</option>'; 602 } 603 604 $ret .= '</select>'; 605 break; 606 case TextPropertyItem::class: 607 /** 608 * @var TextPropertyItem $pitem 609 */ 610 $pitem = $propertyItem; 611 $ret .= '<li>' . "\n"; 612 $ret .= '<label for="text_' . $plugin_name . '_' 613 . $pitem->getName() . '" class="desc">' 614 . self::getString($pitem->getText()) . '</label>'; 615 $ret .= '<input type="text" name="' . $plugin_name . '_' 616 . $pitem->getName() . '"' 617 . ' value="' . self::getDefault( 618 $section, 619 $plugin_name . '_' . $pitem->getName() 620 ) . '"' 621 . ' id="text_' . $plugin_name . '_' 622 . $pitem->getName() . '"' 623 . ($pitem->getSize() != null 624 ? ' size="' . $pitem->getSize() . '"' 625 : '') 626 . ($pitem->getLen() != null 627 ? ' maxlength="' . $pitem->getLen() . '"' 628 : '') 629 . '>'; 630 break; 631 case NumberPropertyItem::class: 632 $ret .= '<li>' . "\n"; 633 $ret .= '<label for="number_' . $plugin_name . '_' 634 . $propertyItem->getName() . '" class="desc">' 635 . self::getString($propertyItem->getText()) . '</label>'; 636 $ret .= '<input type="number" name="' . $plugin_name . '_' 637 . $propertyItem->getName() . '"' 638 . ' value="' . self::getDefault( 639 $section, 640 $plugin_name . '_' . $propertyItem->getName() 641 ) . '"' 642 . ' id="number_' . $plugin_name . '_' 643 . $propertyItem->getName() . '"' 644 . ' min="0"' 645 . '>'; 646 break; 647 default: 648 break; 649 } 650 651 return $ret; 652 } 653 654 /** 655 * Returns html div with editable options for plugin 656 * 657 * @param string $section name of config section in $GLOBALS['cfg'][$section] 658 * @param array $list array with plugin instances 659 * 660 * @return string html fieldset with plugin options 661 */ 662 public static function getOptions($section, array $list) 663 { 664 $ret = ''; 665 // Options for plugins that support them 666 foreach ($list as $plugin) { 667 $properties = $plugin->getProperties(); 668 $text = null; 669 $options = null; 670 if ($properties != null) { 671 $text = $properties->getText(); 672 $options = $properties->getOptions(); 673 } 674 675 $elem = explode('\\', get_class($plugin)); 676 $plugin_name = (string) array_pop($elem); 677 unset($elem); 678 $plugin_name = mb_strtolower( 679 mb_substr( 680 $plugin_name, 681 mb_strlen($section) 682 ) 683 ); 684 685 $ret .= '<div id="' . $plugin_name 686 . '_options" class="format_specific_options">'; 687 $ret .= '<h3>' . self::getString($text) . '</h3>'; 688 689 $no_options = true; 690 if ($options !== null && count($options) > 0) { 691 foreach ($options->getProperties() as $propertyMainGroup) { 692 // check for hidden properties 693 $no_options = true; 694 foreach ($propertyMainGroup->getProperties() as $propertyItem) { 695 if (strcmp(HiddenPropertyItem::class, get_class($propertyItem))) { 696 $no_options = false; 697 break; 698 } 699 } 700 701 $ret .= self::getOneOption( 702 $section, 703 $plugin_name, 704 $propertyMainGroup 705 ); 706 } 707 } 708 709 if ($no_options) { 710 $ret .= '<p>' . __('This format has no options') . '</p>'; 711 } 712 $ret .= '</div>'; 713 } 714 715 return $ret; 716 } 717 718 public static function getAuthPlugin(): AuthenticationPlugin 719 { 720 global $cfg; 721 722 $class = 'PhpMyAdmin\\Plugins\\Auth\\Authentication' . ucfirst(strtolower($cfg['Server']['auth_type'])); 723 724 if (! class_exists($class)) { 725 Core::fatalError( 726 __('Invalid authentication method set in configuration:') 727 . ' ' . $cfg['Server']['auth_type'] 728 ); 729 } 730 731 /** @var AuthenticationPlugin $plugin */ 732 $plugin = new $class(); 733 734 return $plugin; 735 } 736} 737