Translated using Weblate (Portuguese)
[phpmyadmin.git] / src / Plugins.php
blob15d8937d5729a072c97f67e2309c82306df059f4
1 <?php
3 declare(strict_types=1);
5 namespace PhpMyAdmin;
7 use FilesystemIterator;
8 use PhpMyAdmin\Container\ContainerBuilder;
9 use PhpMyAdmin\Html\MySQLDocumentation;
10 use PhpMyAdmin\Import\ImportSettings;
11 use PhpMyAdmin\Plugins\ExportPlugin;
12 use PhpMyAdmin\Plugins\ImportPlugin;
13 use PhpMyAdmin\Plugins\Plugin;
14 use PhpMyAdmin\Plugins\SchemaPlugin;
15 use PhpMyAdmin\Properties\Options\Groups\OptionsPropertySubgroup;
16 use PhpMyAdmin\Properties\Options\Items\BoolPropertyItem;
17 use PhpMyAdmin\Properties\Options\Items\DocPropertyItem;
18 use PhpMyAdmin\Properties\Options\Items\HiddenPropertyItem;
19 use PhpMyAdmin\Properties\Options\Items\MessageOnlyPropertyItem;
20 use PhpMyAdmin\Properties\Options\Items\NumberPropertyItem;
21 use PhpMyAdmin\Properties\Options\Items\RadioPropertyItem;
22 use PhpMyAdmin\Properties\Options\Items\SelectPropertyItem;
23 use PhpMyAdmin\Properties\Options\Items\TextPropertyItem;
24 use PhpMyAdmin\Properties\Options\OptionsPropertyGroup;
25 use PhpMyAdmin\Properties\Options\OptionsPropertyItem;
26 use SplFileInfo;
27 use Throwable;
29 use function __;
30 use function class_exists;
31 use function count;
32 use function htmlspecialchars;
33 use function is_array;
34 use function is_subclass_of;
35 use function mb_strtolower;
36 use function mb_strtoupper;
37 use function mb_substr;
38 use function method_exists;
39 use function preg_match_all;
40 use function sprintf;
41 use function str_contains;
42 use function str_replace;
43 use function str_starts_with;
44 use function strcasecmp;
45 use function usort;
47 class Plugins
49 /**
50 * Instantiates the specified plugin type for a certain format
52 * @param string $type the type of the plugin (import, export, etc)
53 * @param string $format the format of the plugin (sql, xml, et )
54 * @param array|string|null $param parameter to plugin by which they can decide whether they can work
55 * @psalm-param array{export_type: string, single_table: bool}|string|null $param
57 * @return object|null new plugin instance
59 public static function getPlugin(string $type, string $format, array|string|null $param = null): object|null
61 $GLOBALS['plugin_param'] = $param;
62 $pluginType = mb_strtoupper($type[0]) . mb_strtolower(mb_substr($type, 1));
63 $pluginFormat = mb_strtoupper($format[0]) . mb_strtolower(mb_substr($format, 1));
64 $class = sprintf('PhpMyAdmin\\Plugins\\%s\\%s%s', $pluginType, $pluginType, $pluginFormat);
65 if (! class_exists($class)) {
66 return null;
69 if ($type === 'export') {
70 $container = ContainerBuilder::getContainer();
72 /** @psalm-suppress MixedMethodCall */
73 return new $class(
74 $container->get('relation'),
75 $container->get('export'),
76 $container->get('transformations'),
80 return new $class();
83 /**
84 * @param string $type server|database|table|raw
85 * @psalm-param 'server'|'database'|'table'|'raw' $type
87 * @return ExportPlugin[]
88 * @psalm-return list<ExportPlugin>
90 public static function getExport(string $type, bool $singleTable): array
92 $GLOBALS['plugin_param'] = ['export_type' => $type, 'single_table' => $singleTable];
94 return self::getPlugins('Export');
97 /**
98 * @return ImportPlugin[]
99 * @psalm-return list<ImportPlugin>
101 public static function getImport(): array
103 return self::getPlugins('Import');
107 * @return SchemaPlugin[]
108 * @psalm-return list<SchemaPlugin>
110 public static function getSchema(): array
112 return self::getPlugins('Schema');
116 * Reads all plugin information
118 * @param string $type the type of the plugin (import, export, etc)
119 * @psalm-param 'Export'|'Import'|'Schema' $type
121 * @return Plugin[] list of plugin instances
122 * @psalm-return (
123 * $type is 'Export'
124 * ? list<ExportPlugin>
125 * : ($type is 'Import' ? list<ImportPlugin> : list<SchemaPlugin>)
128 private static function getPlugins(string $type): array
130 try {
131 $files = new FilesystemIterator(ROOT_PATH . 'src/Plugins/' . $type);
132 } catch (Throwable) {
133 return [];
136 $plugins = [];
138 /** @var SplFileInfo $fileInfo */
139 foreach ($files as $fileInfo) {
140 if (! $fileInfo->isReadable() || ! $fileInfo->isFile() || $fileInfo->getExtension() !== 'php') {
141 continue;
144 if (! str_starts_with($fileInfo->getFilename(), $type)) {
145 continue;
148 $class = sprintf('PhpMyAdmin\\Plugins\\%s\\%s', $type, $fileInfo->getBasename('.php'));
149 if (! class_exists($class) || ! is_subclass_of($class, Plugin::class) || ! $class::isAvailable()) {
150 continue;
153 if ($type === 'Export' && is_subclass_of($class, ExportPlugin::class)) {
154 $container = ContainerBuilder::getContainer();
155 $plugins[] = new $class(
156 $container->get('relation'),
157 $container->get('export'),
158 $container->get('transformations'),
160 } elseif ($type === 'Import' && is_subclass_of($class, ImportPlugin::class)) {
161 $plugins[] = new $class();
162 } elseif ($type === 'Schema' && is_subclass_of($class, SchemaPlugin::class)) {
163 $plugins[] = new $class();
167 usort($plugins, static fn (Plugin $plugin1, Plugin $plugin2): int => strcasecmp(
168 $plugin1->getProperties()->getText(),
169 $plugin2->getProperties()->getText(),
172 return $plugins;
176 * Returns locale string for $name or $name if no locale is found
178 * @param string|null $name for local string
180 * @return string locale string for $name
182 public static function getString(string|null $name): string
184 return $GLOBALS[$name] ?? $name ?? '';
188 * Returns html input tag option 'checked' if plugin $opt
189 * should be set by config or request
191 * @param string $section name of config section in
192 * \PhpMyAdmin\Config::getInstance()->settings[$section] for plugin
193 * @param string $opt name of option
194 * @psalm-param 'Export'|'Import'|'Schema' $section
196 * @return string html input tag option 'checked'
198 public static function checkboxCheck(string $section, string $opt): string
200 // If the form is being repopulated using $_GET data, that is priority
201 if (
202 isset($_GET[$opt])
203 || ! isset($_GET['repopulate'])
204 && ((ImportSettings::$timeoutPassed && isset($_REQUEST[$opt]))
205 || ! empty(Config::getInstance()->settings[$section][$opt]))
207 return ' checked="checked"';
210 return '';
214 * Returns default value for option $opt
216 * @param string $section name of config section in
217 * \PhpMyAdmin\Config::getInstance()->settings[$section] for plugin
218 * @param string $opt name of option
219 * @psalm-param 'Export'|'Import'|'Schema' $section
221 * @return string default value for option $opt
223 public static function getDefault(string $section, string $opt): string
225 if (isset($_GET[$opt])) {
226 // If the form is being repopulated using $_GET data, that is priority
227 return htmlspecialchars($_GET[$opt]);
230 if (isset($_REQUEST[$opt]) && ImportSettings::$timeoutPassed) {
231 return htmlspecialchars($_REQUEST[$opt]);
234 $config = Config::getInstance();
235 if (! isset($config->settings[$section][$opt])) {
236 return '';
239 $matches = [];
240 /* Possibly replace localised texts */
241 if (! preg_match_all('/(str[A-Z][A-Za-z0-9]*)/', (string) $config->settings[$section][$opt], $matches)) {
242 return htmlspecialchars((string) $config->settings[$section][$opt]);
245 $val = $config->settings[$section][$opt];
246 foreach ($matches[0] as $match) {
247 if (! isset($GLOBALS[$match])) {
248 continue;
251 $val = str_replace($match, $GLOBALS[$match], $val);
254 return htmlspecialchars($val);
258 * @param ExportPlugin[]|ImportPlugin[]|SchemaPlugin[] $list
260 * @return array<int, array<string, bool|string>>
261 * @psalm-return list<array{name: non-empty-lowercase-string, text: string, is_selected: bool, is_binary: bool}>
263 public static function getChoice(array $list, string $default): array
265 $return = [];
266 foreach ($list as $plugin) {
267 $pluginName = $plugin->getName();
268 $properties = $plugin->getProperties();
269 $return[] = [
270 'name' => $pluginName,
271 'text' => self::getString($properties->getText()),
272 'is_selected' => $pluginName === $default,
273 'is_binary' => $properties->getForceFile(),
277 return $return;
281 * Returns single option in a list element
283 * @param string $section name of config section in $cfg[$section] for plugin
284 * @param string $pluginName unique plugin name
285 * @param OptionsPropertyItem $propertyGroup options property main group instance
286 * @param bool $isSubgroup if this group is a subgroup
287 * @psalm-param 'Export'|'Import'|'Schema' $section
289 * @return string table row with option
291 private static function getOneOption(
292 string $section,
293 string $pluginName,
294 OptionsPropertyItem $propertyGroup,
295 bool $isSubgroup = false,
296 ): string {
297 $ret = "\n";
299 $properties = null;
300 if (! $isSubgroup) {
301 // for subgroup headers
302 if (str_contains($propertyGroup::class, 'PropertyItem')) {
303 $properties = [$propertyGroup];
304 } else {
305 // for main groups
306 $ret .= '<div id="' . $pluginName . '_' . $propertyGroup->getName() . '">';
308 $text = null;
309 if (method_exists($propertyGroup, 'getText')) {
310 $text = $propertyGroup->getText();
313 if ($text != null) {
314 $ret .= '<h5 class="card-title mt-4 mb-2">' . self::getString($text) . '</h5>';
317 $ret .= '<ul class="list-group">';
321 $notSubgroupHeader = false;
322 if ($properties === null) {
323 $notSubgroupHeader = true;
324 if ($propertyGroup instanceof OptionsPropertyGroup) {
325 $properties = $propertyGroup->getProperties();
329 $propertyClass = null;
330 if ($properties !== null) {
331 /** @var OptionsPropertySubgroup $propertyItem */
332 foreach ($properties as $propertyItem) {
333 $propertyClass = $propertyItem::class;
334 // if the property is a subgroup, we deal with it recursively
335 if (str_contains($propertyClass, 'Subgroup')) {
336 // for subgroups
337 // each subgroup can have a header, which may also be a form element
338 /** @var OptionsPropertyItem|null $subgroupHeader */
339 $subgroupHeader = $propertyItem->getSubgroupHeader();
340 if ($subgroupHeader !== null) {
341 $ret .= self::getOneOption($section, $pluginName, $subgroupHeader);
344 $ret .= '<li class="list-group-item"><ul class="list-group"';
345 if ($subgroupHeader !== null) {
346 $ret .= ' id="ul_' . $subgroupHeader->getName() . '">';
347 } else {
348 $ret .= '>';
351 $ret .= self::getOneOption($section, $pluginName, $propertyItem, true);
352 continue;
355 // single property item
356 $ret .= self::getHtmlForProperty($section, $pluginName, $propertyItem);
360 if ($isSubgroup) {
361 // end subgroup
362 $ret .= '</ul></li>';
363 } elseif ($notSubgroupHeader) {
364 // end main group
365 $ret .= '</ul></div>';
368 if (method_exists($propertyGroup, 'getDoc')) {
369 $doc = $propertyGroup->getDoc();
370 if (is_array($doc)) {
371 if (count($doc) === 3) {
372 $ret .= MySQLDocumentation::show($doc[1], false, null, null, $doc[2]);
373 } elseif (count($doc) === 1) {
374 $ret .= MySQLDocumentation::showDocumentation('faq', $doc[0]);
375 } else {
376 $ret .= MySQLDocumentation::show($doc[1]);
381 // Close the list element after $doc link is displayed
382 if (
383 $propertyClass === BoolPropertyItem::class
384 || $propertyClass === MessageOnlyPropertyItem::class
385 || $propertyClass === SelectPropertyItem::class
386 || $propertyClass === TextPropertyItem::class
388 $ret .= '</li>';
391 return $ret . "\n";
395 * Get HTML for properties items
397 * @param string $section name of config section in $cfg[$section] for plugin
398 * @param string $pluginName unique plugin name
399 * @param OptionsPropertyItem $propertyItem Property item
400 * @psalm-param 'Export'|'Import'|'Schema' $section
402 public static function getHtmlForProperty(
403 string $section,
404 string $pluginName,
405 OptionsPropertyItem $propertyItem,
406 ): string {
407 $ret = '';
408 $propertyClass = $propertyItem::class;
409 switch ($propertyClass) {
410 case BoolPropertyItem::class:
411 $ret .= '<li class="list-group-item">' . "\n";
412 $ret .= '<div class="form-check form-switch">' . "\n";
413 $ret .= '<input class="form-check-input" type="checkbox" role="switch" name="' . $pluginName . '_'
414 . $propertyItem->getName() . '"'
415 . ' value="something" id="checkbox_' . $pluginName . '_'
416 . $propertyItem->getName() . '"'
417 . ' '
418 . self::checkboxCheck(
419 $section,
420 $pluginName . '_' . $propertyItem->getName(),
423 if ($propertyItem->getForce() != null) {
424 // Same code is also few lines lower, update both if needed
425 $ret .= ' onclick="if (!this.checked &amp;&amp; '
426 . '(!document.getElementById(\'checkbox_' . $pluginName
427 . '_' . $propertyItem->getForce() . '\') '
428 . '|| !document.getElementById(\'checkbox_'
429 . $pluginName . '_' . $propertyItem->getForce()
430 . '\').checked)) '
431 . 'return false; else return true;"';
434 $ret .= '>';
435 $ret .= '<label class="form-check-label" for="checkbox_' . $pluginName . '_'
436 . $propertyItem->getName() . '">'
437 . self::getString($propertyItem->getText()) . '</label></div>';
438 break;
439 case DocPropertyItem::class:
440 echo DocPropertyItem::class;
441 break;
442 case HiddenPropertyItem::class:
443 $ret .= '<li class="list-group-item"><input type="hidden" name="' . $pluginName . '_'
444 . $propertyItem->getName() . '"'
445 . ' value="' . self::getDefault(
446 $section,
447 $pluginName . '_' . $propertyItem->getName(),
449 . '"></li>';
450 break;
451 case MessageOnlyPropertyItem::class:
452 $ret .= '<li class="list-group-item">' . "\n";
453 $ret .= self::getString($propertyItem->getText());
454 break;
455 case RadioPropertyItem::class:
456 /** @var RadioPropertyItem $pitem */
457 $pitem = $propertyItem;
459 $default = self::getDefault(
460 $section,
461 $pluginName . '_' . $pitem->getName(),
464 $ret .= '<li class="list-group-item">';
466 foreach ($pitem->getValues() as $key => $val) {
467 $ret .= '<div class="form-check"><input type="radio" name="' . $pluginName
468 . '_' . $pitem->getName() . '" class="form-check-input" value="' . $key
469 . '" id="radio_' . $pluginName . '_'
470 . $pitem->getName() . '_' . $key . '"';
471 if ($key == $default) {
472 $ret .= ' checked';
475 $ret .= '><label class="form-check-label" for="radio_' . $pluginName . '_'
476 . $pitem->getName() . '_' . $key . '">'
477 . self::getString($val) . '</label></div>';
480 $ret .= '</li>';
482 break;
483 case SelectPropertyItem::class:
484 /** @var SelectPropertyItem $pitem */
485 $pitem = $propertyItem;
486 $ret .= '<li class="list-group-item">' . "\n";
487 $ret .= '<label for="select_' . $pluginName . '_'
488 . $pitem->getName() . '" class="form-label">'
489 . self::getString($pitem->getText()) . '</label>';
490 $ret .= '<select class="form-select" name="' . $pluginName . '_'
491 . $pitem->getName() . '"'
492 . ' id="select_' . $pluginName . '_'
493 . $pitem->getName() . '">';
494 $default = self::getDefault(
495 $section,
496 $pluginName . '_' . $pitem->getName(),
498 foreach ($pitem->getValues() as $key => $val) {
499 $ret .= '<option value="' . $key . '"';
500 if ($key == $default) {
501 $ret .= ' selected';
504 $ret .= '>' . self::getString($val) . '</option>';
507 $ret .= '</select>';
508 break;
509 case TextPropertyItem::class:
510 /** @var TextPropertyItem $pitem */
511 $pitem = $propertyItem;
512 $ret .= '<li class="list-group-item">' . "\n";
513 $ret .= '<label for="text_' . $pluginName . '_'
514 . $pitem->getName() . '" class="form-label">'
515 . self::getString($pitem->getText()) . '</label>';
516 $ret .= '<input class="form-control" type="text" name="' . $pluginName . '_'
517 . $pitem->getName() . '"'
518 . ' value="' . self::getDefault(
519 $section,
520 $pluginName . '_' . $pitem->getName(),
521 ) . '"'
522 . ' id="text_' . $pluginName . '_'
523 . $pitem->getName() . '"'
524 . ($pitem->getSize() !== 0
525 ? ' size="' . $pitem->getSize() . '"'
526 : '')
527 . ($pitem->getLen() !== 0
528 ? ' maxlength="' . $pitem->getLen() . '"'
529 : '')
530 . '>';
531 break;
532 case NumberPropertyItem::class:
533 $ret .= '<li class="list-group-item">' . "\n";
534 $ret .= '<label for="number_' . $pluginName . '_'
535 . $propertyItem->getName() . '" class="form-label">'
536 . self::getString($propertyItem->getText()) . '</label>';
537 $ret .= '<input class="form-control" type="number" name="' . $pluginName . '_'
538 . $propertyItem->getName() . '"'
539 . ' value="' . self::getDefault(
540 $section,
541 $pluginName . '_' . $propertyItem->getName(),
542 ) . '"'
543 . ' id="number_' . $pluginName . '_'
544 . $propertyItem->getName() . '"'
545 . ' min="0"'
546 . '>';
547 break;
548 default:
549 break;
552 return $ret;
556 * Returns html div with editable options for plugin
558 * @param string $section name of config section in $cfg[$section]
559 * @param ExportPlugin[]|ImportPlugin[]|SchemaPlugin[] $list array with plugin instances
560 * @psalm-param 'Export'|'Import'|'Schema' $section
562 * @return string html fieldset with plugin options
564 public static function getOptions(string $section, array $list): string
566 $ret = '';
567 // Options for plugins that support them
568 foreach ($list as $plugin) {
569 $properties = $plugin->getProperties();
570 $text = $properties->getText();
571 $options = $properties->getOptions();
573 $pluginName = $plugin->getName();
575 $ret .= '<div id="' . $pluginName
576 . '_options" class="format_specific_options">';
577 $ret .= '<h3>' . self::getString($text) . '</h3>';
579 $noOptions = true;
580 if ($options !== null && count($options) > 0) {
581 foreach ($options->getProperties() as $propertyMainGroup) {
582 // check for hidden properties
583 $noOptions = true;
584 foreach ($propertyMainGroup->getProperties() as $propertyItem) {
585 if (! $propertyItem instanceof HiddenPropertyItem) {
586 $noOptions = false;
587 break;
591 $ret .= self::getOneOption($section, $pluginName, $propertyMainGroup);
595 if ($noOptions) {
596 $ret .= '<p class="card-text">' . __('This format has no options') . '</p>';
599 $ret .= '</div>';
602 return $ret;