Plugin.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. <?php
  2. /**
  3. * @package Grav\Common
  4. *
  5. * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common;
  9. use ArrayAccess;
  10. use Grav\Common\Data\Blueprint;
  11. use Grav\Common\Data\Data;
  12. use Grav\Common\Page\Interfaces\PageInterface;
  13. use Grav\Common\Config\Config;
  14. use LogicException;
  15. use RocketTheme\Toolbox\File\YamlFile;
  16. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  17. use Symfony\Component\EventDispatcher\EventDispatcher;
  18. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  19. use function defined;
  20. use function is_bool;
  21. use function is_string;
  22. /**
  23. * Class Plugin
  24. * @package Grav\Common
  25. */
  26. class Plugin implements EventSubscriberInterface, ArrayAccess
  27. {
  28. /** @var string */
  29. public $name;
  30. /** @var array */
  31. public $features = [];
  32. /** @var Grav */
  33. protected $grav;
  34. /** @var Config|null */
  35. protected $config;
  36. /** @var bool */
  37. protected $active = true;
  38. /** @var Blueprint|null */
  39. protected $blueprint;
  40. /**
  41. * By default assign all methods as listeners using the default priority.
  42. *
  43. * @return array
  44. */
  45. public static function getSubscribedEvents()
  46. {
  47. $methods = get_class_methods(static::class);
  48. $list = [];
  49. foreach ($methods as $method) {
  50. if (strpos($method, 'on') === 0) {
  51. $list[$method] = [$method, 0];
  52. }
  53. }
  54. return $list;
  55. }
  56. /**
  57. * Constructor.
  58. *
  59. * @param string $name
  60. * @param Grav $grav
  61. * @param Config|null $config
  62. */
  63. public function __construct($name, Grav $grav, Config $config = null)
  64. {
  65. $this->name = $name;
  66. $this->grav = $grav;
  67. if ($config) {
  68. $this->setConfig($config);
  69. }
  70. }
  71. /**
  72. * @param Config $config
  73. * @return $this
  74. */
  75. public function setConfig(Config $config)
  76. {
  77. $this->config = $config;
  78. return $this;
  79. }
  80. /**
  81. * Get configuration of the plugin.
  82. *
  83. * @return array
  84. */
  85. public function config()
  86. {
  87. return null !== $this->config ? $this->config["plugins.{$this->name}"] : [];
  88. }
  89. /**
  90. * Determine if plugin is running under the admin
  91. *
  92. * @return bool
  93. */
  94. public function isAdmin()
  95. {
  96. return Utils::isAdminPlugin();
  97. }
  98. /**
  99. * Determine if plugin is running under the CLI
  100. *
  101. * @return bool
  102. */
  103. public function isCli()
  104. {
  105. return defined('GRAV_CLI');
  106. }
  107. /**
  108. * Determine if this route is in Admin and active for the plugin
  109. *
  110. * @param string $plugin_route
  111. * @return bool
  112. */
  113. protected function isPluginActiveAdmin($plugin_route)
  114. {
  115. $active = false;
  116. /** @var Uri $uri */
  117. $uri = $this->grav['uri'];
  118. /** @var Config $config */
  119. $config = $this->config ?? $this->grav['config'];
  120. if (strpos($uri->path(), $config->get('plugins.admin.route') . '/' . $plugin_route) === false) {
  121. $active = false;
  122. } elseif (isset($uri->paths()[1]) && $uri->paths()[1] === $plugin_route) {
  123. $active = true;
  124. }
  125. return $active;
  126. }
  127. /**
  128. * @param array $events
  129. * @return void
  130. */
  131. protected function enable(array $events)
  132. {
  133. /** @var EventDispatcher $dispatcher */
  134. $dispatcher = $this->grav['events'];
  135. foreach ($events as $eventName => $params) {
  136. if (is_string($params)) {
  137. $dispatcher->addListener($eventName, [$this, $params]);
  138. } elseif (is_string($params[0])) {
  139. $dispatcher->addListener($eventName, [$this, $params[0]], $this->getPriority($params, $eventName));
  140. } else {
  141. foreach ($params as $listener) {
  142. $dispatcher->addListener($eventName, [$this, $listener[0]], $this->getPriority($listener, $eventName));
  143. }
  144. }
  145. }
  146. }
  147. /**
  148. * @param array $params
  149. * @param string $eventName
  150. * @return int
  151. */
  152. private function getPriority($params, $eventName)
  153. {
  154. $override = implode('.', ['priorities', $this->name, $eventName, $params[0]]);
  155. return $this->grav['config']->get($override) ?? $params[1] ?? 0;
  156. }
  157. /**
  158. * @param array $events
  159. * @return void
  160. */
  161. protected function disable(array $events)
  162. {
  163. /** @var EventDispatcher $dispatcher */
  164. $dispatcher = $this->grav['events'];
  165. foreach ($events as $eventName => $params) {
  166. if (is_string($params)) {
  167. $dispatcher->removeListener($eventName, [$this, $params]);
  168. } elseif (is_string($params[0])) {
  169. $dispatcher->removeListener($eventName, [$this, $params[0]]);
  170. } else {
  171. foreach ($params as $listener) {
  172. $dispatcher->removeListener($eventName, [$this, $listener[0]]);
  173. }
  174. }
  175. }
  176. }
  177. /**
  178. * Whether or not an offset exists.
  179. *
  180. * @param string $offset An offset to check for.
  181. * @return bool Returns TRUE on success or FALSE on failure.
  182. */
  183. public function offsetExists($offset)
  184. {
  185. if ($offset === 'title') {
  186. $offset = 'name';
  187. }
  188. $blueprint = $this->getBlueprint();
  189. return isset($blueprint[$offset]);
  190. }
  191. /**
  192. * Returns the value at specified offset.
  193. *
  194. * @param string $offset The offset to retrieve.
  195. * @return mixed Can return all value types.
  196. */
  197. public function offsetGet($offset)
  198. {
  199. if ($offset === 'title') {
  200. $offset = 'name';
  201. }
  202. $blueprint = $this->getBlueprint();
  203. return $blueprint[$offset] ?? null;
  204. }
  205. /**
  206. * Assigns a value to the specified offset.
  207. *
  208. * @param string $offset The offset to assign the value to.
  209. * @param mixed $value The value to set.
  210. * @throws LogicException
  211. */
  212. public function offsetSet($offset, $value)
  213. {
  214. throw new LogicException(__CLASS__ . ' blueprints cannot be modified.');
  215. }
  216. /**
  217. * Unsets an offset.
  218. *
  219. * @param string $offset The offset to unset.
  220. * @throws LogicException
  221. */
  222. public function offsetUnset($offset)
  223. {
  224. throw new LogicException(__CLASS__ . ' blueprints cannot be modified.');
  225. }
  226. /**
  227. * @return array
  228. */
  229. public function __debugInfo(): array
  230. {
  231. $array = (array)$this;
  232. unset($array["\0*\0grav"]);
  233. $array["\0*\0config"] = $this->config();
  234. return $array;
  235. }
  236. /**
  237. * This function will search a string for markdown links in a specific format. The link value can be
  238. * optionally compared against via the $internal_regex and operated on by the callback $function
  239. * provided.
  240. *
  241. * format: [plugin:myplugin_name](function_data)
  242. *
  243. * @param string $content The string to perform operations upon
  244. * @param callable $function The anonymous callback function
  245. * @param string $internal_regex Optional internal regex to extra data from
  246. * @return string
  247. */
  248. protected function parseLinks($content, $function, $internal_regex = '(.*)')
  249. {
  250. $regex = '/\[plugin:(?:' . preg_quote($this->name, '/') . ')\]\(' . $internal_regex . '\)/i';
  251. $result = preg_replace_callback($regex, $function, $content);
  252. \assert($result !== null);
  253. return $result;
  254. }
  255. /**
  256. * Merge global and page configurations.
  257. *
  258. * WARNING: This method modifies page header!
  259. *
  260. * @param PageInterface $page The page to merge the configurations with the
  261. * plugin settings.
  262. * @param mixed $deep false = shallow|true = recursive|merge = recursive+unique
  263. * @param array $params Array of additional configuration options to
  264. * merge with the plugin settings.
  265. * @param string $type Is this 'plugins' or 'themes'
  266. * @return Data
  267. */
  268. protected function mergeConfig(PageInterface $page, $deep = false, $params = [], $type = 'plugins')
  269. {
  270. /** @var Config $config */
  271. $config = $this->config ?? $this->grav['config'];
  272. $class_name = $this->name;
  273. $class_name_merged = $class_name . '.merged';
  274. $defaults = $config->get($type . '.' . $class_name, []);
  275. $page_header = $page->header();
  276. $header = [];
  277. if (!isset($page_header->{$class_name_merged}) && isset($page_header->{$class_name})) {
  278. // Get default plugin configurations and retrieve page header configuration
  279. $config = $page_header->{$class_name};
  280. if (is_bool($config)) {
  281. // Overwrite enabled option with boolean value in page header
  282. $config = ['enabled' => $config];
  283. }
  284. // Merge page header settings using deep or shallow merging technique
  285. $header = $this->mergeArrays($deep, $defaults, $config);
  286. // Create new config object and set it on the page object so it's cached for next time
  287. $page->modifyHeader($class_name_merged, new Data($header));
  288. } elseif (isset($page_header->{$class_name_merged})) {
  289. $merged = $page_header->{$class_name_merged};
  290. $header = $merged->toArray();
  291. }
  292. if (empty($header)) {
  293. $header = $defaults;
  294. }
  295. // Merge additional parameter with configuration options
  296. $header = $this->mergeArrays($deep, $header, $params);
  297. // Return configurations as a new data config class
  298. return new Data($header);
  299. }
  300. /**
  301. * Merge arrays based on deepness
  302. *
  303. * @param string|bool $deep
  304. * @param array $array1
  305. * @param array $array2
  306. * @return array
  307. */
  308. private function mergeArrays($deep, $array1, $array2)
  309. {
  310. if ($deep === 'merge') {
  311. return Utils::arrayMergeRecursiveUnique($array1, $array2);
  312. }
  313. if ($deep === true) {
  314. return array_replace_recursive($array1, $array2);
  315. }
  316. return array_merge($array1, $array2);
  317. }
  318. /**
  319. * Persists to disk the plugin parameters currently stored in the Grav Config object
  320. *
  321. * @param string $name The name of the plugin whose config it should store.
  322. * @return bool
  323. */
  324. public static function saveConfig($name)
  325. {
  326. if (!$name) {
  327. return false;
  328. }
  329. $grav = Grav::instance();
  330. /** @var UniformResourceLocator $locator */
  331. $locator = $grav['locator'];
  332. $filename = 'config://plugins/' . $name . '.yaml';
  333. $file = YamlFile::instance((string)$locator->findResource($filename, true, true));
  334. $content = $grav['config']->get('plugins.' . $name);
  335. $file->save($content);
  336. $file->free();
  337. unset($file);
  338. return true;
  339. }
  340. /**
  341. * Simpler getter for the plugin blueprint
  342. *
  343. * @return Blueprint
  344. */
  345. public function getBlueprint()
  346. {
  347. if (null === $this->blueprint) {
  348. $this->loadBlueprint();
  349. \assert($this->blueprint instanceof Blueprint);
  350. }
  351. return $this->blueprint;
  352. }
  353. /**
  354. * Load blueprints.
  355. *
  356. * @return void
  357. */
  358. protected function loadBlueprint()
  359. {
  360. if (null === $this->blueprint) {
  361. $grav = Grav::instance();
  362. /** @var Plugins $plugins */
  363. $plugins = $grav['plugins'];
  364. $data = $plugins->get($this->name);
  365. \assert($data !== null);
  366. $this->blueprint = $data->blueprints();
  367. }
  368. }
  369. }