Teknik is a suite of services with attractive and functional interfaces. https://www.teknik.io/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Plugin.php 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  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\Cache\PersistentCache;
  11. use Piwik\Plugin\Dependency;
  12. use Piwik\Plugin\MetadataLoader;
  13. /**
  14. * @see Piwik\Plugin\MetadataLoader
  15. */
  16. require_once PIWIK_INCLUDE_PATH . '/core/Plugin/MetadataLoader.php';
  17. /**
  18. * Base class of all Plugin Descriptor classes.
  19. *
  20. * Any plugin that wants to add event observers to one of Piwik's {@hook # hooks},
  21. * or has special installation/uninstallation logic must implement this class.
  22. * Plugins that can specify everything they need to in the _plugin.json_ files,
  23. * such as themes, don't need to implement this class.
  24. *
  25. * Class implementations should be named after the plugin they are a part of
  26. * (eg, `class UserCountry extends Plugin`).
  27. *
  28. * ### Plugin Metadata
  29. *
  30. * In addition to providing a place for plugins to install/uninstall themselves
  31. * and add event observers, this class is also responsible for loading metadata
  32. * found in the plugin.json file.
  33. *
  34. * The plugin.json file must exist in the root directory of a plugin. It can
  35. * contain the following information:
  36. *
  37. * - **description**: An internationalized string description of what the plugin
  38. * does.
  39. * - **homepage**: The URL to the plugin's website.
  40. * - **authors**: A list of author arrays with keys for 'name', 'email' and 'homepage'
  41. * - **license**: The license the code uses (eg, GPL, MIT, etc.).
  42. * - **license_homepage**: URL to website describing the license used.
  43. * - **version**: The plugin version (eg, 1.0.1).
  44. * - **theme**: `true` or `false`. If `true`, the plugin will be treated as a theme.
  45. *
  46. * ### Examples
  47. *
  48. * **How to extend**
  49. *
  50. * use Piwik\Common;
  51. * use Piwik\Plugin;
  52. * use Piwik\Db;
  53. *
  54. * class MyPlugin extends Plugin
  55. * {
  56. * public function getListHooksRegistered()
  57. * {
  58. * return array(
  59. * 'API.getReportMetadata' => 'getReportMetadata',
  60. * 'Another.event' => array(
  61. * 'function' => 'myOtherPluginFunction',
  62. * 'after' => true // executes this callback after others
  63. * )
  64. * );
  65. * }
  66. *
  67. * public function install()
  68. * {
  69. * Db::exec("CREATE TABLE " . Common::prefixTable('mytable') . "...");
  70. * }
  71. *
  72. * public function uninstall()
  73. * {
  74. * Db::exec("DROP TABLE IF EXISTS " . Common::prefixTable('mytable'));
  75. * }
  76. *
  77. * public function getReportMetadata(&$metadata)
  78. * {
  79. * // ...
  80. * }
  81. *
  82. * public function myOtherPluginFunction()
  83. * {
  84. * // ...
  85. * }
  86. * }
  87. *
  88. * @api
  89. */
  90. class Plugin
  91. {
  92. /**
  93. * Name of this plugin.
  94. *
  95. * @var string
  96. */
  97. protected $pluginName;
  98. /**
  99. * Holds plugin metadata.
  100. *
  101. * @var array
  102. */
  103. private $pluginInformation;
  104. /**
  105. * As the cache is used quite often we avoid having to create instances all the time. We reuse it which is not
  106. * perfect but efficient. If the cache is used we need to make sure to call setCacheKey() before usage as there
  107. * is maybe a different key set since last usage.
  108. *
  109. * @var PersistentCache
  110. */
  111. private $cache;
  112. /**
  113. * Constructor.
  114. *
  115. * @param string|bool $pluginName A plugin name to force. If not supplied, it is set
  116. * to the last part of the class name.
  117. * @throws \Exception If plugin metadata is defined in both the getInformation() method
  118. * and the **plugin.json** file.
  119. */
  120. public function __construct($pluginName = false)
  121. {
  122. if (empty($pluginName)) {
  123. $pluginName = explode('\\', get_class($this));
  124. $pluginName = end($pluginName);
  125. }
  126. $this->pluginName = $pluginName;
  127. $metadataLoader = new MetadataLoader($pluginName);
  128. $this->pluginInformation = $metadataLoader->load();
  129. if ($this->hasDefinedPluginInformationInPluginClass() && $metadataLoader->hasPluginJson()) {
  130. 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);
  131. }
  132. $this->cache = new PersistentCache('Plugin' . $pluginName);
  133. }
  134. private function hasDefinedPluginInformationInPluginClass()
  135. {
  136. $myClassName = get_class();
  137. $pluginClassName = get_class($this);
  138. if ($pluginClassName == $myClassName) {
  139. // plugin has not defined its own class
  140. return false;
  141. }
  142. $foo = new \ReflectionMethod(get_class($this), 'getInformation');
  143. $declaringClass = $foo->getDeclaringClass()->getName();
  144. return $declaringClass != $myClassName;
  145. }
  146. /**
  147. * Returns plugin information, including:
  148. *
  149. * - 'description' => string // 1-2 sentence description of the plugin
  150. * - 'author' => string // plugin author
  151. * - 'author_homepage' => string // author homepage URL (or email "mailto:youremail@example.org")
  152. * - 'homepage' => string // plugin homepage URL
  153. * - 'license' => string // plugin license
  154. * - 'license_homepage' => string // license homepage URL
  155. * - '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
  156. * - 'theme' => bool // Whether this plugin is a theme (a theme is a plugin, but a plugin is not necessarily a theme)
  157. *
  158. * @return array
  159. * @deprecated
  160. */
  161. public function getInformation()
  162. {
  163. return $this->pluginInformation;
  164. }
  165. /**
  166. * Returns a list of hooks with associated event observers.
  167. *
  168. * Derived classes should use this method to associate callbacks with events.
  169. *
  170. * @return array eg,
  171. *
  172. * array(
  173. * 'API.getReportMetadata' => 'myPluginFunction',
  174. * 'Another.event' => array(
  175. * 'function' => 'myOtherPluginFunction',
  176. * 'after' => true // execute after callbacks w/o ordering
  177. * )
  178. * 'Yet.Another.event' => array(
  179. * 'function' => 'myOtherPluginFunction',
  180. * 'before' => true // execute before callbacks w/o ordering
  181. * )
  182. * )
  183. */
  184. public function getListHooksRegistered()
  185. {
  186. return array();
  187. }
  188. /**
  189. * This method is executed after a plugin is loaded and translations are registered.
  190. * Useful for initialization code that uses translated strings.
  191. */
  192. public function postLoad()
  193. {
  194. return;
  195. }
  196. /**
  197. * Installs the plugin. Derived classes should implement this class if the plugin
  198. * needs to:
  199. *
  200. * - create tables
  201. * - update existing tables
  202. * - etc.
  203. *
  204. * @throws \Exception if installation of fails for some reason.
  205. */
  206. public function install()
  207. {
  208. return;
  209. }
  210. /**
  211. * Uninstalls the plugins. Derived classes should implement this method if the changes
  212. * made in {@link install()} need to be undone during uninstallation.
  213. *
  214. * In most cases, if you have an {@link install()} method, you should provide
  215. * an {@link uninstall()} method.
  216. *
  217. * @throws \Exception if uninstallation of fails for some reason.
  218. */
  219. public function uninstall()
  220. {
  221. return;
  222. }
  223. /**
  224. * Executed every time the plugin is enabled.
  225. */
  226. public function activate()
  227. {
  228. return;
  229. }
  230. /**
  231. * Executed every time the plugin is disabled.
  232. */
  233. public function deactivate()
  234. {
  235. return;
  236. }
  237. /**
  238. * Returns the plugin version number.
  239. *
  240. * @return string
  241. */
  242. final public function getVersion()
  243. {
  244. $info = $this->getInformation();
  245. return $info['version'];
  246. }
  247. /**
  248. * Returns `true` if this plugin is a theme, `false` if otherwise.
  249. *
  250. * @return bool
  251. */
  252. public function isTheme()
  253. {
  254. $info = $this->getInformation();
  255. return !empty($info['theme']) && (bool)$info['theme'];
  256. }
  257. /**
  258. * Returns the plugin's base class name without the namespace,
  259. * e.g., `"UserCountry"` when the plugin class is `"Piwik\Plugins\UserCountry\UserCountry"`.
  260. *
  261. * @return string
  262. */
  263. final public function getPluginName()
  264. {
  265. return $this->pluginName;
  266. }
  267. /**
  268. * Tries to find a component such as a Menu or Tasks within this plugin.
  269. *
  270. * @param string $componentName The name of the component you want to look for. In case you request a
  271. * component named 'Menu' it'll look for a file named 'Menu.php' within the
  272. * root of the plugin folder that implements a class named
  273. * Piwik\Plugin\$PluginName\Menu . If such a file exists but does not implement
  274. * this class it'll silently ignored.
  275. * @param string $expectedSubclass If not empty, a check will be performed whether a found file extends the
  276. * given subclass. If the requested file exists but does not extend this class
  277. * a warning will be shown to advice a developer to extend this certain class.
  278. *
  279. * @return \stdClass|null Null if the requested component does not exist or an instance of the found
  280. * component.
  281. */
  282. public function findComponent($componentName, $expectedSubclass)
  283. {
  284. $this->cache->setCacheKey('Plugin' . $this->pluginName . $componentName . $expectedSubclass);
  285. $componentFile = sprintf('%s/plugins/%s/%s.php', PIWIK_INCLUDE_PATH, $this->pluginName, $componentName);
  286. if ($this->cache->has()) {
  287. $klassName = $this->cache->get();
  288. if (empty($klassName)) {
  289. return; // might by "false" in case has no menu, widget, ...
  290. }
  291. if (file_exists($componentFile)) {
  292. include_once $componentFile;
  293. }
  294. } else {
  295. $this->cache->set(false); // prevent from trying to load over and over again for instance if there is no Menu for a plugin
  296. if (!file_exists($componentFile)) {
  297. return;
  298. }
  299. require_once $componentFile;
  300. $klassName = sprintf('Piwik\\Plugins\\%s\\%s', $this->pluginName, $componentName);
  301. if (!class_exists($klassName)) {
  302. return;
  303. }
  304. if (!empty($expectedSubclass) && !is_subclass_of($klassName, $expectedSubclass)) {
  305. Log::warning(sprintf('Cannot use component %s for plugin %s, class %s does not extend %s',
  306. $componentName, $this->pluginName, $klassName, $expectedSubclass));
  307. return;
  308. }
  309. $this->cache->set($klassName);
  310. }
  311. return new $klassName;
  312. }
  313. public function findMultipleComponents($directoryWithinPlugin, $expectedSubclass)
  314. {
  315. $this->cache->setCacheKey('Plugin' . $this->pluginName . $directoryWithinPlugin . $expectedSubclass);
  316. if ($this->cache->has()) {
  317. $components = $this->cache->get();
  318. if($this->includeComponents($components)) {
  319. return $components;
  320. } else {
  321. // problem including one cached file, refresh cache
  322. }
  323. }
  324. $components = $this->doFindMultipleComponents($directoryWithinPlugin, $expectedSubclass);
  325. $this->cache->set($components);
  326. return $components;
  327. }
  328. /**
  329. * Detect whether there are any missing dependencies.
  330. *
  331. * @param null $piwikVersion Defaults to the current Piwik version
  332. * @return bool
  333. */
  334. public function hasMissingDependencies($piwikVersion = null)
  335. {
  336. $requirements = $this->getMissingDependencies($piwikVersion);
  337. return !empty($requirements);
  338. }
  339. public function getMissingDependencies($piwikVersion = null)
  340. {
  341. if (empty($this->pluginInformation['require'])) {
  342. return array();
  343. }
  344. $dependency = new Dependency();
  345. if (!is_null($piwikVersion)) {
  346. $dependency->setPiwikVersion($piwikVersion);
  347. }
  348. return $dependency->getMissingDependencies($this->pluginInformation['require']);
  349. }
  350. /**
  351. * Extracts the plugin name from a backtrace array. Returns `false` if we can't find one.
  352. *
  353. * @param array $backtrace The result of {@link debug_backtrace()} or
  354. * [Exception::getTrace()](http://www.php.net/manual/en/exception.gettrace.php).
  355. * @return string|false
  356. */
  357. public static function getPluginNameFromBacktrace($backtrace)
  358. {
  359. foreach ($backtrace as $tracepoint) {
  360. // try and discern the plugin name
  361. if (isset($tracepoint['class'])) {
  362. $className = self::getPluginNameFromNamespace($tracepoint['class']);
  363. if ($className) {
  364. return $className;
  365. }
  366. }
  367. }
  368. return false;
  369. }
  370. /**
  371. * Extracts the plugin name from a namespace name or a fully qualified class name. Returns `false`
  372. * if we can't find one.
  373. *
  374. * @param string $namespaceOrClassName The namespace or class string.
  375. * @return string|false
  376. */
  377. public static function getPluginNameFromNamespace($namespaceOrClassName)
  378. {
  379. if (preg_match("/Piwik\\\\Plugins\\\\([a-zA-Z_0-9]+)\\\\/", $namespaceOrClassName, $matches)) {
  380. return $matches[1];
  381. } else {
  382. return false;
  383. }
  384. }
  385. /**
  386. * @param $directoryWithinPlugin
  387. * @param $expectedSubclass
  388. * @return array
  389. */
  390. private function doFindMultipleComponents($directoryWithinPlugin, $expectedSubclass)
  391. {
  392. $components = array();
  393. $baseDir = PIWIK_INCLUDE_PATH . '/plugins/' . $this->pluginName . '/' . $directoryWithinPlugin;
  394. $files = Filesystem::globr($baseDir, '*.php');
  395. foreach ($files as $file) {
  396. require_once $file;
  397. $fileName = str_replace(array($baseDir . '/', '.php'), '', $file);
  398. $klassName = sprintf('Piwik\\Plugins\\%s\\%s\\%s', $this->pluginName, $directoryWithinPlugin, str_replace('/', '\\', $fileName));
  399. if (!class_exists($klassName)) {
  400. continue;
  401. }
  402. if (!empty($expectedSubclass) && !is_subclass_of($klassName, $expectedSubclass)) {
  403. continue;
  404. }
  405. $klass = new \ReflectionClass($klassName);
  406. if ($klass->isAbstract()) {
  407. continue;
  408. }
  409. $components[$file] = $klassName;
  410. }
  411. return $components;
  412. }
  413. /**
  414. * @param $components
  415. * @return bool true if all files were included, false if any file cannot be read
  416. */
  417. private function includeComponents($components)
  418. {
  419. foreach ($components as $file => $klass) {
  420. if (!is_readable($file)) {
  421. return false;
  422. }
  423. }
  424. foreach ($components as $file => $klass) {
  425. include_once $file;
  426. }
  427. return true;
  428. }
  429. }