PageRenderTime 49ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/core/PluginsManager.php

https://github.com/quarkness/piwik
PHP | 658 lines | 493 code | 40 blank | 125 comment | 20 complexity | 6bfd78c290a524ceef479304bfc11509 MD5 | raw file
  1. <?php
  2. /**
  3. * Piwik - Open source web analytics
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. * @version $Id$
  8. *
  9. * @category Piwik
  10. * @package Piwik
  11. */
  12. /**
  13. * @see core/Menu/Abstract.php
  14. * @see core/Menu/Main.php
  15. * @see core/Menu/Admin.php
  16. * @see core/Menu/Top.php
  17. * @see core/PluginsFunctions/WidgetsList.php
  18. * @see core/PluginsFunctions/Sql.php
  19. */
  20. require_once PIWIK_INCLUDE_PATH . '/core/Menu/Abstract.php';
  21. require_once PIWIK_INCLUDE_PATH . '/core/Menu/Main.php';
  22. require_once PIWIK_INCLUDE_PATH . '/core/Menu/Admin.php';
  23. require_once PIWIK_INCLUDE_PATH . '/core/Menu/Top.php';
  24. require_once PIWIK_INCLUDE_PATH . '/core/PluginsFunctions/WidgetsList.php';
  25. require_once PIWIK_INCLUDE_PATH . '/core/PluginsFunctions/Sql.php';
  26. /**
  27. * Plugin manager
  28. *
  29. * @package Piwik
  30. * @subpackage Piwik_PluginsManager
  31. */
  32. class Piwik_PluginsManager
  33. {
  34. /**
  35. * @var Event_Dispatcher
  36. */
  37. public $dispatcher;
  38. protected $pluginsToLoad = array();
  39. protected $doLoadPlugins = true;
  40. protected $loadedPlugins = array();
  41. protected $doLoadAlwaysActivatedPlugins = true;
  42. protected $pluginToAlwaysActivate = array(
  43. 'CoreHome',
  44. 'CoreUpdater',
  45. 'CoreAdminHome',
  46. 'CorePluginsAdmin',
  47. 'Installation',
  48. 'SitesManager',
  49. 'UsersManager',
  50. 'API',
  51. 'Proxy',
  52. 'LanguagesManager',
  53. );
  54. static private $instance = null;
  55. /**
  56. * Returns the singleton Piwik_PluginsManager
  57. *
  58. * @return Piwik_PluginsManager
  59. */
  60. static public function getInstance()
  61. {
  62. if (self::$instance == null)
  63. {
  64. self::$instance = new self;
  65. }
  66. return self::$instance;
  67. }
  68. private function __construct()
  69. {
  70. $this->dispatcher = Event_Dispatcher::getInstance();
  71. }
  72. /**
  73. * Returns true if plugin is always activated
  74. *
  75. * @param string $name Name of plugin
  76. * @return bool
  77. */
  78. public function isPluginAlwaysActivated( $name )
  79. {
  80. return in_array( $name, $this->pluginToAlwaysActivate);
  81. }
  82. /**
  83. * Returns true if plugin has been activated
  84. *
  85. * @param string $name Name of plugin
  86. * @return bool
  87. */
  88. public function isPluginActivated( $name )
  89. {
  90. return in_array( $name, $this->pluginsToLoad)
  91. || $this->isPluginAlwaysActivated( $name );
  92. }
  93. /**
  94. * Returns true if plugin is loaded (in memory)
  95. *
  96. * @parm string $name Name of plugin
  97. * @return bool
  98. */
  99. public function isPluginLoaded( $name )
  100. {
  101. return isset($this->loadedPlugins[$name]);
  102. }
  103. /**
  104. * Reads the directories inside the plugins/ directory and returns their names in an array
  105. *
  106. * @return array
  107. */
  108. public function readPluginsDirectory()
  109. {
  110. $pluginsName = _glob( PIWIK_INCLUDE_PATH . '/plugins/*', GLOB_ONLYDIR);
  111. $pluginsName = $pluginsName == false ? array() : array_map('basename', $pluginsName);
  112. return $pluginsName;
  113. }
  114. /**
  115. * Deactivate plugin
  116. *
  117. * @param string $pluginName Name of plugin
  118. */
  119. public function deactivatePlugin($pluginName)
  120. {
  121. $plugins = $this->pluginsToLoad;
  122. $key = array_search($pluginName,$plugins);
  123. if($key !== false)
  124. {
  125. unset($plugins[$key]);
  126. Zend_Registry::get('config')->Plugins = $plugins;
  127. }
  128. $pluginsTracker = Zend_Registry::get('config')->Plugins_Tracker->Plugins_Tracker;
  129. if(!is_null($pluginsTracker))
  130. {
  131. $pluginsTracker = $pluginsTracker->toArray();
  132. $key = array_search($pluginName,$pluginsTracker);
  133. if($key !== false)
  134. {
  135. unset($pluginsTracker[$key]);
  136. Zend_Registry::get('config')->Plugins_Tracker = array('Plugins_Tracker' => $pluginsTracker);
  137. }
  138. }
  139. // Delete merged js/css files to force regenerations to exclude the deactivated plugin
  140. Piwik::deleteAllCacheOnUpdate();
  141. }
  142. /**
  143. * Install loaded plugins
  144. */
  145. public function installLoadedPlugins()
  146. {
  147. foreach($this->getLoadedPlugins() as $plugin)
  148. {
  149. try {
  150. $this->installPluginIfNecessary( $plugin );
  151. }catch(Exception $e){
  152. echo $e->getMessage();
  153. }
  154. }
  155. }
  156. /**
  157. * Activate the specified plugin and install (if needed)
  158. *
  159. * @param string $pluginName Name of plugin
  160. */
  161. public function activatePlugin($pluginName)
  162. {
  163. $plugins = Zend_Registry::get('config')->Plugins->Plugins->toArray();
  164. if(in_array($pluginName,$plugins))
  165. {
  166. throw new Exception("Plugin '$pluginName' already activated.");
  167. }
  168. $existingPlugins = $this->readPluginsDirectory();
  169. if( array_search($pluginName,$existingPlugins) === false)
  170. {
  171. throw new Exception("Unable to find the plugin '$pluginName'.");
  172. }
  173. $plugin = $this->loadPlugin($pluginName);
  174. $this->installPluginIfNecessary($plugin);
  175. // we add the plugin to the list of activated plugins
  176. $plugins[] = $pluginName;
  177. // the config file will automatically be saved with the new plugin
  178. Zend_Registry::get('config')->Plugins = $plugins;
  179. // Delete merged js/css files to force regenerations to include the activated plugin
  180. Piwik::deleteAllCacheOnUpdate();
  181. }
  182. /**
  183. * Load the specified plugins
  184. *
  185. * @param array $pluginsToLoad Array of plugins to load
  186. */
  187. public function loadPlugins( array $pluginsToLoad )
  188. {
  189. // case no plugins to load
  190. if(is_null($pluginsToLoad))
  191. {
  192. $pluginsToLoad = array();
  193. }
  194. $this->pluginsToLoad = $pluginsToLoad;
  195. $this->reloadPlugins();
  196. }
  197. /**
  198. * Disable plugin loading
  199. */
  200. public function doNotLoadPlugins()
  201. {
  202. $this->doLoadPlugins = false;
  203. }
  204. /**
  205. * Disable loading of "always activated" plugins
  206. */
  207. public function doNotLoadAlwaysActivatedPlugins()
  208. {
  209. $this->doLoadAlwaysActivatedPlugins = false;
  210. }
  211. /**
  212. * Load translations for loaded plugins
  213. *
  214. * @param string $language Optional language code
  215. */
  216. public function loadPluginTranslations($language = false)
  217. {
  218. if(empty($language))
  219. {
  220. $language = Piwik_Translate::getInstance()->getLanguageToLoad();
  221. }
  222. $plugins = $this->getLoadedPlugins();
  223. foreach($plugins as $plugin)
  224. {
  225. $this->loadTranslation( $plugin, $language );
  226. }
  227. }
  228. /**
  229. * Execute postLoad() hook for loaded plugins
  230. *
  231. * @see Piwik_Plugin::postLoad()
  232. */
  233. public function postLoadPlugins()
  234. {
  235. $plugins = $this->getLoadedPlugins();
  236. foreach($plugins as $plugin)
  237. {
  238. $plugin->postLoad();
  239. }
  240. }
  241. /**
  242. * Returns an array containing the plugins class names (eg. 'Piwik_UserCountry' and NOT 'UserCountry')
  243. *
  244. * @return array
  245. */
  246. public function getLoadedPluginsName()
  247. {
  248. return array_map('get_class', $this->getLoadedPlugins());
  249. }
  250. /**
  251. * Returns an array of key,value with the following format: array(
  252. * 'UserCountry' => Piwik_Plugin $pluginObject,
  253. * 'UserSettings' => Piwik_Plugin $pluginObject,
  254. * );
  255. *
  256. * @return array
  257. */
  258. public function getLoadedPlugins()
  259. {
  260. return $this->loadedPlugins;
  261. }
  262. /**
  263. * Returns the given Piwik_Plugin object
  264. *
  265. * @param string $name
  266. * @return Piwik_Piwik
  267. */
  268. public function getLoadedPlugin($name)
  269. {
  270. if(!isset($this->loadedPlugins[$name]))
  271. {
  272. throw new Exception("The plugin '$name' has not been loaded.");
  273. }
  274. return $this->loadedPlugins[$name];
  275. }
  276. /**
  277. * Load the plugins classes installed.
  278. * Register the observers for every plugin.
  279. */
  280. private function reloadPlugins()
  281. {
  282. $this->pluginsToLoad = array_unique($this->pluginsToLoad);
  283. if($this->doLoadAlwaysActivatedPlugins)
  284. {
  285. $this->pluginsToLoad = array_merge($this->pluginsToLoad, $this->pluginToAlwaysActivate);
  286. }
  287. foreach($this->pluginsToLoad as $pluginName)
  288. {
  289. if(!$this->isPluginLoaded($pluginName))
  290. {
  291. $newPlugin = $this->loadPlugin($pluginName);
  292. if($this->doLoadPlugins
  293. && $this->isPluginActivated($pluginName))
  294. {
  295. $this->addPluginObservers( $newPlugin );
  296. }
  297. }
  298. }
  299. }
  300. /**
  301. * Loads the plugin filename and instantiates the plugin with the given name, eg. UserCountry
  302. * Do NOT give the class name ie. Piwik_UserCountry, but give the plugin name ie. UserCountry
  303. *
  304. * @param string $pluginName
  305. * @return Piwik_Plugin
  306. */
  307. public function loadPlugin( $pluginName )
  308. {
  309. if(isset($this->loadedPlugins[$pluginName]))
  310. {
  311. return $this->loadedPlugins[$pluginName];
  312. }
  313. $pluginFileName = $pluginName . '/' . $pluginName . '.php';
  314. $pluginClassName = 'Piwik_'.$pluginName;
  315. if( !Piwik_Common::isValidFilename($pluginName))
  316. {
  317. throw new Exception("The plugin filename '$pluginFileName' is not a valid filename");
  318. }
  319. $path = PIWIK_INCLUDE_PATH . '/plugins/' . $pluginFileName;
  320. if(!file_exists($path))
  321. {
  322. throw new Exception("Unable to load plugin '$pluginName' because '$path' couldn't be found.
  323. You can manually uninstall the plugin by removing the line <code>Plugins[] = $pluginName</code> from the Piwik config file.");
  324. }
  325. // Don't remove this.
  326. // Our autoloader can't find plugins/PluginName/PluginName.php
  327. require_once $path; // prefixed by PIWIK_INCLUDE_PATH
  328. if(!class_exists($pluginClassName, false))
  329. {
  330. throw new Exception("The class $pluginClassName couldn't be found in the file '$path'");
  331. }
  332. $newPlugin = new $pluginClassName();
  333. if(!($newPlugin instanceof Piwik_Plugin))
  334. {
  335. throw new Exception("The plugin $pluginClassName in the file $path must inherit from Piwik_Plugin.");
  336. }
  337. $this->addLoadedPlugin( $pluginName, $newPlugin);
  338. return $newPlugin;
  339. }
  340. /**
  341. * Unload plugin
  342. *
  343. * @param Piwik_Plugin $plugin
  344. */
  345. public function unloadPlugin( $plugin )
  346. {
  347. if(!($plugin instanceof Piwik_Plugin ))
  348. {
  349. $plugin = $this->loadPlugin( $plugin );
  350. }
  351. $hooks = $plugin->getListHooksRegistered();
  352. foreach($hooks as $hookName => $methodToCall)
  353. {
  354. $success = $this->dispatcher->removeObserver( array( $plugin, $methodToCall), $hookName );
  355. if($success !== true)
  356. {
  357. throw new Exception("Error unloading plugin = ".$plugin->getPluginName() . ", method = $methodToCall, hook = $hookName ");
  358. }
  359. }
  360. unset($this->loadedPlugins[$plugin->getPluginName()]);
  361. }
  362. /**
  363. * Unload all loaded plugins
  364. */
  365. public function unloadPlugins()
  366. {
  367. $pluginsLoaded = $this->getLoadedPlugins();
  368. foreach($pluginsLoaded as $plugin)
  369. {
  370. $this->unloadPlugin($plugin);
  371. }
  372. }
  373. /**
  374. * Install loaded plugins
  375. */
  376. private function installPlugins()
  377. {
  378. foreach($this->getLoadedPlugins() as $plugin)
  379. {
  380. $this->installPlugin($plugin);
  381. }
  382. }
  383. /**
  384. * Install a specific plugin
  385. *
  386. * @param Piwik_Plugin $plugin
  387. * @throws Exception if installation fails
  388. */
  389. private function installPlugin( Piwik_Plugin $plugin )
  390. {
  391. try{
  392. $plugin->install();
  393. } catch(Exception $e) {
  394. throw new Piwik_PluginsManager_PluginException($plugin->getPluginName(), $e->getMessage()); }
  395. }
  396. /**
  397. * For the given plugin, add all the observers of this plugin.
  398. *
  399. * @param Piwik_Plugin $plugin
  400. */
  401. private function addPluginObservers( Piwik_Plugin $plugin )
  402. {
  403. $hooks = $plugin->getListHooksRegistered();
  404. foreach($hooks as $hookName => $methodToCall)
  405. {
  406. $this->dispatcher->addObserver( array( $plugin, $methodToCall), $hookName );
  407. }
  408. }
  409. /**
  410. * Add a plugin in the loaded plugins array
  411. *
  412. * @param string $pluginName plugin name without prefix (eg. 'UserCountry')
  413. * @param Piwik_Plugin $newPlugin
  414. */
  415. private function addLoadedPlugin( $pluginName, Piwik_Plugin $newPlugin )
  416. {
  417. $this->loadedPlugins[$pluginName] = $newPlugin;
  418. }
  419. /**
  420. * Load translation
  421. *
  422. * @param Piwik_Plugin $plugin
  423. * @param string $langCode
  424. */
  425. private function loadTranslation( $plugin, $langCode )
  426. {
  427. // we are in Tracker mode if Piwik_Loader is not (yet) loaded
  428. if(!class_exists('Piwik_Loader', false))
  429. {
  430. return ;
  431. }
  432. $infos = $plugin->getInformation();
  433. if(!isset($infos['translationAvailable']))
  434. {
  435. $infos['translationAvailable'] = false;
  436. }
  437. $translationAvailable = $infos['translationAvailable'];
  438. if(!$translationAvailable)
  439. {
  440. return;
  441. }
  442. $pluginName = $plugin->getPluginName();
  443. $path = PIWIK_INCLUDE_PATH . '/plugins/' . $pluginName .'/lang/%s.php';
  444. $defaultLangPath = sprintf($path, $langCode);
  445. $defaultEnglishLangPath = sprintf($path, 'en');
  446. $translations = array();
  447. if(file_exists($defaultLangPath))
  448. {
  449. require $defaultLangPath;
  450. }
  451. elseif(file_exists($defaultEnglishLangPath))
  452. {
  453. require $defaultEnglishLangPath;
  454. }
  455. else
  456. {
  457. throw new Exception("Language file not found for the plugin '$pluginName'.");
  458. }
  459. Piwik_Translate::getInstance()->mergeTranslationArray($translations);
  460. }
  461. /**
  462. * Return names of installed plugins
  463. *
  464. * @return array
  465. */
  466. public function getInstalledPluginsName()
  467. {
  468. if(!class_exists('Zend_Registry', false))
  469. {
  470. throw new Exception("Not possible to list installed plugins (case Tracker module)");
  471. }
  472. $pluginNames = Zend_Registry::get('config')->PluginsInstalled->PluginsInstalled->toArray();
  473. return $pluginNames;
  474. }
  475. /**
  476. * Install a plugin, if necessary
  477. *
  478. * @param Piwik_Plugin $plugin
  479. */
  480. private function installPluginIfNecessary( Piwik_Plugin $plugin )
  481. {
  482. $pluginName = $plugin->getPluginName();
  483. // is the plugin already installed or is it the first time we activate it?
  484. $pluginsInstalled = $this->getInstalledPluginsName();
  485. if(!in_array($pluginName,$pluginsInstalled))
  486. {
  487. $this->installPlugin($plugin);
  488. $pluginsInstalled[] = $pluginName;
  489. Zend_Registry::get('config')->PluginsInstalled = array('PluginsInstalled' => $pluginsInstalled);
  490. }
  491. $information = $plugin->getInformation();
  492. // if the plugin is to be loaded during the statistics logging
  493. if(isset($information['TrackerPlugin'])
  494. && $information['TrackerPlugin'] === true)
  495. {
  496. $pluginsTracker = Zend_Registry::get('config')->Plugins_Tracker->Plugins_Tracker;
  497. if(is_null($pluginsTracker))
  498. {
  499. $pluginsTracker = array();
  500. }
  501. else
  502. {
  503. $pluginsTracker = $pluginsTracker->toArray();
  504. }
  505. if(!in_array($pluginName, $pluginsTracker))
  506. {
  507. $pluginsTracker[] = $pluginName;
  508. Zend_Registry::get('config')->Plugins_Tracker = array('Plugins_Tracker' => $pluginsTracker);
  509. }
  510. }
  511. }
  512. }
  513. /**
  514. * @package Piwik
  515. * @subpackage Piwik_PluginsManager
  516. */
  517. class Piwik_PluginsManager_PluginException extends Exception
  518. {
  519. function __construct($pluginName, $message)
  520. {
  521. parent::__construct("There was a problem installing the plugin ". $pluginName . ": " . $message. "
  522. If this plugin has already been installed, and if you want to hide this message</b>, you must add the following line under the
  523. [PluginsInstalled]
  524. entry in your config/config.ini.php file:
  525. PluginsInstalled[] = $pluginName" );
  526. }
  527. }
  528. /**
  529. * Post an event to the dispatcher which will notice the observers
  530. *
  531. * @param string $eventName The event name
  532. * @param mixed $object Object, array or string that the listeners can read and/or modify.
  533. * Listeners can call $object =& $notification->getNotificationObject(); to fetch and then modify this variable.
  534. * @param array $info Additional array of data that can be used by the listeners, but not edited
  535. * @param bool $pending Should the notification be posted to plugins that register after the notification was sent?
  536. * @return void
  537. */
  538. function Piwik_PostEvent( $eventName, &$object = null, $info = array(), $pending = false )
  539. {
  540. $notification = new Piwik_Event_Notification($object, $eventName, $info);
  541. Piwik_PluginsManager::getInstance()->dispatcher->postNotification( $notification, $pending, $bubble = false );
  542. }
  543. /**
  544. * Register an action to execute for a given event
  545. *
  546. * @param string $hookName Name of event
  547. * @param function $function Callback hook
  548. */
  549. function Piwik_AddAction( $hookName, $function )
  550. {
  551. Piwik_PluginsManager::getInstance()->dispatcher->addObserver( $function, $hookName );
  552. }
  553. /**
  554. * Event notification
  555. *
  556. * @package Piwik
  557. *
  558. * @see Event_Notification, libs/Event/Notification.php
  559. * @link http://pear.php.net/package/Event_Dispatcher/docs/latest/Event_Dispatcher/Event_Notification.html
  560. */
  561. class Piwik_Event_Notification extends Event_Notification
  562. {
  563. static $showProfiler = false;
  564. /**
  565. * Use notification counter to profile runtime execution
  566. * time and memory usage.
  567. *
  568. * @param mixed $callback Callback function
  569. */
  570. function increaseNotificationCount(/* array($className|object, $method) */) {
  571. parent::increaseNotificationCount();
  572. if(self::$showProfiler && func_num_args() == 1)
  573. {
  574. $callback = func_get_arg(0);
  575. if(is_array($callback)) {
  576. $className = is_object($callback[0]) ? get_class($callback[0]) : $callback[0];
  577. $method = $callback[1];
  578. echo "after $className -> $method <br />";
  579. echo "-"; Piwik::printTimer();
  580. echo "<br />";
  581. echo "-"; Piwik::printMemoryLeak();
  582. echo "<br />";
  583. }
  584. }
  585. }
  586. }