PageRenderTime 195ms CodeModel.GetById 36ms RepoModel.GetById 5ms app.codeStats 0ms

/core/Plugin/Manager.php

https://github.com/CodeYellowBV/piwik
PHP | 1287 lines | 962 code | 104 blank | 221 comment | 59 complexity | 88af3d0c89590dd3d8eb1341ad1539fa MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik\Plugin;
  10. use Piwik\Common;
  11. use Piwik\Config as PiwikConfig;
  12. use Piwik\Config;
  13. use Piwik\EventDispatcher;
  14. use Piwik\Filesystem;
  15. use Piwik\Option;
  16. use Piwik\Plugin;
  17. use Piwik\Singleton;
  18. use Piwik\Theme;
  19. use Piwik\Tracker;
  20. use Piwik\Translate;
  21. use Piwik\Updater;
  22. require_once PIWIK_INCLUDE_PATH . '/core/EventDispatcher.php';
  23. /**
  24. * The singleton that manages plugin loading/unloading and installation/uninstallation.
  25. *
  26. * @method static \Piwik\Plugin\Manager getInstance()
  27. */
  28. class Manager extends Singleton
  29. {
  30. protected $pluginsToLoad = array();
  31. protected $doLoadPlugins = true;
  32. /**
  33. * @var Plugin[]
  34. */
  35. protected $loadedPlugins = array();
  36. /**
  37. * Default theme used in Piwik.
  38. */
  39. const DEFAULT_THEME = "Morpheus";
  40. protected $doLoadAlwaysActivatedPlugins = true;
  41. // These are always activated and cannot be deactivated
  42. protected $pluginToAlwaysActivate = array(
  43. 'CoreHome',
  44. 'CoreUpdater',
  45. 'CoreAdminHome',
  46. 'CoreConsole',
  47. 'CorePluginsAdmin',
  48. 'CoreVisualizations',
  49. 'Installation',
  50. 'SitesManager',
  51. 'UsersManager',
  52. 'API',
  53. 'Proxy',
  54. 'LanguagesManager',
  55. // default Piwik theme, always enabled
  56. self::DEFAULT_THEME,
  57. );
  58. // Plugins bundled with core package, disabled by default
  59. protected $corePluginsDisabledByDefault = array(
  60. 'DBStats',
  61. 'ExampleCommand',
  62. 'ExampleSettingsPlugin',
  63. 'ExampleUI',
  64. 'ExampleVisualization',
  65. 'ExamplePluginTemplate',
  66. );
  67. // Themes bundled with core package, disabled by default
  68. protected $coreThemesDisabledByDefault = array(
  69. 'ExampleTheme'
  70. );
  71. /**
  72. * Loads plugin that are enabled
  73. */
  74. public function loadActivatedPlugins()
  75. {
  76. $pluginsToLoad = Config::getInstance()->Plugins['Plugins'];
  77. $this->loadPlugins($pluginsToLoad);
  78. }
  79. /**
  80. * Called during Tracker
  81. */
  82. public function loadCorePluginsDuringTracker()
  83. {
  84. $pluginsToLoad = Config::getInstance()->Plugins['Plugins'];
  85. $pluginsToLoad = array_diff($pluginsToLoad, Tracker::getPluginsNotToLoad());
  86. if(defined('PIWIK_TEST_MODE')) {
  87. $pluginsToLoad = array_intersect($pluginsToLoad, $this->getPluginsToLoadDuringTests());
  88. }
  89. $this->loadPlugins($pluginsToLoad);
  90. }
  91. /**
  92. * @return array names of plugins that have been loaded
  93. */
  94. public function loadTrackerPlugins()
  95. {
  96. $this->unloadPlugins();
  97. $pluginsTracker = PiwikConfig::getInstance()->Plugins_Tracker['Plugins_Tracker'];
  98. if (empty($pluginsTracker)) {
  99. return array();
  100. }
  101. $pluginsTracker = array_diff($pluginsTracker, Tracker::getPluginsNotToLoad());
  102. if(defined('PIWIK_TEST_MODE')) {
  103. $pluginsTracker = array_intersect($pluginsTracker, $this->getPluginsToLoadDuringTests());
  104. }
  105. $this->doNotLoadAlwaysActivatedPlugins();
  106. $this->loadPlugins($pluginsTracker);
  107. return $pluginsTracker;
  108. }
  109. public function getPluginsToLoadDuringTests()
  110. {
  111. $toLoad = array();
  112. $loadStandalonePluginsDuringTests = @Config::getInstance()->DebugTests['enable_load_standalone_plugins_during_tests'];
  113. foreach($this->readPluginsDirectory() as $plugin) {
  114. $forceDisable = array(
  115. 'ExampleVisualization', // adds an icon
  116. 'LoginHttpAuth', // other Login plugins would conflict
  117. );
  118. if(in_array($plugin, $forceDisable)) {
  119. continue;
  120. }
  121. // Load all default plugins
  122. $isPluginBundledWithCore = $this->isPluginBundledWithCore($plugin);
  123. // Load plugins from submodules
  124. $isPluginOfficiallySupported = $this->isPluginOfficialAndNotBundledWithCore($plugin);
  125. // Also load plugins which are Git repositories (eg. being developed)
  126. $isPluginHasGitRepository = file_exists( PIWIK_INCLUDE_PATH . '/plugins/' . $plugin . '/.git/config');
  127. $loadPlugin = $isPluginBundledWithCore || $isPluginOfficiallySupported;
  128. if($loadStandalonePluginsDuringTests) {
  129. $loadPlugin = $loadPlugin || $isPluginHasGitRepository;
  130. } else {
  131. $loadPlugin = $loadPlugin && !$isPluginHasGitRepository;
  132. }
  133. // Do not enable other Themes
  134. $disabledThemes = $this->coreThemesDisabledByDefault;
  135. // PleineLune is officially supported, yet we don't want to enable another theme in tests (we test for Morpheus)
  136. $disabledThemes[] = "PleineLune";
  137. $isThemeDisabled = in_array($plugin, $disabledThemes);
  138. $loadPlugin = $loadPlugin && !$isThemeDisabled;
  139. if($loadPlugin) {
  140. $toLoad[] = $plugin;
  141. }
  142. }
  143. return $toLoad;
  144. }
  145. public function getCorePluginsDisabledByDefault()
  146. {
  147. return array_merge( $this->corePluginsDisabledByDefault, $this->coreThemesDisabledByDefault);
  148. }
  149. // If a plugin hooks onto at least an event starting with "Tracker.", we load the plugin during tracker
  150. const TRACKER_EVENT_PREFIX = 'Tracker.';
  151. /**
  152. * @param $pluginName
  153. * @return bool
  154. */
  155. public function isPluginOfficialAndNotBundledWithCore($pluginName)
  156. {
  157. static $gitModules;
  158. if(empty($gitModules)) {
  159. $gitModules = file_get_contents(PIWIK_INCLUDE_PATH . '/.gitmodules');
  160. }
  161. // All submodules are officially maintained plugins
  162. $isSubmodule = false !== strpos($gitModules, "plugins/" . $pluginName . "\n");
  163. return $isSubmodule;
  164. }
  165. /**
  166. * Update Plugins config
  167. *
  168. * @param array $plugins Plugins
  169. */
  170. private function updatePluginsConfig($pluginsToLoad)
  171. {
  172. $section = PiwikConfig::getInstance()->Plugins;
  173. $section['Plugins'] = $pluginsToLoad;
  174. PiwikConfig::getInstance()->Plugins = $section;
  175. }
  176. /**
  177. * Update Plugins_Tracker config
  178. *
  179. * @param array $plugins Plugins
  180. */
  181. private function updatePluginsTrackerConfig($plugins)
  182. {
  183. $section = PiwikConfig::getInstance()->Plugins_Tracker;
  184. $section['Plugins_Tracker'] = $plugins;
  185. PiwikConfig::getInstance()->Plugins_Tracker = $section;
  186. }
  187. /**
  188. * Update PluginsInstalled config
  189. *
  190. * @param array $plugins Plugins
  191. */
  192. private function updatePluginsInstalledConfig($plugins)
  193. {
  194. $section = PiwikConfig::getInstance()->PluginsInstalled;
  195. $section['PluginsInstalled'] = $plugins;
  196. PiwikConfig::getInstance()->PluginsInstalled = $section;
  197. }
  198. public function clearPluginsInstalledConfig()
  199. {
  200. $this->updatePluginsInstalledConfig( array() );
  201. PiwikConfig::getInstance()->forceSave();
  202. PiwikConfig::getInstance()->init();
  203. }
  204. /**
  205. * Returns true if plugin is always activated
  206. *
  207. * @param string $name Name of plugin
  208. * @return bool
  209. */
  210. private function isPluginAlwaysActivated($name)
  211. {
  212. return in_array($name, $this->pluginToAlwaysActivate);
  213. }
  214. /**
  215. * Returns true if the plugin can be uninstalled. Any non-core plugin can be uninstalled.
  216. *
  217. * @param $name
  218. * @return bool
  219. */
  220. private function isPluginUninstallable($name)
  221. {
  222. return !$this->isPluginBundledWithCore($name);
  223. }
  224. /**
  225. * Returns `true` if a plugin has been activated.
  226. *
  227. * @param string $name Name of plugin, eg, `'Actions'`.
  228. * @return bool
  229. * @api
  230. */
  231. public function isPluginActivated($name)
  232. {
  233. return in_array($name, $this->pluginsToLoad)
  234. || $this->isPluginAlwaysActivated($name);
  235. }
  236. /**
  237. * Returns `true` if plugin is loaded (in memory).
  238. *
  239. * @param string $name Name of plugin, eg, `'Acions'`.
  240. * @return bool
  241. * @api
  242. */
  243. public function isPluginLoaded($name)
  244. {
  245. return isset($this->loadedPlugins[$name]);
  246. }
  247. /**
  248. * Reads the directories inside the plugins/ directory and returns their names in an array
  249. *
  250. * @return array
  251. */
  252. public function readPluginsDirectory()
  253. {
  254. $pluginsName = _glob(self::getPluginsDirectory() . '*', GLOB_ONLYDIR);
  255. $result = array();
  256. if ($pluginsName != false) {
  257. foreach ($pluginsName as $path) {
  258. if (self::pluginStructureLooksValid($path)) {
  259. $result[] = basename($path);
  260. }
  261. }
  262. }
  263. return $result;
  264. }
  265. public static function getPluginsDirectory()
  266. {
  267. return PIWIK_INCLUDE_PATH . '/plugins/';
  268. }
  269. /**
  270. * Deactivate plugin
  271. *
  272. * @param string $pluginName Name of plugin
  273. */
  274. public function deactivatePlugin($pluginName)
  275. {
  276. // execute deactivate() to let the plugin do cleanups
  277. $this->executePluginDeactivate($pluginName);
  278. $this->unloadPluginFromMemory($pluginName);
  279. $this->removePluginFromConfig($pluginName);
  280. $this->clearCache($pluginName);
  281. }
  282. /**
  283. * Tries to find the given components such as a Menu or Tasks implemented by plugins.
  284. * This method won't cache the found components. If you need to find the same component multiple times you might
  285. * want to cache the result to save a tiny bit of time.
  286. *
  287. * @param string $componentName The name of the component you want to look for. In case you request a
  288. * component named 'Menu' it'll look for a file named 'Menu.php' within the
  289. * root of all plugin folders that implement a class named
  290. * Piwik\Plugin\$PluginName\Menu.
  291. * @param string $expectedSubclass If not empty, a check will be performed whether a found file extends the
  292. * given subclass. If the requested file exists but does not extend this class
  293. * a warning will be shown to advice a developer to extend this certain class.
  294. *
  295. * @return \stdClass[]
  296. */
  297. public function findComponents($componentName, $expectedSubclass)
  298. {
  299. $plugins = $this->getLoadedPlugins();
  300. $components = array();
  301. foreach ($plugins as $plugin) {
  302. $component = $plugin->findComponent($componentName, $expectedSubclass);
  303. if (!empty($component)) {
  304. $components[] = $component;
  305. }
  306. }
  307. return $components;
  308. }
  309. /**
  310. * Uninstalls a Plugin (deletes plugin files from the disk)
  311. * Only deactivated plugins can be uninstalled
  312. *
  313. * @param $pluginName
  314. * @throws \Exception
  315. * @return bool
  316. */
  317. public function uninstallPlugin($pluginName)
  318. {
  319. if ($this->isPluginLoaded($pluginName)) {
  320. throw new \Exception("To uninstall the plugin $pluginName, first disable it in Piwik > Settings > Plugins");
  321. }
  322. $this->returnLoadedPluginsInfo();
  323. $this->executePluginDeactivate($pluginName);
  324. $this->executePluginUninstall($pluginName);
  325. $this->removePluginFromPluginsInstalledConfig($pluginName);
  326. $this->unloadPluginFromMemory($pluginName);
  327. $this->removePluginFromConfig($pluginName);
  328. Option::delete('version_' . $pluginName);
  329. \Piwik\Settings\Manager::cleanupPluginSettings($pluginName);
  330. $this->clearCache($pluginName);
  331. self::deletePluginFromFilesystem($pluginName);
  332. if ($this->isPluginInFilesystem($pluginName)) {
  333. return false;
  334. }
  335. return true;
  336. }
  337. /**
  338. * @param string $pluginName
  339. */
  340. private function clearCache($pluginName)
  341. {
  342. Filesystem::deleteAllCacheOnUpdate($pluginName);
  343. }
  344. public static function deletePluginFromFilesystem($plugin)
  345. {
  346. Filesystem::unlinkRecursive(PIWIK_INCLUDE_PATH . '/plugins/' . $plugin, $deleteRootToo = true);
  347. }
  348. /**
  349. * Install loaded plugins
  350. *
  351. * @throws
  352. * @return array Error messages of plugin install fails
  353. */
  354. public function installLoadedPlugins()
  355. {
  356. $messages = array();
  357. foreach ($this->getLoadedPlugins() as $plugin) {
  358. try {
  359. $this->installPluginIfNecessary($plugin);
  360. } catch (\Exception $e) {
  361. $messages[] = $e->getMessage();
  362. }
  363. }
  364. return $messages;
  365. }
  366. /**
  367. * Activate the specified plugin and install (if needed)
  368. *
  369. * @param string $pluginName Name of plugin
  370. * @throws \Exception
  371. */
  372. public function activatePlugin($pluginName)
  373. {
  374. $plugins = PiwikConfig::getInstance()->Plugins['Plugins'];
  375. if (in_array($pluginName, $plugins)) {
  376. throw new \Exception("Plugin '$pluginName' already activated.");
  377. }
  378. if (!$this->isPluginInFilesystem($pluginName)) {
  379. throw new \Exception("Plugin '$pluginName' cannot be found in the filesystem in plugins/ directory.");
  380. }
  381. $this->deactivateThemeIfTheme($pluginName);
  382. // Load plugin
  383. $plugin = $this->loadPlugin($pluginName);
  384. if ($plugin === null) {
  385. throw new \Exception("The plugin '$pluginName' was found in the filesystem, but could not be loaded.'");
  386. }
  387. $this->installPluginIfNecessary($plugin);
  388. $plugin->activate();
  389. EventDispatcher::getInstance()->postPendingEventsTo($plugin);
  390. $this->pluginsToLoad[] = $pluginName;
  391. $this->updatePluginsConfig($this->pluginsToLoad);
  392. PiwikConfig::getInstance()->forceSave();
  393. $this->clearCache($pluginName);
  394. }
  395. protected function isPluginInFilesystem($pluginName)
  396. {
  397. $existingPlugins = $this->readPluginsDirectory();
  398. $isPluginInFilesystem = array_search($pluginName, $existingPlugins) !== false;
  399. return Filesystem::isValidFilename($pluginName)
  400. && $isPluginInFilesystem;
  401. }
  402. /**
  403. * Returns the currently enabled theme.
  404. *
  405. * If no theme is enabled, the **Morpheus** plugin is returned (this is the base and default theme).
  406. *
  407. * @return Plugin
  408. * @api
  409. */
  410. public function getThemeEnabled()
  411. {
  412. $plugins = $this->getLoadedPlugins();
  413. $theme = false;
  414. foreach ($plugins as $plugin) {
  415. /* @var $plugin Plugin */
  416. if ($plugin->isTheme()
  417. && $this->isPluginActivated($plugin->getPluginName())
  418. ) {
  419. if ($plugin->getPluginName() != self::DEFAULT_THEME) {
  420. return $plugin; // enabled theme (not default)
  421. }
  422. $theme = $plugin; // default theme
  423. }
  424. }
  425. return $theme;
  426. }
  427. /**
  428. * @param string $themeName
  429. * @throws \Exception
  430. * @return Theme
  431. */
  432. public function getTheme($themeName)
  433. {
  434. $plugins = $this->getLoadedPlugins();
  435. foreach ($plugins as $plugin) {
  436. if ($plugin->isTheme() && $plugin->getPluginName() == $themeName) {
  437. return new Theme($plugin);
  438. }
  439. }
  440. throw new \Exception('Theme not found : ' . $themeName);
  441. }
  442. public function getNumberOfActivatedPlugins()
  443. {
  444. $counter = 0;
  445. $pluginNames = $this->getLoadedPluginsName();
  446. foreach ($pluginNames as $pluginName) {
  447. if ($this->isPluginActivated($pluginName)) {
  448. $counter++;
  449. }
  450. }
  451. return $counter;
  452. }
  453. /**
  454. * Returns info regarding all plugins. Loads plugins that can be loaded.
  455. *
  456. * @return array An array that maps plugin names with arrays of plugin information. Plugin
  457. * information consists of the following entries:
  458. *
  459. * - **activated**: Whether the plugin is activated.
  460. * - **alwaysActivated**: Whether the plugin should always be activated,
  461. * or not.
  462. * - **uninstallable**: Whether the plugin is uninstallable or not.
  463. * - **invalid**: If the plugin is invalid, this property will be set to true.
  464. * If the plugin is not invalid, this property will not exist.
  465. * - **info**: If the plugin was loaded, will hold the plugin information.
  466. * See {@link Piwik\Plugin::getInformation()}.
  467. * @api
  468. */
  469. public function returnLoadedPluginsInfo()
  470. {
  471. $language = Translate::getLanguageToLoad();
  472. $plugins = array();
  473. $listPlugins = array_merge(
  474. $this->readPluginsDirectory(),
  475. PiwikConfig::getInstance()->Plugins['Plugins']
  476. );
  477. $listPlugins = array_unique($listPlugins);
  478. foreach ($listPlugins as $pluginName) {
  479. // Hide plugins that are never going to be used
  480. if($this->isPluginBogus($pluginName)) {
  481. continue;
  482. }
  483. // If the plugin is not core and looks bogus, do not load
  484. if ($this->isPluginThirdPartyAndBogus($pluginName)) {
  485. $info = array(
  486. 'invalid' => true,
  487. 'activated' => false,
  488. 'alwaysActivated' => false,
  489. 'uninstallable' => true,
  490. );
  491. } else {
  492. $this->loadTranslation($pluginName, $language);
  493. $this->loadPlugin($pluginName);
  494. $info = array(
  495. 'activated' => $this->isPluginActivated($pluginName),
  496. 'alwaysActivated' => $this->isPluginAlwaysActivated($pluginName),
  497. 'uninstallable' => $this->isPluginUninstallable($pluginName),
  498. );
  499. }
  500. $plugins[$pluginName] = $info;
  501. }
  502. $this->loadPluginTranslations();
  503. $loadedPlugins = $this->getLoadedPlugins();
  504. foreach ($loadedPlugins as $oPlugin) {
  505. $pluginName = $oPlugin->getPluginName();
  506. $info = array(
  507. 'info' => $oPlugin->getInformation(),
  508. 'activated' => $this->isPluginActivated($pluginName),
  509. 'alwaysActivated' => $this->isPluginAlwaysActivated($pluginName),
  510. 'missingRequirements' => $oPlugin->getMissingDependencies(),
  511. 'uninstallable' => $this->isPluginUninstallable($pluginName),
  512. );
  513. $plugins[$pluginName] = $info;
  514. }
  515. return $plugins;
  516. }
  517. protected static function isManifestFileFound($path)
  518. {
  519. return file_exists($path . "/" . MetadataLoader::PLUGIN_JSON_FILENAME);
  520. }
  521. /**
  522. * Returns `true` if the plugin is bundled with core or `false` if it is third party.
  523. *
  524. * @param string $name The name of the plugin, eg, `'Actions'`.
  525. * @return bool
  526. */
  527. public function isPluginBundledWithCore($name)
  528. {
  529. // Reading the plugins from the global.ini.php config file
  530. $pluginsBundledWithPiwik = PiwikConfig::getInstance()->getFromGlobalConfig('Plugins');
  531. $pluginsBundledWithPiwik = $pluginsBundledWithPiwik['Plugins'];
  532. return (!empty($pluginsBundledWithPiwik)
  533. && in_array($name, $pluginsBundledWithPiwik))
  534. || in_array($name, $this->getCorePluginsDisabledByDefault())
  535. || $name == self::DEFAULT_THEME;
  536. }
  537. protected function isPluginThirdPartyAndBogus($pluginName)
  538. {
  539. if($this->isPluginBundledWithCore($pluginName)) {
  540. return false;
  541. }
  542. if($this->isPluginBogus($pluginName)) {
  543. return true;
  544. }
  545. $path = $this->getPluginsDirectory() . $pluginName;
  546. if(!$this->isManifestFileFound($path)) {
  547. return true;
  548. }
  549. return false;
  550. }
  551. /**
  552. * Load the specified plugins.
  553. *
  554. * @param array $pluginsToLoad Array of plugins to load.
  555. */
  556. public function loadPlugins(array $pluginsToLoad)
  557. {
  558. $pluginsToLoad = array_unique($pluginsToLoad);
  559. $this->pluginsToLoad = $pluginsToLoad;
  560. $this->reloadPlugins();
  561. }
  562. /**
  563. * Disable plugin loading.
  564. */
  565. public function doNotLoadPlugins()
  566. {
  567. $this->doLoadPlugins = false;
  568. }
  569. /**
  570. * Disable loading of "always activated" plugins.
  571. */
  572. public function doNotLoadAlwaysActivatedPlugins()
  573. {
  574. $this->doLoadAlwaysActivatedPlugins = false;
  575. }
  576. /**
  577. * Load translations for loaded plugins
  578. *
  579. * @param bool|string $language Optional language code
  580. */
  581. public function loadPluginTranslations($language = false)
  582. {
  583. if (empty($language)) {
  584. $language = Translate::getLanguageToLoad();
  585. }
  586. $plugins = $this->getLoadedPlugins();
  587. foreach ($plugins as $plugin) {
  588. $this->loadTranslation($plugin, $language);
  589. }
  590. }
  591. /**
  592. * Execute postLoad() hook for loaded plugins
  593. */
  594. public function postLoadPlugins()
  595. {
  596. $plugins = $this->getLoadedPlugins();
  597. foreach ($plugins as $plugin) {
  598. $plugin->postLoad();
  599. }
  600. }
  601. /**
  602. * Returns an array containing the plugins class names (eg. 'UserCountry' and NOT 'UserCountry')
  603. *
  604. * @return array
  605. */
  606. public function getLoadedPluginsName()
  607. {
  608. return array_keys($this->getLoadedPlugins());
  609. }
  610. /**
  611. * Returns an array mapping loaded plugin names with their plugin objects, eg,
  612. *
  613. * array(
  614. * 'UserCountry' => Plugin $pluginObject,
  615. * 'UserSettings' => Plugin $pluginObject,
  616. * );
  617. *
  618. * @return Plugin[]
  619. */
  620. public function getLoadedPlugins()
  621. {
  622. return $this->loadedPlugins;
  623. }
  624. /**
  625. * @param string $piwikVersion
  626. * @return Plugin[]
  627. */
  628. public function getIncompatiblePlugins($piwikVersion)
  629. {
  630. $plugins = $this->getLoadedPlugins();
  631. $incompatible = array();
  632. foreach ($plugins as $plugin) {
  633. if ($plugin->hasMissingDependencies($piwikVersion)) {
  634. $incompatible[] = $plugin;
  635. }
  636. }
  637. return $incompatible;
  638. }
  639. /**
  640. * Returns an array of plugins that are currently loaded and activated,
  641. * mapping loaded plugin names with their plugin objects, eg,
  642. *
  643. * array(
  644. * 'UserCountry' => Plugin $pluginObject,
  645. * 'UserSettings' => Plugin $pluginObject,
  646. * );
  647. *
  648. * @return Plugin[]
  649. */
  650. public function getPluginsLoadedAndActivated()
  651. {
  652. $plugins = $this->getLoadedPlugins();
  653. $enabled = $this->getActivatedPlugins();
  654. if(empty($enabled)) {
  655. return array();
  656. }
  657. $enabled = array_combine($enabled, $enabled);
  658. $plugins = array_intersect_key($plugins, $enabled);
  659. return $plugins;
  660. }
  661. /**
  662. * Returns a list of all names of currently activated plugin eg,
  663. *
  664. * array(
  665. * 'UserCountry'
  666. * 'UserSettings'
  667. * );
  668. *
  669. * @return string[]
  670. */
  671. public function getActivatedPlugins()
  672. {
  673. return $this->pluginsToLoad;
  674. }
  675. /**
  676. * Returns a Plugin object by name.
  677. *
  678. * @param string $name The name of the plugin, eg, `'Actions'`.
  679. * @throws \Exception If the plugin has not been loaded.
  680. * @return Plugin
  681. */
  682. public function getLoadedPlugin($name)
  683. {
  684. if (!isset($this->loadedPlugins[$name])) {
  685. throw new \Exception("The plugin '$name' has not been loaded.");
  686. }
  687. return $this->loadedPlugins[$name];
  688. }
  689. /**
  690. * Load the plugins classes installed.
  691. * Register the observers for every plugin.
  692. */
  693. private function reloadPlugins()
  694. {
  695. if ($this->doLoadAlwaysActivatedPlugins) {
  696. $this->pluginsToLoad = array_merge($this->pluginsToLoad, $this->pluginToAlwaysActivate);
  697. }
  698. $this->pluginsToLoad = array_unique($this->pluginsToLoad);
  699. $pluginsToPostPendingEventsTo = array();
  700. foreach ($this->pluginsToLoad as $pluginName) {
  701. if (!$this->isPluginLoaded($pluginName)
  702. && !$this->isPluginThirdPartyAndBogus($pluginName)
  703. ) {
  704. $newPlugin = $this->loadPlugin($pluginName);
  705. if ($newPlugin === null) {
  706. continue;
  707. }
  708. if ($newPlugin->hasMissingDependencies()) {
  709. $this->deactivatePlugin($pluginName);
  710. continue;
  711. }
  712. $pluginsToPostPendingEventsTo[] = $newPlugin;
  713. }
  714. }
  715. // post pending events after all plugins are successfully loaded
  716. foreach ($pluginsToPostPendingEventsTo as $plugin) {
  717. EventDispatcher::getInstance()->postPendingEventsTo($plugin);
  718. }
  719. }
  720. public function getIgnoredBogusPlugins()
  721. {
  722. $ignored = array();
  723. foreach ($this->pluginsToLoad as $pluginName) {
  724. if ($this->isPluginThirdPartyAndBogus($pluginName)) {
  725. $ignored[] = $pluginName;
  726. }
  727. }
  728. return $ignored;
  729. }
  730. /**
  731. * Returns the name of all plugins found in this Piwik instance
  732. * (including those not enabled and themes)
  733. *
  734. * @return array
  735. */
  736. public static function getAllPluginsNames()
  737. {
  738. $pluginsToLoad = array_merge(
  739. PiwikConfig::getInstance()->Plugins['Plugins'],
  740. self::getInstance()->readPluginsDirectory(),
  741. self::getInstance()->getCorePluginsDisabledByDefault()
  742. );
  743. $pluginsToLoad = array_values(array_unique($pluginsToLoad));
  744. return $pluginsToLoad;
  745. }
  746. /**
  747. * Loads the plugin filename and instantiates the plugin with the given name, eg. UserCountry
  748. *
  749. * @param string $pluginName
  750. * @throws \Exception
  751. * @return Plugin|null
  752. */
  753. public function loadPlugin($pluginName)
  754. {
  755. if (isset($this->loadedPlugins[$pluginName])) {
  756. return $this->loadedPlugins[$pluginName];
  757. }
  758. $newPlugin = $this->makePluginClass($pluginName);
  759. $this->addLoadedPlugin($pluginName, $newPlugin);
  760. return $newPlugin;
  761. }
  762. /**
  763. * @param $pluginName
  764. * @return Plugin
  765. * @throws \Exception
  766. */
  767. protected function makePluginClass($pluginName)
  768. {
  769. $pluginFileName = sprintf("%s/%s.php", $pluginName, $pluginName);
  770. $pluginClassName = $pluginName;
  771. if (!Filesystem::isValidFilename($pluginName)) {
  772. throw new \Exception(sprintf("The plugin filename '%s' is not a valid filename", $pluginFileName));
  773. }
  774. $path = self::getPluginsDirectory() . $pluginFileName;
  775. if (!file_exists($path)) {
  776. // Create the smallest minimal Piwik Plugin
  777. // Eg. Used for Morpheus default theme which does not have a Morpheus.php file
  778. return new Plugin($pluginName);
  779. }
  780. require_once $path;
  781. $namespacedClass = $this->getClassNamePlugin($pluginName);
  782. if (!class_exists($namespacedClass, false)) {
  783. throw new \Exception("The class $pluginClassName couldn't be found in the file '$path'");
  784. }
  785. $newPlugin = new $namespacedClass;
  786. if (!($newPlugin instanceof Plugin)) {
  787. throw new \Exception("The plugin $pluginClassName in the file $path must inherit from Plugin.");
  788. }
  789. return $newPlugin;
  790. }
  791. protected function getClassNamePlugin($pluginName)
  792. {
  793. $className = $pluginName;
  794. if ($pluginName == 'API') {
  795. $className = 'Plugin';
  796. }
  797. return "\\Piwik\\Plugins\\$pluginName\\$className";
  798. }
  799. /**
  800. * Unload plugin
  801. *
  802. * @param Plugin|string $plugin
  803. * @throws \Exception
  804. */
  805. public function unloadPlugin($plugin)
  806. {
  807. if (!($plugin instanceof Plugin)) {
  808. $oPlugin = $this->loadPlugin($plugin);
  809. if ($oPlugin === null) {
  810. unset($this->loadedPlugins[$plugin]);
  811. return;
  812. }
  813. $plugin = $oPlugin;
  814. }
  815. unset($this->loadedPlugins[$plugin->getPluginName()]);
  816. }
  817. /**
  818. * Unload all loaded plugins
  819. */
  820. public function unloadPlugins()
  821. {
  822. $pluginsLoaded = $this->getLoadedPlugins();
  823. foreach ($pluginsLoaded as $plugin) {
  824. $this->unloadPlugin($plugin);
  825. }
  826. }
  827. /**
  828. * Install a specific plugin
  829. *
  830. * @param Plugin $plugin
  831. * @throws \Piwik\Plugin\PluginException if installation fails
  832. */
  833. private function executePluginInstall(Plugin $plugin)
  834. {
  835. try {
  836. $plugin->install();
  837. } catch (\Exception $e) {
  838. throw new \Piwik\Plugin\PluginException($plugin->getPluginName(), $e->getMessage());
  839. }
  840. }
  841. /**
  842. * Add a plugin in the loaded plugins array
  843. *
  844. * @param string $pluginName plugin name without prefix (eg. 'UserCountry')
  845. * @param Plugin $newPlugin
  846. */
  847. private function addLoadedPlugin($pluginName, Plugin $newPlugin)
  848. {
  849. $this->loadedPlugins[$pluginName] = $newPlugin;
  850. }
  851. /**
  852. * Load translation
  853. *
  854. * @param Plugin $plugin
  855. * @param string $langCode
  856. * @throws \Exception
  857. * @return bool whether the translation was found and loaded
  858. */
  859. private function loadTranslation($plugin, $langCode)
  860. {
  861. // we are in Tracker mode if Loader is not (yet) loaded
  862. if (!class_exists('Piwik\\Loader', false)) {
  863. return false;
  864. }
  865. if (is_string($plugin)) {
  866. $pluginName = $plugin;
  867. } else {
  868. $pluginName = $plugin->getPluginName();
  869. }
  870. $path = self::getPluginsDirectory() . $pluginName . '/lang/%s.json';
  871. $defaultLangPath = sprintf($path, $langCode);
  872. $defaultEnglishLangPath = sprintf($path, 'en');
  873. $translationsLoaded = false;
  874. // merge in english translations as default first
  875. if (file_exists($defaultEnglishLangPath)) {
  876. $translations = $this->getTranslationsFromFile($defaultEnglishLangPath);
  877. $translationsLoaded = true;
  878. if (isset($translations[$pluginName])) {
  879. // only merge translations of plugin - prevents overwritten strings
  880. Translate::mergeTranslationArray(array($pluginName => $translations[$pluginName]));
  881. }
  882. }
  883. // merge in specific language translations (to overwrite english defaults)
  884. if (file_exists($defaultLangPath)) {
  885. $translations = $this->getTranslationsFromFile($defaultLangPath);
  886. $translationsLoaded = true;
  887. if (isset($translations[$pluginName])) {
  888. // only merge translations of plugin - prevents overwritten strings
  889. Translate::mergeTranslationArray(array($pluginName => $translations[$pluginName]));
  890. }
  891. }
  892. return $translationsLoaded;
  893. }
  894. /**
  895. * Return names of all installed plugins.
  896. *
  897. * @return array
  898. * @api
  899. */
  900. public function getInstalledPluginsName()
  901. {
  902. $pluginNames = PiwikConfig::getInstance()->PluginsInstalled['PluginsInstalled'];
  903. return $pluginNames;
  904. }
  905. /**
  906. * Returns names of plugins that should be loaded, but cannot be since their
  907. * files cannot be found.
  908. *
  909. * @return array
  910. * @api
  911. */
  912. public function getMissingPlugins()
  913. {
  914. $missingPlugins = array();
  915. if (isset(PiwikConfig::getInstance()->Plugins['Plugins'])) {
  916. $plugins = PiwikConfig::getInstance()->Plugins['Plugins'];
  917. foreach ($plugins as $pluginName) {
  918. // if a plugin is listed in the config, but is not loaded, it does not exist in the folder
  919. if (!self::getInstance()->isPluginLoaded($pluginName)
  920. && !$this->isPluginBogus($pluginName)
  921. ) {
  922. $missingPlugins[] = $pluginName;
  923. }
  924. }
  925. }
  926. return $missingPlugins;
  927. }
  928. /**
  929. * Install a plugin, if necessary
  930. *
  931. * @param Plugin $plugin
  932. */
  933. private function installPluginIfNecessary(Plugin $plugin)
  934. {
  935. $pluginName = $plugin->getPluginName();
  936. $saveConfig = false;
  937. // is the plugin already installed or is it the first time we activate it?
  938. $pluginsInstalled = $this->getInstalledPluginsName();
  939. if (!$this->isPluginInstalled($pluginName)) {
  940. $this->executePluginInstall($plugin);
  941. $pluginsInstalled[] = $pluginName;
  942. $this->updatePluginsInstalledConfig($pluginsInstalled);
  943. Updater::recordComponentSuccessfullyUpdated($plugin->getPluginName(), $plugin->getVersion());
  944. $saveConfig = true;
  945. }
  946. if ($this->isTrackerPlugin($plugin)) {
  947. $pluginsTracker = PiwikConfig::getInstance()->Plugins_Tracker['Plugins_Tracker'];
  948. if (is_null($pluginsTracker)) {
  949. $pluginsTracker = array();
  950. }
  951. if (!in_array($pluginName, $pluginsTracker)) {
  952. $pluginsTracker[] = $pluginName;
  953. $this->updatePluginsTrackerConfig($pluginsTracker);
  954. $saveConfig = true;
  955. }
  956. }
  957. if ($saveConfig) {
  958. PiwikConfig::getInstance()->forceSave();
  959. }
  960. }
  961. public function isTrackerPlugin(Plugin $plugin)
  962. {
  963. $hooks = $plugin->getListHooksRegistered();
  964. $hookNames = array_keys($hooks);
  965. foreach ($hookNames as $name) {
  966. if (strpos($name, self::TRACKER_EVENT_PREFIX) === 0) {
  967. return true;
  968. }
  969. if ($name === 'Request.initAuthenticationObject') {
  970. return true;
  971. }
  972. }
  973. return false;
  974. }
  975. private static function pluginStructureLooksValid($path)
  976. {
  977. $name = basename($path);
  978. return file_exists($path . "/" . $name . ".php")
  979. || self::isManifestFileFound($path);
  980. }
  981. /**
  982. * @param $pluginName
  983. */
  984. private function removePluginFromPluginsInstalledConfig($pluginName)
  985. {
  986. $pluginsInstalled = PiwikConfig::getInstance()->PluginsInstalled['PluginsInstalled'];
  987. $key = array_search($pluginName, $pluginsInstalled);
  988. if ($key !== false) {
  989. unset($pluginsInstalled[$key]);
  990. }
  991. $this->updatePluginsInstalledConfig($pluginsInstalled);
  992. }
  993. /**
  994. * @param $pluginName
  995. */
  996. private function removePluginFromPluginsConfig($pluginName)
  997. {
  998. $pluginsEnabled = PiwikConfig::getInstance()->Plugins['Plugins'];
  999. $key = array_search($pluginName, $pluginsEnabled);
  1000. if ($key !== false) {
  1001. unset($pluginsEnabled[$key]);
  1002. }
  1003. $this->updatePluginsConfig($pluginsEnabled);
  1004. }
  1005. private function removePluginFromTrackerConfig($pluginName)
  1006. {
  1007. $pluginsTracker = PiwikConfig::getInstance()->Plugins_Tracker['Plugins_Tracker'];
  1008. if (!is_null($pluginsTracker)) {
  1009. $key = array_search($pluginName, $pluginsTracker);
  1010. if ($key !== false) {
  1011. unset($pluginsTracker[$key]);
  1012. $this->updatePluginsTrackerConfig($pluginsTracker);
  1013. }
  1014. }
  1015. }
  1016. /**
  1017. * @param string $pathToTranslationFile
  1018. * @throws \Exception
  1019. * @return mixed
  1020. */
  1021. private function getTranslationsFromFile($pathToTranslationFile)
  1022. {
  1023. $data = file_get_contents($pathToTranslationFile);
  1024. $translations = json_decode($data, true);
  1025. if (is_null($translations) && Common::hasJsonErrorOccurred()) {
  1026. $jsonError = Common::getLastJsonError();
  1027. $message = sprintf('Not able to load translation file %s: %s', $pathToTranslationFile, $jsonError);
  1028. throw new \Exception($message);
  1029. }
  1030. return $translations;
  1031. }
  1032. /**
  1033. * @param $pluginName
  1034. * @return bool
  1035. */
  1036. private function isPluginBogus($pluginName)
  1037. {
  1038. $bogusPlugins = array(
  1039. 'PluginMarketplace', //defines a plugin.json but 1.x Piwik plugin
  1040. 'DoNotTrack', // Removed in 2.0.3
  1041. 'AnonymizeIP', // Removed in 2.0.3
  1042. );
  1043. return in_array($pluginName, $bogusPlugins);
  1044. }
  1045. private function deactivateThemeIfTheme($pluginName)
  1046. {
  1047. // Only one theme enabled at a time
  1048. $themeEnabled = $this->getThemeEnabled();
  1049. if ($themeEnabled
  1050. && $themeEnabled->getPluginName() != self::DEFAULT_THEME) {
  1051. $themeAlreadyEnabled = $themeEnabled->getPluginName();
  1052. $plugin = $this->loadPlugin($pluginName);
  1053. if ($plugin->isTheme()) {
  1054. $this->deactivatePlugin($themeAlreadyEnabled);
  1055. }
  1056. }
  1057. }
  1058. /**
  1059. * @param $pluginName
  1060. */
  1061. private function executePluginDeactivate($pluginName)
  1062. {
  1063. if (!$this->isPluginBogus($pluginName)) {
  1064. $plugin = $this->loadPlugin($pluginName);
  1065. if ($plugin !== null) {
  1066. $plugin->deactivate();
  1067. }
  1068. }
  1069. }
  1070. /**
  1071. * @param $pluginName
  1072. */
  1073. private function unloadPluginFromMemory($pluginName)
  1074. {
  1075. $key = array_search($pluginName, $this->pluginsToLoad);
  1076. if ($key !== false) {
  1077. unset($this->pluginsToLoad[$key]);
  1078. }
  1079. }
  1080. /**
  1081. * @param $pluginName
  1082. */
  1083. private function removePluginFromConfig($pluginName)
  1084. {
  1085. $this->removePluginFromPluginsConfig($pluginName);
  1086. $this->removePluginFromTrackerConfig($pluginName);
  1087. PiwikConfig::getInstance()->forceSave();
  1088. }
  1089. /**
  1090. * @param $pluginName
  1091. */
  1092. private function executePluginUninstall($pluginName)
  1093. {
  1094. try {
  1095. $plugin = $this->getLoadedPlugin($pluginName);
  1096. $plugin->uninstall();
  1097. } catch (\Exception $e) {
  1098. }
  1099. }
  1100. /**
  1101. * @param $pluginName
  1102. * @return bool
  1103. */
  1104. public function isPluginInstalled($pluginName)
  1105. {
  1106. $pluginsInstalled = $this->getInstalledPluginsName();
  1107. return in_array($pluginName, $pluginsInstalled);
  1108. }
  1109. }
  1110. /**
  1111. */
  1112. class PluginException extends \Exception
  1113. {
  1114. function __construct($pluginName, $message)
  1115. {
  1116. parent::__construct("There was a problem installing the plugin " . $pluginName . ": " . $message . "
  1117. If this plugin has already been installed, and if you want to hide this message</b>, you must add the following line under the
  1118. [PluginsInstalled]
  1119. entry in your config/config.ini.php file:
  1120. PluginsInstalled[] = $pluginName");
  1121. }
  1122. }