Blueprint.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. <?php
  2. /**
  3. * @package Grav\Common\Data
  4. *
  5. * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common\Data;
  9. use Grav\Common\File\CompiledYamlFile;
  10. use Grav\Common\Grav;
  11. use Grav\Common\User\Interfaces\UserInterface;
  12. use RocketTheme\Toolbox\Blueprints\BlueprintForm;
  13. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  14. class Blueprint extends BlueprintForm
  15. {
  16. /** @var string */
  17. protected $context = 'blueprints://';
  18. protected $scope;
  19. /** @var BlueprintSchema */
  20. protected $blueprintSchema;
  21. /** @var array */
  22. protected $defaults;
  23. protected $handlers = [];
  24. public function __clone()
  25. {
  26. if ($this->blueprintSchema) {
  27. $this->blueprintSchema = clone $this->blueprintSchema;
  28. }
  29. }
  30. public function setScope($scope)
  31. {
  32. $this->scope = $scope;
  33. }
  34. /**
  35. * Set default values for field types.
  36. *
  37. * @param array $types
  38. * @return $this
  39. */
  40. public function setTypes(array $types)
  41. {
  42. $this->initInternals();
  43. $this->blueprintSchema->setTypes($types);
  44. return $this;
  45. }
  46. /**
  47. * Get nested structure containing default values defined in the blueprints.
  48. *
  49. * Fields without default value are ignored in the list.
  50. *
  51. * @return array
  52. */
  53. public function getDefaults()
  54. {
  55. $this->initInternals();
  56. if (null === $this->defaults) {
  57. $this->defaults = $this->blueprintSchema->getDefaults();
  58. }
  59. return $this->defaults;
  60. }
  61. /**
  62. * Initialize blueprints with its dynamic fields.
  63. *
  64. * @return $this
  65. */
  66. public function init()
  67. {
  68. foreach ($this->dynamic as $key => $data) {
  69. // Locate field.
  70. $path = explode('/', $key);
  71. $current = &$this->items;
  72. foreach ($path as $field) {
  73. if (\is_object($current)) {
  74. // Handle objects.
  75. if (!isset($current->{$field})) {
  76. $current->{$field} = [];
  77. }
  78. $current = &$current->{$field};
  79. } else {
  80. // Handle arrays and scalars.
  81. if (!\is_array($current)) {
  82. $current = [$field => []];
  83. } elseif (!isset($current[$field])) {
  84. $current[$field] = [];
  85. }
  86. $current = &$current[$field];
  87. }
  88. }
  89. // Set dynamic property.
  90. foreach ($data as $property => $call) {
  91. $action = $call['action'];
  92. $method = 'dynamic' . ucfirst($action);
  93. if (isset($this->handlers[$action])) {
  94. $callable = $this->handlers[$action];
  95. $callable($current, $property, $call);
  96. } elseif (method_exists($this, $method)) {
  97. $this->{$method}($current, $property, $call);
  98. }
  99. }
  100. }
  101. return $this;
  102. }
  103. /**
  104. * Merge two arrays by using blueprints.
  105. *
  106. * @param array $data1
  107. * @param array $data2
  108. * @param string $name Optional
  109. * @param string $separator Optional
  110. * @return array
  111. */
  112. public function mergeData(array $data1, array $data2, $name = null, $separator = '.')
  113. {
  114. $this->initInternals();
  115. return $this->blueprintSchema->mergeData($data1, $data2, $name, $separator);
  116. }
  117. /**
  118. * Process data coming from a form.
  119. *
  120. * @param array $data
  121. * @param array $toggles
  122. * @return array
  123. */
  124. public function processForm(array $data, array $toggles = [])
  125. {
  126. $this->initInternals();
  127. return $this->blueprintSchema->processForm($data, $toggles);
  128. }
  129. /**
  130. * Return data fields that do not exist in blueprints.
  131. *
  132. * @param array $data
  133. * @param string $prefix
  134. * @return array
  135. */
  136. public function extra(array $data, $prefix = '')
  137. {
  138. $this->initInternals();
  139. return $this->blueprintSchema->extra($data, $prefix);
  140. }
  141. /**
  142. * Validate data against blueprints.
  143. *
  144. * @param array $data
  145. * @throws \RuntimeException
  146. */
  147. public function validate(array $data)
  148. {
  149. $this->initInternals();
  150. $this->blueprintSchema->validate($data);
  151. }
  152. /**
  153. * Filter data by using blueprints.
  154. *
  155. * @param array $data
  156. * @param bool $missingValuesAsNull
  157. * @param bool $keepEmptyValues
  158. * @return array
  159. */
  160. public function filter(array $data, bool $missingValuesAsNull = false, bool $keepEmptyValues = false)
  161. {
  162. $this->initInternals();
  163. return $this->blueprintSchema->filter($data, $missingValuesAsNull, $keepEmptyValues);
  164. }
  165. /**
  166. * Flatten data by using blueprints.
  167. *
  168. * @param array $data
  169. * @return array
  170. */
  171. public function flattenData(array $data)
  172. {
  173. $this->initInternals();
  174. return $this->blueprintSchema->flattenData($data);
  175. }
  176. /**
  177. * Return blueprint data schema.
  178. *
  179. * @return BlueprintSchema
  180. */
  181. public function schema()
  182. {
  183. $this->initInternals();
  184. return $this->blueprintSchema;
  185. }
  186. public function addDynamicHandler(string $name, callable $callable): void
  187. {
  188. $this->handlers[$name] = $callable;
  189. }
  190. /**
  191. * Initialize validator.
  192. */
  193. protected function initInternals()
  194. {
  195. if (null === $this->blueprintSchema) {
  196. $types = Grav::instance()['plugins']->formFieldTypes;
  197. $this->blueprintSchema = new BlueprintSchema;
  198. if ($types) {
  199. $this->blueprintSchema->setTypes($types);
  200. }
  201. $this->blueprintSchema->embed('', $this->items);
  202. $this->blueprintSchema->init();
  203. $this->defaults = null;
  204. }
  205. }
  206. /**
  207. * @param string $filename
  208. * @return string
  209. */
  210. protected function loadFile($filename)
  211. {
  212. $file = CompiledYamlFile::instance($filename);
  213. $content = $file->content();
  214. $file->free();
  215. return $content;
  216. }
  217. /**
  218. * @param string|array $path
  219. * @param string $context
  220. * @return array
  221. */
  222. protected function getFiles($path, $context = null)
  223. {
  224. /** @var UniformResourceLocator $locator */
  225. $locator = Grav::instance()['locator'];
  226. if (\is_string($path) && !$locator->isStream($path)) {
  227. // Find path overrides.
  228. $paths = (array) ($this->overrides[$path] ?? null);
  229. // Add path pointing to default context.
  230. if ($context === null) {
  231. $context = $this->context;
  232. }
  233. if ($context && $context[\strlen($context)-1] !== '/') {
  234. $context .= '/';
  235. }
  236. $path = $context . $path;
  237. if (!preg_match('/\.yaml$/', $path)) {
  238. $path .= '.yaml';
  239. }
  240. $paths[] = $path;
  241. } else {
  242. $paths = (array) $path;
  243. }
  244. $files = [];
  245. foreach ($paths as $lookup) {
  246. if (\is_string($lookup) && strpos($lookup, '://')) {
  247. $files = array_merge($files, $locator->findResources($lookup));
  248. } else {
  249. $files[] = $lookup;
  250. }
  251. }
  252. return array_values(array_unique($files));
  253. }
  254. /**
  255. * @param array $field
  256. * @param string $property
  257. * @param array $call
  258. */
  259. protected function dynamicData(array &$field, $property, array &$call)
  260. {
  261. $params = $call['params'];
  262. if (\is_array($params)) {
  263. $function = array_shift($params);
  264. } else {
  265. $function = $params;
  266. $params = [];
  267. }
  268. [$o, $f] = explode('::', $function, 2);
  269. $data = null;
  270. if (!$f) {
  271. if (\function_exists($o)) {
  272. $data = \call_user_func_array($o, $params);
  273. }
  274. } else {
  275. if (method_exists($o, $f)) {
  276. $data = \call_user_func_array([$o, $f], $params);
  277. }
  278. }
  279. // If function returns a value,
  280. if (null !== $data) {
  281. if (\is_array($data) && isset($field[$property]) && \is_array($field[$property])) {
  282. // Combine field and @data-field together.
  283. $field[$property] += $data;
  284. } else {
  285. // Or create/replace field with @data-field.
  286. $field[$property] = $data;
  287. }
  288. }
  289. }
  290. /**
  291. * @param array $field
  292. * @param string $property
  293. * @param array $call
  294. */
  295. protected function dynamicConfig(array &$field, $property, array &$call)
  296. {
  297. $value = $call['params'];
  298. $default = $field[$property] ?? null;
  299. $config = Grav::instance()['config']->get($value, $default);
  300. if (null !== $config) {
  301. $field[$property] = $config;
  302. }
  303. }
  304. /**
  305. * @param array $field
  306. * @param string $property
  307. * @param array $call
  308. */
  309. protected function dynamicSecurity(array &$field, $property, array &$call)
  310. {
  311. if ($property || !empty($field['validate']['ignore'])) {
  312. return;
  313. }
  314. $grav = Grav::instance();
  315. $actions = (array)$call['params'];
  316. /** @var UserInterface|null $user */
  317. $user = $grav['user'] ?? null;
  318. foreach ($actions as $action) {
  319. if (!$user || !$user->authorize($action)) {
  320. $this->addPropertyRecursive($field, 'validate', ['ignore' => true]);
  321. return;
  322. }
  323. }
  324. }
  325. /**
  326. * @param array $field
  327. * @param string $property
  328. * @param array $call
  329. */
  330. protected function dynamicScope(array &$field, $property, array &$call)
  331. {
  332. if ($property && $property !== 'ignore') {
  333. return;
  334. }
  335. $scopes = (array)$call['params'];
  336. $matches = \in_array($this->scope, $scopes, true);
  337. if ($this->scope && $property !== 'ignore') {
  338. $matches = !$matches;
  339. }
  340. if ($matches) {
  341. $this->addPropertyRecursive($field, 'validate', ['ignore' => true]);
  342. return;
  343. }
  344. }
  345. protected function addPropertyRecursive(array &$field, $property, $value)
  346. {
  347. if (\is_array($value) && isset($field[$property]) && \is_array($field[$property])) {
  348. $field[$property] = array_merge_recursive($field[$property], $value);
  349. } else {
  350. $field[$property] = $value;
  351. }
  352. if (!empty($field['fields'])) {
  353. foreach ($field['fields'] as $key => &$child) {
  354. $this->addPropertyRecursive($child, $property, $value);
  355. }
  356. }
  357. }
  358. }