diff --git a/core/Application/Kernel/PluginList.php b/core/Application/Kernel/PluginList.php index 5d263a6ea3ac21507e6ebb09a9a236511169bfd9..70abc543d53863b41bf4ba24fbbd26cd6eaaa5c5 100644 --- a/core/Application/Kernel/PluginList.php +++ b/core/Application/Kernel/PluginList.php @@ -8,6 +8,8 @@ namespace Piwik\Application\Kernel; +use Piwik\Plugin\MetadataLoader; + /** * Lists the currently activated plugins. Used when setting up Piwik's environment before * initializing the DI container. @@ -88,7 +90,8 @@ class PluginList } /** - * Sorts an array of plugins in the order they should be loaded. + * Sorts an array of plugins in the order they should be loaded. We cannot use DI here as DI is not initialized + * at this stage. * * @params string[] $plugins * @return \string[] @@ -111,10 +114,78 @@ class PluginList $otherPluginsToLoadAfterDefaultPlugins = array_diff($plugins, $defaultPluginsLoadedFirst); // sort by name to have a predictable order for those extra plugins - sort($otherPluginsToLoadAfterDefaultPlugins); + natcasesort($otherPluginsToLoadAfterDefaultPlugins); $sorted = array_merge($defaultPluginsLoadedFirst, $otherPluginsToLoadAfterDefaultPlugins); return $sorted; } + + /** + * Sorts an array of plugins in the order they should be saved in config.ini.php. This basically influences + * the order of the plugin config.php and which config will be loaded first. We want to make sure to require the + * config or a required plugin first before loading the plugin that requires it. + * + * We do not sort using this logic on each request since it is much slower than `sortPlugins()`. The order + * of plugins in config.ini.php is only important for the ContainerFactory. During a regular request it is otherwise + * fine to load the plugins in the order of `sortPlugins()` since we will make sure that required plugins will be + * loaded first in plugin manager. + * + * @param string[] $plugins + * @param array[] $pluginJsonCache For internal testing only + * @return \string[] + */ + public function sortPluginsAndRespectDependencies(array $plugins, $pluginJsonCache = array()) + { + $global = $this->getPluginsBundledWithPiwik(); + + if (empty($global)) { + return $plugins; + } + + // we need to make sure a possibly disabled plugin will be still loaded before any 3rd party plugin + $global = array_merge($global, $this->corePluginsDisabledByDefault); + + $global = array_values($global); + $plugins = array_values($plugins); + + $defaultPluginsLoadedFirst = array_intersect($global, $plugins); + + $otherPluginsToLoadAfterDefaultPlugins = array_diff($plugins, $defaultPluginsLoadedFirst); + + // we still want to sort alphabetically by default + natcasesort($otherPluginsToLoadAfterDefaultPlugins); + + $sorted = array(); + foreach ($otherPluginsToLoadAfterDefaultPlugins as $pluginName) { + $sorted = $this->sortRequiredPlugin($pluginName, $pluginJsonCache, $otherPluginsToLoadAfterDefaultPlugins, $sorted); + } + + $sorted = array_merge($defaultPluginsLoadedFirst, $sorted); + + return $sorted; + } + + private function sortRequiredPlugin($pluginName, &$pluginJsonCache, $toBeSorted, $sorted) + { + if (!isset($pluginJsonCache[$pluginName])) { + $loader = new MetadataLoader($pluginName); + $pluginJsonCache[$pluginName] = $loader->loadPluginInfoJson(); + } + + if (!empty($pluginJsonCache[$pluginName]['require'])) { + $dependencies = $pluginJsonCache[$pluginName]['require']; + foreach ($dependencies as $possiblePluginName => $key) { + if (in_array($possiblePluginName, $toBeSorted, true) && !in_array($possiblePluginName, $sorted, true)) { + $sorted = $this->sortRequiredPlugin($possiblePluginName, $pluginJsonCache, $toBeSorted, $sorted); + } + } + } + + if (!in_array($pluginName, $sorted, true)) { + $sorted[] = $pluginName; + } + + return $sorted; + } } diff --git a/core/Plugin/Manager.php b/core/Plugin/Manager.php index 9466304e27b6ae8f123e490e50cf83e94a9b6d18..ee4f658b880689bb6c2ee14a1ab688273ab9dde3 100644 --- a/core/Plugin/Manager.php +++ b/core/Plugin/Manager.php @@ -203,7 +203,7 @@ class Manager */ private function updatePluginsConfig($pluginsToLoad) { - $pluginsToLoad = $this->pluginList->sortPlugins($pluginsToLoad); + $pluginsToLoad = $this->pluginList->sortPluginsAndRespectDependencies($pluginsToLoad); $section = PiwikConfig::getInstance()->Plugins; $section['Plugins'] = $pluginsToLoad; PiwikConfig::getInstance()->Plugins = $section; diff --git a/core/Plugin/MetadataLoader.php b/core/Plugin/MetadataLoader.php index bde849ff7eeb2387cc77596e56741d11044fd433..b3d552cf80504bab18363557be4801587cfecc46 100644 --- a/core/Plugin/MetadataLoader.php +++ b/core/Plugin/MetadataLoader.php @@ -11,6 +11,7 @@ namespace Piwik\Plugin; use Exception; use Piwik\Piwik; use Piwik\Version; +use Piwik\Plugin; /** * @see core/Version.php @@ -90,12 +91,22 @@ class MetadataLoader ); } - private function loadPluginInfoJson() + /** + * It is important that this method works without using anything from DI + * @return array|mixed + */ + public function loadPluginInfoJson() { - $path = $this->getPathToPluginFolder() . '/' . self::PLUGIN_JSON_FILENAME; + $path = $this->getPathToPluginJson(); return $this->loadJsonMetadata($path); } + public function getPathToPluginJson() + { + $path = $this->getPathToPluginFolder() . '/' . self::PLUGIN_JSON_FILENAME; + return $path; + } + private function loadJsonMetadata($path) { if (!file_exists($path)) { diff --git a/tests/PHPUnit/Framework/TestingEnvironmentManipulator.php b/tests/PHPUnit/Framework/TestingEnvironmentManipulator.php index 1aa1a74de89b2b9a8f9965f43a8e8a6d1d9a9aab..59bac601554df09ef0670bfc3cc24f1e2e7eeb3e 100644 --- a/tests/PHPUnit/Framework/TestingEnvironmentManipulator.php +++ b/tests/PHPUnit/Framework/TestingEnvironmentManipulator.php @@ -227,16 +227,15 @@ class TestingEnvironmentManipulator implements EnvironmentManipulator private function getPluginAndRequiredPlugins($pluginName, $plugins) { - $pluginJsonPath = $this->makePathToPluginJson($pluginName); + $pluginLoader = new Plugin\MetadataLoader($pluginName); + $pluginJson = $pluginLoader->loadPluginInfoJson(); - if (file_exists($pluginJsonPath)) { - $pluginJson = json_decode(trim(file_get_contents($pluginJsonPath)), true); + if (!empty($pluginJson['require'])) { + foreach ($pluginJson['require'] as $possiblePluginName => $requiredVersion) { - if (!empty($pluginJson['require'])) { - foreach ($pluginJson['require'] as $possiblePluginName => $requiredVersion) { - if (file_exists($this->makePathToPluginJson($possiblePluginName))) { - $plugins = $this->getPluginAndRequiredPlugins($possiblePluginName, $plugins); - } + $pluginLoader2 = new Plugin\MetadataLoader($possiblePluginName); + if (file_exists($pluginLoader2->getPathToPluginJson())) { + $plugins = $this->getPluginAndRequiredPlugins($possiblePluginName, $plugins); } } } @@ -248,11 +247,6 @@ class TestingEnvironmentManipulator implements EnvironmentManipulator return $plugins; } - private function makePathToPluginJson($pluginName) - { - return Plugin\Manager::getPluginsDirectory() . $pluginName . '/' . Plugin\MetadataLoader::PLUGIN_JSON_FILENAME; - } - private function classExists($klass) { if (class_exists($klass)) { diff --git a/tests/PHPUnit/Integration/Application/Kernel/PluginListTest.php b/tests/PHPUnit/Integration/Application/Kernel/PluginListTest.php new file mode 100644 index 0000000000000000000000000000000000000000..00f873b2eeeaa3e6d1b0c18782923967054c0f8e --- /dev/null +++ b/tests/PHPUnit/Integration/Application/Kernel/PluginListTest.php @@ -0,0 +1,113 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ +namespace Piwik\Tests\Integration\Application\Kernel; + +use Piwik\Application\Kernel\PluginList; +use Piwik\Container\StaticContainer; +use Piwik\Tests\Framework\TestCase\IntegrationTestCase; + +/** + * @group PluginListTest + * @group Core + */ +class PluginListTest extends IntegrationTestCase +{ + + /** + * @var PluginList + */ + private $pluginList = array(); + + public function setUp() + { + parent::setUp(); + $this->pluginList = $this->makePluginList(); + } + + public function test_sortPlugins() + { + $pluginList = $this->makePluginList(); + $sorted = $pluginList->sortPlugins(array('UsersManager', 'CoreHome', 'MyCustomPlugin', 'ExampleCommand', 'MyCustomPlugin2', 'Abcdef')); + $this->assertSame(array( + 'CoreHome', // core plugins loaded first + 'UsersManager', + 'ExampleCommand', // a "by default disabled plugin" is loaded before custom plugins + 'Abcdef', // then we load custom plugins + 'MyCustomPlugin', + 'MyCustomPlugin2', + ), $sorted); + } + + public function test_sortPlugins_onlyCorePlugins() + { + $pluginList = $this->makePluginList(); + $sorted = $pluginList->sortPlugins(array('UsersManager', 'CoreHome')); + $this->assertSame(array('CoreHome','UsersManager'), $sorted); + } + + public function test_sortPluginsAndRespectDependencies_sortsPluginsAlphabetically() + { + $pluginList = $this->makePluginList(); + $sorted = $pluginList->sortPluginsAndRespectDependencies(array( + 'UsersManager', 'MyCustomPlugin', 'ExampleCommand', 'MyCustomPlugin2', 'CoreHome', 'Abcdef' + )); + $this->assertSame(array( + 'CoreHome', // core plugins loaded first + 'UsersManager', + 'ExampleCommand', // a "by default disabled plugin" is loaded before custom plugins + 'Abcdef', // then we load custom plugins + 'MyCustomPlugin', + 'MyCustomPlugin2', + ), $sorted); + } + + public function test_sortPluginsAndRespectDependencies_makesSureToListRequiredDependencyFirst() + { + $pluginJsonInfo = array( + 'Abcdef' => array('require' => array('MyCustomPlugin2' => '2.2.1')), + 'MyCustomPlugin2' => array('require' => array('CoreHome' => '4.2.1', 'MyCustomPlugin3' => '3.0.3')), + 'fooBar' => array('require' => array('Ast' => '1.2.1', 'MyCustomPlugin3' => '3.0.3')) + ); + + $pluginList = $this->makePluginList(); + $sorted = $pluginList->sortPluginsAndRespectDependencies(array( + 'UsersManager', 'MyCustomPlugin', + 'ExampleCommand', 'MyCustomPlugin2', 'Ast', + 'Acc', 'MyCustomPlugin3', 'CoreHome', 'Abcdef', 'fooBar', + ), $pluginJsonInfo); + $this->assertSame(array( + 'CoreHome', // core plugins loaded first + 'UsersManager', + 'ExampleCommand', // a "by default disabled plugin" is loaded before custom plugins + 'MyCustomPlugin3', + 'MyCustomPlugin2', + 'Abcdef', + 'Acc', + 'Ast', + 'fooBar', + 'MyCustomPlugin', + ), $sorted); + } + + public function test_sortPluginsAndRespectDependencies_onlyCorePlugins() + { + $pluginList = $this->makePluginList(); + $sorted = $pluginList->sortPluginsAndRespectDependencies(array('UsersManager', 'CoreHome')); + $this->assertSame(array('CoreHome','UsersManager'), $sorted); + } + + private function makePluginList() + { + $globalSettingsProvider = StaticContainer::get('Piwik\Application\Kernel\GlobalSettingsProvider'); + $section = $globalSettingsProvider->getSection('Plugins'); + // $section['Plugins'] = $pluginsToLoad; + $globalSettingsProvider->setSection('Plugins', $section); + return new PluginList($globalSettingsProvider); + } + +}