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 &amp;&amp; '
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