Plugin.php 13 KB

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