AbstractPicoPlugin.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. <?php
  2. /**
  3. * This file is part of Pico. It's copyrighted by the contributors recorded
  4. * in the version control history of the file, available from the following
  5. * original location:
  6. *
  7. * <https://github.com/picocms/Pico/blob/master/lib/AbstractPicoPlugin.php>
  8. *
  9. * SPDX-License-Identifier: MIT
  10. * License-Filename: LICENSE
  11. */
  12. /**
  13. * Abstract class to extend from when implementing a Pico plugin
  14. *
  15. * Please refer to {@see PicoPluginInterface} for more information about how
  16. * to develop a plugin for Pico.
  17. *
  18. * @see PicoPluginInterface
  19. *
  20. * @author Daniel Rudolf
  21. * @link http://picocms.org
  22. * @license http://opensource.org/licenses/MIT The MIT License
  23. * @version 2.0
  24. */
  25. abstract class AbstractPicoPlugin implements PicoPluginInterface
  26. {
  27. /**
  28. * Current instance of Pico
  29. *
  30. * @see PicoPluginInterface::getPico()
  31. * @var Pico
  32. */
  33. protected $pico;
  34. /**
  35. * Boolean indicating if this plugin is enabled (TRUE) or disabled (FALSE)
  36. *
  37. * @see PicoPluginInterface::isEnabled()
  38. * @see PicoPluginInterface::setEnabled()
  39. * @var bool|null
  40. */
  41. protected $enabled;
  42. /**
  43. * Boolean indicating if this plugin was ever enabled/disabled manually
  44. *
  45. * @see PicoPluginInterface::isStatusChanged()
  46. * @var bool
  47. */
  48. protected $statusChanged = false;
  49. /**
  50. * Boolean indicating whether this plugin matches Pico's API version
  51. *
  52. * @see AbstractPicoPlugin::checkCompatibility()
  53. * @var bool|null
  54. */
  55. protected $nativePlugin;
  56. /**
  57. * List of plugins which this plugin depends on
  58. *
  59. * @see AbstractPicoPlugin::checkDependencies()
  60. * @see PicoPluginInterface::getDependencies()
  61. * @var string[]
  62. */
  63. protected $dependsOn = array();
  64. /**
  65. * List of plugin which depend on this plugin
  66. *
  67. * @see AbstractPicoPlugin::checkDependants()
  68. * @see PicoPluginInterface::getDependants()
  69. * @var object[]|null
  70. */
  71. private $dependants;
  72. /**
  73. * Constructs a new instance of a Pico plugin
  74. *
  75. * @param Pico $pico current instance of Pico
  76. */
  77. public function __construct(Pico $pico)
  78. {
  79. $this->pico = $pico;
  80. }
  81. /**
  82. * {@inheritDoc}
  83. */
  84. public function handleEvent($eventName, array $params)
  85. {
  86. // plugins can be enabled/disabled using the config
  87. if ($eventName === 'onConfigLoaded') {
  88. $this->configEnabled();
  89. }
  90. if ($this->isEnabled() || ($eventName === 'onPluginsLoaded')) {
  91. if (method_exists($this, $eventName)) {
  92. call_user_func_array(array($this, $eventName), $params);
  93. }
  94. }
  95. }
  96. /**
  97. * Enables or disables this plugin depending on Pico's config
  98. */
  99. protected function configEnabled()
  100. {
  101. $pluginEnabled = $this->getPico()->getConfig(get_called_class() . '.enabled');
  102. if ($pluginEnabled !== null) {
  103. $this->setEnabled($pluginEnabled);
  104. } else {
  105. $pluginEnabled = $this->getPluginConfig('enabled');
  106. if ($pluginEnabled !== null) {
  107. $this->setEnabled($pluginEnabled);
  108. } elseif ($this->enabled) {
  109. $this->setEnabled(true, true, true);
  110. } elseif ($this->enabled === null) {
  111. // make sure dependencies are already fulfilled,
  112. // otherwise the plugin needs to be enabled manually
  113. try {
  114. $this->setEnabled(true, false, true);
  115. } catch (RuntimeException $e) {
  116. $this->enabled = false;
  117. }
  118. }
  119. }
  120. }
  121. /**
  122. * {@inheritDoc}
  123. */
  124. public function setEnabled($enabled, $recursive = true, $auto = false)
  125. {
  126. $this->statusChanged = (!$this->statusChanged) ? !$auto : true;
  127. $this->enabled = (bool) $enabled;
  128. if ($enabled) {
  129. $this->checkCompatibility();
  130. $this->checkDependencies($recursive);
  131. } else {
  132. $this->checkDependants($recursive);
  133. }
  134. }
  135. /**
  136. * {@inheritDoc}
  137. */
  138. public function isEnabled()
  139. {
  140. return $this->enabled;
  141. }
  142. /**
  143. * {@inheritDoc}
  144. */
  145. public function isStatusChanged()
  146. {
  147. return $this->statusChanged;
  148. }
  149. /**
  150. * {@inheritDoc}
  151. */
  152. public function getPico()
  153. {
  154. return $this->pico;
  155. }
  156. /**
  157. * Returns either the value of the specified plugin config variable or
  158. * the config array
  159. *
  160. * @param string $configName optional name of a config variable
  161. * @param mixed $default optional default value to return when the
  162. * named config variable doesn't exist
  163. *
  164. * @return mixed if no name of a config variable has been supplied, the
  165. * plugin's config array is returned; otherwise it returns either the
  166. * value of the named config variable, or, if the named config variable
  167. * doesn't exist, the provided default value or NULL
  168. */
  169. public function getPluginConfig($configName = null, $default = null)
  170. {
  171. $pluginConfig = $this->getPico()->getConfig(get_called_class(), array());
  172. if ($configName === null) {
  173. return $pluginConfig;
  174. }
  175. return isset($pluginConfig[$configName]) ? $pluginConfig[$configName] : $default;
  176. }
  177. /**
  178. * Passes all not satisfiable method calls to Pico
  179. *
  180. * @see PicoPluginInterface::getPico()
  181. *
  182. * @deprecated 3.0.0
  183. *
  184. * @param string $methodName name of the method to call
  185. * @param array $params parameters to pass
  186. *
  187. * @return mixed return value of the called method
  188. */
  189. public function __call($methodName, array $params)
  190. {
  191. if (method_exists($this->getPico(), $methodName)) {
  192. return call_user_func_array(array($this->getPico(), $methodName), $params);
  193. }
  194. throw new BadMethodCallException(
  195. 'Call to undefined method ' . get_class($this->getPico()) . '::' . $methodName . '() '
  196. . 'through ' . get_called_class() . '::__call()'
  197. );
  198. }
  199. /**
  200. * Enables all plugins which this plugin depends on
  201. *
  202. * @see PicoPluginInterface::getDependencies()
  203. *
  204. * @param bool $recursive enable required plugins automatically
  205. *
  206. * @throws RuntimeException thrown when a dependency fails
  207. */
  208. protected function checkDependencies($recursive)
  209. {
  210. foreach ($this->getDependencies() as $pluginName) {
  211. try {
  212. $plugin = $this->getPico()->getPlugin($pluginName);
  213. } catch (RuntimeException $e) {
  214. throw new RuntimeException(
  215. "Unable to enable plugin '" . get_called_class() . "': "
  216. . "Required plugin '" . $pluginName . "' not found"
  217. );
  218. }
  219. // plugins which don't implement PicoPluginInterface are always enabled
  220. if (($plugin instanceof PicoPluginInterface) && !$plugin->isEnabled()) {
  221. if ($recursive) {
  222. if (!$plugin->isStatusChanged()) {
  223. $plugin->setEnabled(true, true, true);
  224. } else {
  225. throw new RuntimeException(
  226. "Unable to enable plugin '" . get_called_class() . "': "
  227. . "Required plugin '" . $pluginName . "' was disabled manually"
  228. );
  229. }
  230. } else {
  231. throw new RuntimeException(
  232. "Unable to enable plugin '" . get_called_class() . "': "
  233. . "Required plugin '" . $pluginName . "' is disabled"
  234. );
  235. }
  236. }
  237. }
  238. }
  239. /**
  240. * {@inheritDoc}
  241. */
  242. public function getDependencies()
  243. {
  244. return (array) $this->dependsOn;
  245. }
  246. /**
  247. * Disables all plugins which depend on this plugin
  248. *
  249. * @see PicoPluginInterface::getDependants()
  250. *
  251. * @param bool $recursive disabled dependant plugins automatically
  252. *
  253. * @throws RuntimeException thrown when a dependency fails
  254. */
  255. protected function checkDependants($recursive)
  256. {
  257. $dependants = $this->getDependants();
  258. if ($dependants) {
  259. if ($recursive) {
  260. foreach ($this->getDependants() as $pluginName => $plugin) {
  261. if ($plugin->isEnabled()) {
  262. if (!$plugin->isStatusChanged()) {
  263. $plugin->setEnabled(false, true, true);
  264. } else {
  265. throw new RuntimeException(
  266. "Unable to disable plugin '" . get_called_class() . "': "
  267. . "Required by manually enabled plugin '" . $pluginName . "'"
  268. );
  269. }
  270. }
  271. }
  272. } else {
  273. $dependantsList = 'plugin' . ((count($dependants) > 1) ? 's' : '') . ' '
  274. . "'" . implode("', '", array_keys($dependants)) . "'";
  275. throw new RuntimeException(
  276. "Unable to disable plugin '" . get_called_class() . "': "
  277. . "Required by " . $dependantsList
  278. );
  279. }
  280. }
  281. }
  282. /**
  283. * {@inheritDoc}
  284. */
  285. public function getDependants()
  286. {
  287. if ($this->dependants === null) {
  288. $this->dependants = array();
  289. foreach ($this->getPico()->getPlugins() as $pluginName => $plugin) {
  290. // only plugins which implement PicoPluginInterface support dependencies
  291. if ($plugin instanceof PicoPluginInterface) {
  292. $dependencies = $plugin->getDependencies();
  293. if (in_array(get_called_class(), $dependencies)) {
  294. $this->dependants[$pluginName] = $plugin;
  295. }
  296. }
  297. }
  298. }
  299. return $this->dependants;
  300. }
  301. /**
  302. * Checks compatibility with Pico's API version
  303. *
  304. * Pico automatically adds a dependency to {@see PicoDeprecated} when the
  305. * plugin's API is older than Pico's API. {@see PicoDeprecated} furthermore
  306. * throws a exception when it can't provide compatibility in such cases.
  307. * However, we still have to decide whether this plugin is compatible to
  308. * newer API versions, what requires some special (version specific)
  309. * precaution and is therefore usually not the case.
  310. *
  311. * @throws RuntimeException thrown when the plugin's and Pico's API aren't
  312. * compatible
  313. */
  314. protected function checkCompatibility()
  315. {
  316. if ($this->nativePlugin === null) {
  317. $picoClassName = get_class($this->pico);
  318. $picoApiVersion = defined($picoClassName . '::API_VERSION') ? $picoClassName::API_VERSION : 1;
  319. $pluginApiVersion = defined('static::API_VERSION') ? static::API_VERSION : 1;
  320. $this->nativePlugin = ($pluginApiVersion === $picoApiVersion);
  321. if (!$this->nativePlugin && ($pluginApiVersion > $picoApiVersion)) {
  322. throw new RuntimeException(
  323. "Unable to enable plugin '" . get_called_class() . "': The plugin's API (version "
  324. . $pluginApiVersion . ") isn't compatible with Pico's API (version " . $picoApiVersion . ")"
  325. );
  326. }
  327. }
  328. }
  329. }