diff --git a/composer.json b/composer.json index 7828d719c6..6bf7f11a93 100644 --- a/composer.json +++ b/composer.json @@ -58,6 +58,7 @@ "symfony/expression-language": "^5.2.3", "symfony/polyfill-ctype": "^1.17.0", "symfony/polyfill-mbstring": "^1.17.0", + "symfony/polyfill-php80": "^1.16", "twig/twig": "^3.0.1", "webmozart/assert": "^1.10", "williamdes/mariadb-mysql-kbs": "^1.2" diff --git a/libraries/classes/Plugins.php b/libraries/classes/Plugins.php index c7fa0ef304..c0b0c7a53f 100644 --- a/libraries/classes/Plugins.php +++ b/libraries/classes/Plugins.php @@ -4,10 +4,12 @@ declare(strict_types=1); namespace PhpMyAdmin; +use FilesystemIterator; use PhpMyAdmin\Html\MySQLDocumentation; use PhpMyAdmin\Plugins\AuthenticationPlugin; use PhpMyAdmin\Plugins\ExportPlugin; use PhpMyAdmin\Plugins\ImportPlugin; +use PhpMyAdmin\Plugins\Plugin; use PhpMyAdmin\Plugins\SchemaPlugin; use PhpMyAdmin\Properties\Options\Groups\OptionsPropertySubgroup; use PhpMyAdmin\Properties\Options\Items\BoolPropertyItem; @@ -22,6 +24,8 @@ use PhpMyAdmin\Properties\Options\OptionsPropertyItem; use PhpMyAdmin\Properties\Plugins\ExportPluginProperties; use PhpMyAdmin\Properties\Plugins\PluginPropertyItem; use PhpMyAdmin\Properties\Plugins\SchemaPluginProperties; +use SplFileInfo; +use Throwable; use function __; use function array_pop; @@ -30,19 +34,16 @@ use function count; use function explode; use function get_class; use function htmlspecialchars; -use function is_file; use function mb_strlen; use function mb_strpos; use function mb_strtolower; use function mb_strtoupper; use function mb_substr; use function method_exists; -use function opendir; -use function preg_match; use function preg_match_all; -use function readdir; use function sprintf; use function str_replace; +use function str_starts_with; use function strcasecmp; use function strcmp; use function strtolower; @@ -83,10 +84,11 @@ class Plugins */ public static function getExport(string $type, bool $singleTable): array { - return self::getPlugins('export', 'libraries/classes/Plugins/Export/', [ - 'export_type' => $type, - 'single_table' => $singleTable, - ]); + global $plugin_param; + + $plugin_param = ['export_type' => $type, 'single_table' => $singleTable]; + + return self::getPlugins('Export'); } /** @@ -96,7 +98,11 @@ class Plugins */ public static function getImport(string $type): array { - return self::getPlugins('import', 'libraries/classes/Plugins/Import/', $type); + global $plugin_param; + + $plugin_param = $type; + + return self::getPlugins('Import'); } /** @@ -104,89 +110,54 @@ class Plugins */ public static function getSchema(): array { - return self::getPlugins('schema', 'libraries/classes/Plugins/Schema/', null); + return self::getPlugins('Schema'); } /** - * Reads all plugin information from directory $plugins_dir + * Reads all plugin information * - * @param string $plugin_type the type of the plugin (import, export, etc) - * @param string $plugins_dir directory with plugins - * @param array|string|null $plugin_param parameter to plugin by which they can - * decide whether they can work + * @param string $type the type of the plugin (import, export, etc) * * @return array list of plugin instances */ - private static function getPlugins(string $plugin_type, string $plugins_dir, $plugin_param): array + private static function getPlugins(string $type): array { - global $skip_import; - - $GLOBALS['plugin_param'] = $plugin_param; - - $fullFsPathPluginDir = ROOT_PATH . $plugins_dir; - - $handle = @opendir($fullFsPathPluginDir); - if (! $handle) { + try { + $files = new FilesystemIterator(ROOT_PATH . 'libraries/classes/Plugins/' . $type); + } catch (Throwable $e) { return []; } - $plugin_list = []; + $plugins = []; - $namespace = 'PhpMyAdmin\\' . str_replace('/', '\\', mb_substr($plugins_dir, 18)); - $class_type = mb_strtoupper($plugin_type[0], 'UTF-8') - . mb_strtolower(mb_substr($plugin_type, 1), 'UTF-8'); - - $prefix_class_name = $namespace . $class_type; - - while ($file = @readdir($handle)) { - // In some situations, Mac OS creates a new file for each file - // (for example ._csv.php) so the following regexp - // matches a file which does not start with a dot but ends - // with ".php" - if ( - ! is_file($fullFsPathPluginDir . $file) - || ! preg_match( - '@^' . $class_type . '([^\.]+)\.php$@i', - $file, - $matches - ) - ) { + /** @var SplFileInfo $fileInfo */ + foreach ($files as $fileInfo) { + if (! $fileInfo->isReadable() || ! $fileInfo->isFile() || $fileInfo->getExtension() !== 'php') { continue; } - /** @var bool $skip_import */ - $skip_import = false; - - include_once $fullFsPathPluginDir . $file; - - if ($skip_import) { + if (! str_starts_with($fileInfo->getFilename(), $type)) { continue; } - $class_name = $prefix_class_name . $matches[1]; - $plugin = new $class_name(); - if ($plugin->getProperties() === null) { + $class = sprintf('PhpMyAdmin\\Plugins\\%s\\%s', $type, $fileInfo->getBasename('.php')); + if (! class_exists($class)) { continue; } - $plugin_list[] = $plugin; + $plugin = new $class(); + if (! ($plugin instanceof Plugin) || ! $plugin->isAvailable()) { + continue; + } + + $plugins[] = $plugin; } - usort( - $plugin_list, - /** - * @param mixed $cmp_name_1 - * @param mixed $cmp_name_2 - */ - static function ($cmp_name_1, $cmp_name_2) { - return strcasecmp( - $cmp_name_1->getProperties()->getText(), - $cmp_name_2->getProperties()->getText() - ); - } - ); + usort($plugins, static function (Plugin $plugin1, Plugin $plugin2): int { + return strcasecmp($plugin1->getProperties()->getText(), $plugin2->getProperties()->getText()); + }); - return $plugin_list; + return $plugins; } /** diff --git a/libraries/classes/Plugins/Export/ExportPdf.php b/libraries/classes/Plugins/Export/ExportPdf.php index e170f4573a..b9303cae5d 100644 --- a/libraries/classes/Plugins/Export/ExportPdf.php +++ b/libraries/classes/Plugins/Export/ExportPdf.php @@ -1,7 +1,4 @@ pdf = $pdf; } + + public function isAvailable(): bool + { + return class_exists(TCPDF::class); + } } diff --git a/libraries/classes/Plugins/Export/ExportXml.php b/libraries/classes/Plugins/Export/ExportXml.php index 8cf9fe292f..692203947e 100644 --- a/libraries/classes/Plugins/Export/ExportXml.php +++ b/libraries/classes/Plugins/Export/ExportXml.php @@ -1,7 +1,4 @@ tables = $tables; } + + public function isAvailable(): bool + { + global $db; + + // Can't do server export. + return isset($db) && strlen($db) > 0; + } } diff --git a/libraries/classes/Plugins/ExportPlugin.php b/libraries/classes/Plugins/ExportPlugin.php index a5a68cd05c..65f3cd82b4 100644 --- a/libraries/classes/Plugins/ExportPlugin.php +++ b/libraries/classes/Plugins/ExportPlugin.php @@ -9,6 +9,7 @@ namespace PhpMyAdmin\Plugins; use PhpMyAdmin\Export; use PhpMyAdmin\Properties\Plugins\ExportPluginProperties; +use PhpMyAdmin\Properties\Plugins\PluginPropertyItem; use PhpMyAdmin\Relation; use PhpMyAdmin\Transformations; @@ -20,7 +21,7 @@ use function stripos; * methods, but those are not declared here, because they are not implemented * by all export plugins. */ -abstract class ExportPlugin +abstract class ExportPlugin implements Plugin { /** * PhpMyAdmin\Properties\Plugins\ExportPluginProperties object containing @@ -259,7 +260,7 @@ abstract class ExportPlugin * * @return ExportPluginProperties */ - public function getProperties() + public function getProperties(): PluginPropertyItem { return $this->properties; } @@ -411,4 +412,9 @@ abstract class ExportPlugin return $relation; } + + public function isAvailable(): bool + { + return true; + } } diff --git a/libraries/classes/Plugins/Import/ImportLdi.php b/libraries/classes/Plugins/Import/ImportLdi.php index f4b4db1078..c891c823a4 100644 --- a/libraries/classes/Plugins/Import/ImportLdi.php +++ b/libraries/classes/Plugins/Import/ImportLdi.php @@ -1,7 +1,4 @@ isAvailable()) { + return; + } + $this->setProperties(); } @@ -198,4 +189,12 @@ class ImportLdi extends AbstractImportCsv $this->import->runQuery('', '', $sql_data); $finished = true; } + + public function isAvailable(): bool + { + global $plugin_param; + + // We need relations enabled and we work only on database. + return isset($plugin_param) && $plugin_param === 'table'; + } } diff --git a/libraries/classes/Plugins/ImportPlugin.php b/libraries/classes/Plugins/ImportPlugin.php index 2adc5b32a6..e69cee716c 100644 --- a/libraries/classes/Plugins/ImportPlugin.php +++ b/libraries/classes/Plugins/ImportPlugin.php @@ -10,6 +10,7 @@ namespace PhpMyAdmin\Plugins; use PhpMyAdmin\File; use PhpMyAdmin\Import; use PhpMyAdmin\Properties\Plugins\ImportPluginProperties; +use PhpMyAdmin\Properties\Plugins\PluginPropertyItem; use function strlen; @@ -17,7 +18,7 @@ use function strlen; * Provides a common interface that will have to be implemented by all of the * import plugins. */ -abstract class ImportPlugin +abstract class ImportPlugin implements Plugin { /** * ImportPluginProperties object containing the import plugin properties @@ -50,7 +51,7 @@ abstract class ImportPlugin * * @return ImportPluginProperties */ - public function getProperties() + public function getProperties(): PluginPropertyItem { return $this->properties; } @@ -86,4 +87,9 @@ abstract class ImportPlugin $options, ]; } + + public function isAvailable(): bool + { + return true; + } } diff --git a/libraries/classes/Plugins/Plugin.php b/libraries/classes/Plugins/Plugin.php new file mode 100644 index 0000000000..1837595950 --- /dev/null +++ b/libraries/classes/Plugins/Plugin.php @@ -0,0 +1,14 @@ +properties; } @@ -85,4 +86,9 @@ abstract class SchemaPlugin return $ret; } + + public function isAvailable(): bool + { + return true; + } } diff --git a/test/classes/PluginsTest.php b/test/classes/PluginsTest.php new file mode 100644 index 0000000000..b142296a28 --- /dev/null +++ b/test/classes/PluginsTest.php @@ -0,0 +1,49 @@ +assertEquals(['export_type' => 'database', 'single_table' => false], $plugin_param); + $this->assertIsArray($plugins); + $this->assertCount(14, $plugins); + $this->assertContainsOnlyInstancesOf(Plugins\ExportPlugin::class, $plugins); + } + + public function testGetImport(): void + { + global $plugin_param; + + $plugins = Plugins::getImport('database'); + $this->assertEquals('database', $plugin_param); + $this->assertIsArray($plugins); + $this->assertCount(6, $plugins); + $this->assertContainsOnlyInstancesOf(Plugins\ImportPlugin::class, $plugins); + } + + public function testGetSchema(): void + { + $plugins = Plugins::getSchema(); + $this->assertIsArray($plugins); + $this->assertCount(4, $plugins); + $this->assertContainsOnlyInstancesOf(Plugins\SchemaPlugin::class, $plugins); + } +}