PageRenderTime 43ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/core/Plugin.php

https://github.com/CodeYellowBV/piwik
PHP | 364 lines | 216 code | 18 blank | 130 comment | 9 complexity | 9eaa91ff15dbe77110ce48e1a7e5a65f 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;
  10. use Piwik\Plugin\Dependency;
  11. use Piwik\Plugin\MetadataLoader;
  12. /**
  13. * @see Piwik\Plugin\MetadataLoader
  14. */
  15. require_once PIWIK_INCLUDE_PATH . '/core/Plugin/MetadataLoader.php';
  16. /**
  17. * Base class of all Plugin Descriptor classes.
  18. *
  19. * Any plugin that wants to add event observers to one of Piwik's {@hook # hooks},
  20. * or has special installation/uninstallation logic must implement this class.
  21. * Plugins that can specify everything they need to in the _plugin.json_ files,
  22. * such as themes, don't need to implement this class.
  23. *
  24. * Class implementations should be named after the plugin they are a part of
  25. * (eg, `class UserCountry extends Plugin`).
  26. *
  27. * ### Plugin Metadata
  28. *
  29. * In addition to providing a place for plugins to install/uninstall themselves
  30. * and add event observers, this class is also responsible for loading metadata
  31. * found in the plugin.json file.
  32. *
  33. * The plugin.json file must exist in the root directory of a plugin. It can
  34. * contain the following information:
  35. *
  36. * - **description**: An internationalized string description of what the plugin
  37. * does.
  38. * - **homepage**: The URL to the plugin's website.
  39. * - **authors**: A list of author arrays with keys for 'name', 'email' and 'homepage'
  40. * - **license**: The license the code uses (eg, GPL, MIT, etc.).
  41. * - **license_homepage**: URL to website describing the license used.
  42. * - **version**: The plugin version (eg, 1.0.1).
  43. * - **theme**: `true` or `false`. If `true`, the plugin will be treated as a theme.
  44. *
  45. * ### Examples
  46. *
  47. * **How to extend**
  48. *
  49. * use Piwik\Common;
  50. * use Piwik\Plugin;
  51. * use Piwik\Db;
  52. *
  53. * class MyPlugin extends Plugin
  54. * {
  55. * public function getListHooksRegistered()
  56. * {
  57. * return array(
  58. * 'API.getReportMetadata' => 'getReportMetadata',
  59. * 'Another.event' => array(
  60. * 'function' => 'myOtherPluginFunction',
  61. * 'after' => true // executes this callback after others
  62. * )
  63. * );
  64. * }
  65. *
  66. * public function install()
  67. * {
  68. * Db::exec("CREATE TABLE " . Common::prefixTable('mytable') . "...");
  69. * }
  70. *
  71. * public function uninstall()
  72. * {
  73. * Db::exec("DROP TABLE IF EXISTS " . Common::prefixTable('mytable'));
  74. * }
  75. *
  76. * public function getReportMetadata(&$metadata)
  77. * {
  78. * // ...
  79. * }
  80. *
  81. * public function myOtherPluginFunction()
  82. * {
  83. * // ...
  84. * }
  85. * }
  86. *
  87. * @api
  88. */
  89. class Plugin
  90. {
  91. /**
  92. * Name of this plugin.
  93. *
  94. * @var string
  95. */
  96. protected $pluginName;
  97. /**
  98. * Holds plugin metadata.
  99. *
  100. * @var array
  101. */
  102. private $pluginInformation;
  103. /**
  104. * Constructor.
  105. *
  106. * @param string|bool $pluginName A plugin name to force. If not supplied, it is set
  107. * to the last part of the class name.
  108. * @throws \Exception If plugin metadata is defined in both the getInformation() method
  109. * and the **plugin.json** file.
  110. */
  111. public function __construct($pluginName = false)
  112. {
  113. if (empty($pluginName)) {
  114. $pluginName = explode('\\', get_class($this));
  115. $pluginName = end($pluginName);
  116. }
  117. $this->pluginName = $pluginName;
  118. $metadataLoader = new MetadataLoader($pluginName);
  119. $this->pluginInformation = $metadataLoader->load();
  120. if ($this->hasDefinedPluginInformationInPluginClass() && $metadataLoader->hasPluginJson()) {
  121. throw new \Exception('Plugin ' . $pluginName . ' has defined the method getInformation() and as well as having a plugin.json file. Please delete the getInformation() method from the plugin class. Alternatively, you may delete the plugin directory from plugins/' . $pluginName);
  122. }
  123. }
  124. private function hasDefinedPluginInformationInPluginClass()
  125. {
  126. $myClassName = get_class();
  127. $pluginClassName = get_class($this);
  128. if ($pluginClassName == $myClassName) {
  129. // plugin has not defined its own class
  130. return false;
  131. }
  132. $foo = new \ReflectionMethod(get_class($this), 'getInformation');
  133. $declaringClass = $foo->getDeclaringClass()->getName();
  134. return $declaringClass != $myClassName;
  135. }
  136. /**
  137. * Returns plugin information, including:
  138. *
  139. * - 'description' => string // 1-2 sentence description of the plugin
  140. * - 'author' => string // plugin author
  141. * - 'author_homepage' => string // author homepage URL (or email "mailto:youremail@example.org")
  142. * - 'homepage' => string // plugin homepage URL
  143. * - 'license' => string // plugin license
  144. * - 'license_homepage' => string // license homepage URL
  145. * - 'version' => string // plugin version number; examples and 3rd party plugins must not use Version::VERSION; 3rd party plugins must increment the version number with each plugin release
  146. * - 'theme' => bool // Whether this plugin is a theme (a theme is a plugin, but a plugin is not necessarily a theme)
  147. *
  148. * @return array
  149. * @deprecated
  150. */
  151. public function getInformation()
  152. {
  153. return $this->pluginInformation;
  154. }
  155. /**
  156. * Returns a list of hooks with associated event observers.
  157. *
  158. * Derived classes should use this method to associate callbacks with events.
  159. *
  160. * @return array eg,
  161. *
  162. * array(
  163. * 'API.getReportMetadata' => 'myPluginFunction',
  164. * 'Another.event' => array(
  165. * 'function' => 'myOtherPluginFunction',
  166. * 'after' => true // execute after callbacks w/o ordering
  167. * )
  168. * 'Yet.Another.event' => array(
  169. * 'function' => 'myOtherPluginFunction',
  170. * 'before' => true // execute before callbacks w/o ordering
  171. * )
  172. * )
  173. */
  174. public function getListHooksRegistered()
  175. {
  176. return array();
  177. }
  178. /**
  179. * This method is executed after a plugin is loaded and translations are registered.
  180. * Useful for initialization code that uses translated strings.
  181. */
  182. public function postLoad()
  183. {
  184. return;
  185. }
  186. /**
  187. * Installs the plugin. Derived classes should implement this class if the plugin
  188. * needs to:
  189. *
  190. * - create tables
  191. * - update existing tables
  192. * - etc.
  193. *
  194. * @throws Exception if installation of fails for some reason.
  195. */
  196. public function install()
  197. {
  198. return;
  199. }
  200. /**
  201. * Uninstalls the plugins. Derived classes should implement this method if the changes
  202. * made in {@link install()} need to be undone during uninstallation.
  203. *
  204. * In most cases, if you have an {@link install()} method, you should provide
  205. * an {@link uninstall()} method.
  206. *
  207. * @throws \Exception if uninstallation of fails for some reason.
  208. */
  209. public function uninstall()
  210. {
  211. return;
  212. }
  213. /**
  214. * Executed every time the plugin is enabled.
  215. */
  216. public function activate()
  217. {
  218. return;
  219. }
  220. /**
  221. * Executed every time the plugin is disabled.
  222. */
  223. public function deactivate()
  224. {
  225. return;
  226. }
  227. /**
  228. * Returns the plugin version number.
  229. *
  230. * @return string
  231. */
  232. final public function getVersion()
  233. {
  234. $info = $this->getInformation();
  235. return $info['version'];
  236. }
  237. /**
  238. * Returns `true` if this plugin is a theme, `false` if otherwise.
  239. *
  240. * @return bool
  241. */
  242. public function isTheme()
  243. {
  244. $info = $this->getInformation();
  245. return !empty($info['theme']) && (bool)$info['theme'];
  246. }
  247. /**
  248. * Returns the plugin's base class name without the namespace,
  249. * e.g., `"UserCountry"` when the plugin class is `"Piwik\Plugins\UserCountry\UserCountry"`.
  250. *
  251. * @return string
  252. */
  253. final public function getPluginName()
  254. {
  255. return $this->pluginName;
  256. }
  257. /**
  258. * Tries to find a component such as a Menu or Tasks within this plugin.
  259. *
  260. * @param string $componentName The name of the component you want to look for. In case you request a
  261. * component named 'Menu' it'll look for a file named 'Menu.php' within the
  262. * root of the plugin folder that implements a class named
  263. * Piwik\Plugin\$PluginName\Menu . If such a file exists but does not implement
  264. * this class it'll silently ignored.
  265. * @param string $expectedSubclass If not empty, a check will be performed whether a found file extends the
  266. * given subclass. If the requested file exists but does not extend this class
  267. * a warning will be shown to advice a developer to extend this certain class.
  268. *
  269. * @return \stdClass|null Null if the requested component does not exist or an instance of the found
  270. * component.
  271. */
  272. public function findComponent($componentName, $expectedSubclass)
  273. {
  274. $componentFile = sprintf('%s/plugins/%s/%s.php', PIWIK_INCLUDE_PATH, $this->pluginName, $componentName);
  275. if (!file_exists($componentFile)) {
  276. return;
  277. }
  278. $klassName = sprintf('Piwik\\Plugins\\%s\\%s', $this->pluginName, $componentName);
  279. if (!class_exists($klassName)) {
  280. return;
  281. }
  282. if (!empty($expectedSubclass) && !is_subclass_of($klassName, $expectedSubclass)) {
  283. Log::warning(sprintf('Cannot use component %s for plugin %s, class %s does not extend %s',
  284. $componentName, $this->pluginName, $klassName, $expectedSubclass));
  285. return;
  286. }
  287. return new $klassName;
  288. }
  289. /**
  290. * Detect whether there are any missing dependencies.
  291. *
  292. * @param null $piwikVersion Defaults to the current Piwik version
  293. * @return bool
  294. */
  295. public function hasMissingDependencies($piwikVersion = null)
  296. {
  297. $requirements = $this->getMissingDependencies($piwikVersion);
  298. return !empty($requirements);
  299. }
  300. public function getMissingDependencies($piwikVersion = null)
  301. {
  302. if (empty($this->pluginInformation['require'])) {
  303. return array();
  304. }
  305. $dependency = new Dependency();
  306. if (!is_null($piwikVersion)) {
  307. $dependency->setPiwikVersion($piwikVersion);
  308. }
  309. return $dependency->getMissingDependencies($this->pluginInformation['require']);
  310. }
  311. /**
  312. * Extracts the plugin name from a backtrace array. Returns `false` if we can't find one.
  313. *
  314. * @param array $backtrace The result of {@link debug_backtrace()} or
  315. * [Exception::getTrace()](http://www.php.net/manual/en/exception.gettrace.php).
  316. * @return string|false
  317. */
  318. public static function getPluginNameFromBacktrace($backtrace)
  319. {
  320. foreach ($backtrace as $tracepoint) {
  321. // try and discern the plugin name
  322. if (isset($tracepoint['class'])
  323. && preg_match("/Piwik\\\\Plugins\\\\([a-zA-Z_0-9]+)\\\\/", $tracepoint['class'], $matches)
  324. ) {
  325. return $matches[1];
  326. }
  327. }
  328. return false;
  329. }
  330. }