Blueprint.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. <?php
  2. /**
  3. * @package Grav\Common\Data
  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\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. use RuntimeException;
  15. use function call_user_func_array;
  16. use function count;
  17. use function function_exists;
  18. use function in_array;
  19. use function is_array;
  20. use function is_int;
  21. use function is_object;
  22. use function is_string;
  23. use function strlen;
  24. /**
  25. * Class Blueprint
  26. * @package Grav\Common\Data
  27. */
  28. class Blueprint extends BlueprintForm
  29. {
  30. /** @var string */
  31. protected $context = 'blueprints://';
  32. /** @var string|null */
  33. protected $scope;
  34. /** @var BlueprintSchema|null */
  35. protected $blueprintSchema;
  36. /** @var object|null */
  37. protected $object;
  38. /** @var array|null */
  39. protected $defaults;
  40. /** @var array */
  41. protected $handlers = [];
  42. /**
  43. * Clone blueprint.
  44. */
  45. public function __clone()
  46. {
  47. if (null !== $this->blueprintSchema) {
  48. $this->blueprintSchema = clone $this->blueprintSchema;
  49. }
  50. }
  51. /**
  52. * @param string $scope
  53. * @return void
  54. */
  55. public function setScope($scope)
  56. {
  57. $this->scope = $scope;
  58. }
  59. /**
  60. * @param object $object
  61. * @return void
  62. */
  63. public function setObject($object)
  64. {
  65. $this->object = $object;
  66. }
  67. /**
  68. * Set default values for field types.
  69. *
  70. * @param array $types
  71. * @return $this
  72. */
  73. public function setTypes(array $types)
  74. {
  75. $this->initInternals();
  76. $this->blueprintSchema->setTypes($types);
  77. return $this;
  78. }
  79. /**
  80. * @param string $name
  81. * @return array|mixed|null
  82. * @since 1.7
  83. */
  84. public function getDefaultValue(string $name)
  85. {
  86. $path = explode('.', $name);
  87. $current = $this->getDefaults();
  88. foreach ($path as $field) {
  89. if (is_object($current) && isset($current->{$field})) {
  90. $current = $current->{$field};
  91. } elseif (is_array($current) && isset($current[$field])) {
  92. $current = $current[$field];
  93. } else {
  94. return null;
  95. }
  96. }
  97. return $current;
  98. }
  99. /**
  100. * Get nested structure containing default values defined in the blueprints.
  101. *
  102. * Fields without default value are ignored in the list.
  103. *
  104. * @return array
  105. */
  106. public function getDefaults()
  107. {
  108. $this->initInternals();
  109. if (null === $this->defaults) {
  110. $this->defaults = $this->blueprintSchema->getDefaults();
  111. }
  112. return $this->defaults;
  113. }
  114. /**
  115. * Initialize blueprints with its dynamic fields.
  116. *
  117. * @return $this
  118. */
  119. public function init()
  120. {
  121. foreach ($this->dynamic as $key => $data) {
  122. // Locate field.
  123. $path = explode('/', $key);
  124. $current = &$this->items;
  125. foreach ($path as $field) {
  126. if (is_object($current)) {
  127. // Handle objects.
  128. if (!isset($current->{$field})) {
  129. $current->{$field} = [];
  130. }
  131. $current = &$current->{$field};
  132. } else {
  133. // Handle arrays and scalars.
  134. if (!is_array($current)) {
  135. $current = [$field => []];
  136. } elseif (!isset($current[$field])) {
  137. $current[$field] = [];
  138. }
  139. $current = &$current[$field];
  140. }
  141. }
  142. // Set dynamic property.
  143. foreach ($data as $property => $call) {
  144. $action = $call['action'];
  145. $method = 'dynamic' . ucfirst($action);
  146. $call['object'] = $this->object;
  147. if (isset($this->handlers[$action])) {
  148. $callable = $this->handlers[$action];
  149. $callable($current, $property, $call);
  150. } elseif (method_exists($this, $method)) {
  151. $this->{$method}($current, $property, $call);
  152. }
  153. }
  154. }
  155. return $this;
  156. }
  157. /**
  158. * Extend blueprint with another blueprint.
  159. *
  160. * @param BlueprintForm|array $extends
  161. * @param bool $append
  162. * @return $this
  163. */
  164. public function extend($extends, $append = false)
  165. {
  166. parent::extend($extends, $append);
  167. $this->deepInit($this->items);
  168. return $this;
  169. }
  170. /**
  171. * @param string $name
  172. * @param mixed $value
  173. * @param string $separator
  174. * @param bool $append
  175. * @return $this
  176. */
  177. public function embed($name, $value, $separator = '/', $append = false)
  178. {
  179. parent::embed($name, $value, $separator, $append);
  180. $this->deepInit($this->items);
  181. return $this;
  182. }
  183. /**
  184. * Merge two arrays by using blueprints.
  185. *
  186. * @param array $data1
  187. * @param array $data2
  188. * @param string|null $name Optional
  189. * @param string $separator Optional
  190. * @return array
  191. */
  192. public function mergeData(array $data1, array $data2, $name = null, $separator = '.')
  193. {
  194. $this->initInternals();
  195. return $this->blueprintSchema->mergeData($data1, $data2, $name, $separator);
  196. }
  197. /**
  198. * Process data coming from a form.
  199. *
  200. * @param array $data
  201. * @param array $toggles
  202. * @return array
  203. */
  204. public function processForm(array $data, array $toggles = [])
  205. {
  206. $this->initInternals();
  207. return $this->blueprintSchema->processForm($data, $toggles);
  208. }
  209. /**
  210. * Return data fields that do not exist in blueprints.
  211. *
  212. * @param array $data
  213. * @param string $prefix
  214. * @return array
  215. */
  216. public function extra(array $data, $prefix = '')
  217. {
  218. $this->initInternals();
  219. return $this->blueprintSchema->extra($data, $prefix);
  220. }
  221. /**
  222. * Validate data against blueprints.
  223. *
  224. * @param array $data
  225. * @param array $options
  226. * @return void
  227. * @throws RuntimeException
  228. */
  229. public function validate(array $data, array $options = [])
  230. {
  231. $this->initInternals();
  232. $this->blueprintSchema->validate($data, $options);
  233. }
  234. /**
  235. * Filter data by using blueprints.
  236. *
  237. * @param array $data
  238. * @param bool $missingValuesAsNull
  239. * @param bool $keepEmptyValues
  240. * @return array
  241. */
  242. public function filter(array $data, bool $missingValuesAsNull = false, bool $keepEmptyValues = false)
  243. {
  244. $this->initInternals();
  245. return $this->blueprintSchema->filter($data, $missingValuesAsNull, $keepEmptyValues) ?? [];
  246. }
  247. /**
  248. * Flatten data by using blueprints.
  249. *
  250. * @param array $data Data to be flattened.
  251. * @param bool $includeAll True if undefined properties should also be included.
  252. * @param string $name Property which will be flattened, useful for flattening repeating data.
  253. * @return array
  254. */
  255. public function flattenData(array $data, bool $includeAll = false, string $name = '')
  256. {
  257. $this->initInternals();
  258. return $this->blueprintSchema->flattenData($data, $includeAll, $name);
  259. }
  260. /**
  261. * Return blueprint data schema.
  262. *
  263. * @return BlueprintSchema
  264. */
  265. public function schema()
  266. {
  267. $this->initInternals();
  268. return $this->blueprintSchema;
  269. }
  270. /**
  271. * @param string $name
  272. * @param callable $callable
  273. * @return void
  274. */
  275. public function addDynamicHandler(string $name, callable $callable): void
  276. {
  277. $this->handlers[$name] = $callable;
  278. }
  279. /**
  280. * Initialize validator.
  281. *
  282. * @return void
  283. */
  284. protected function initInternals()
  285. {
  286. if (null === $this->blueprintSchema) {
  287. $types = Grav::instance()['plugins']->formFieldTypes;
  288. $this->blueprintSchema = new BlueprintSchema;
  289. if ($types) {
  290. $this->blueprintSchema->setTypes($types);
  291. }
  292. $this->blueprintSchema->embed('', $this->items);
  293. $this->blueprintSchema->init();
  294. $this->defaults = null;
  295. }
  296. }
  297. /**
  298. * @param string $filename
  299. * @return array
  300. */
  301. protected function loadFile($filename)
  302. {
  303. $file = CompiledYamlFile::instance($filename);
  304. $content = (array)$file->content();
  305. $file->free();
  306. return $content;
  307. }
  308. /**
  309. * @param string|array $path
  310. * @param string|null $context
  311. * @return array
  312. */
  313. protected function getFiles($path, $context = null)
  314. {
  315. /** @var UniformResourceLocator $locator */
  316. $locator = Grav::instance()['locator'];
  317. if (is_string($path) && !$locator->isStream($path)) {
  318. if (is_file($path)) {
  319. return [$path];
  320. }
  321. // Find path overrides.
  322. if (null === $context) {
  323. $paths = (array) ($this->overrides[$path] ?? null);
  324. } else {
  325. $paths = [];
  326. }
  327. // Add path pointing to default context.
  328. if ($context === null) {
  329. $context = $this->context;
  330. }
  331. if ($context && $context[strlen($context)-1] !== '/') {
  332. $context .= '/';
  333. }
  334. $path = $context . $path;
  335. if (!preg_match('/\.yaml$/', $path)) {
  336. $path .= '.yaml';
  337. }
  338. $paths[] = $path;
  339. } else {
  340. $paths = (array) $path;
  341. }
  342. $files = [];
  343. foreach ($paths as $lookup) {
  344. if (is_string($lookup) && strpos($lookup, '://')) {
  345. $files = array_merge($files, $locator->findResources($lookup));
  346. } else {
  347. $files[] = $lookup;
  348. }
  349. }
  350. return array_values(array_unique($files));
  351. }
  352. /**
  353. * @param array $field
  354. * @param string $property
  355. * @param array $call
  356. * @return void
  357. */
  358. protected function dynamicData(array &$field, $property, array &$call)
  359. {
  360. $params = $call['params'];
  361. if (is_array($params)) {
  362. $function = array_shift($params);
  363. } else {
  364. $function = $params;
  365. $params = [];
  366. }
  367. [$o, $f] = explode('::', $function, 2);
  368. $data = null;
  369. if (!$f) {
  370. if (function_exists($o)) {
  371. $data = call_user_func_array($o, $params);
  372. }
  373. } else {
  374. if (method_exists($o, $f)) {
  375. $data = call_user_func_array([$o, $f], $params);
  376. }
  377. }
  378. // If function returns a value,
  379. if (null !== $data) {
  380. if (is_array($data) && isset($field[$property]) && is_array($field[$property])) {
  381. // Combine field and @data-field together.
  382. $field[$property] += $data;
  383. } else {
  384. // Or create/replace field with @data-field.
  385. $field[$property] = $data;
  386. }
  387. }
  388. }
  389. /**
  390. * @param array $field
  391. * @param string $property
  392. * @param array $call
  393. * @return void
  394. */
  395. protected function dynamicConfig(array &$field, $property, array &$call)
  396. {
  397. $params = $call['params'];
  398. if (is_array($params)) {
  399. $value = array_shift($params);
  400. $params = array_shift($params);
  401. } else {
  402. $value = $params;
  403. $params = [];
  404. }
  405. $default = $field[$property] ?? null;
  406. $config = Grav::instance()['config']->get($value, $default);
  407. if (!empty($field['value_only'])) {
  408. $config = array_combine($config, $config);
  409. }
  410. if (null !== $config) {
  411. if (!empty($params['append']) && is_array($config) && isset($field[$property]) && is_array($field[$property])) {
  412. // Combine field and @config-field together.
  413. $field[$property] += $config;
  414. } else {
  415. // Or create/replace field with @config-field.
  416. $field[$property] = $config;
  417. }
  418. }
  419. }
  420. /**
  421. * @param array $field
  422. * @param string $property
  423. * @param array $call
  424. * @return void
  425. */
  426. protected function dynamicSecurity(array &$field, $property, array &$call)
  427. {
  428. if ($property || !empty($field['validate']['ignore'])) {
  429. return;
  430. }
  431. $grav = Grav::instance();
  432. $actions = (array)$call['params'];
  433. /** @var UserInterface|null $user */
  434. $user = $grav['user'] ?? null;
  435. $success = null !== $user;
  436. if ($success) {
  437. $success = $this->resolveActions($user, $actions);
  438. }
  439. if (!$success) {
  440. static::addPropertyRecursive($field, 'validate', ['ignore' => true]);
  441. }
  442. }
  443. /**
  444. * @param UserInterface|null $user
  445. * @param array $actions
  446. * @param string $op
  447. * @return bool
  448. */
  449. protected function resolveActions(?UserInterface $user, array $actions, string $op = 'and')
  450. {
  451. if (null === $user) {
  452. return false;
  453. }
  454. $c = $i = count($actions);
  455. foreach ($actions as $key => $action) {
  456. if (!is_int($key) && is_array($actions)) {
  457. $i -= $this->resolveActions($user, $action, $key);
  458. } elseif ($user->authorize($action)) {
  459. $i--;
  460. }
  461. }
  462. if ($op === 'and') {
  463. return $i === 0;
  464. }
  465. return $c !== $i;
  466. }
  467. /**
  468. * @param array $field
  469. * @param string $property
  470. * @param array $call
  471. * @return void
  472. */
  473. protected function dynamicScope(array &$field, $property, array &$call)
  474. {
  475. if ($property && $property !== 'ignore') {
  476. return;
  477. }
  478. $scopes = (array)$call['params'];
  479. $matches = in_array($this->scope, $scopes, true);
  480. if ($this->scope && $property !== 'ignore') {
  481. $matches = !$matches;
  482. }
  483. if ($matches) {
  484. static::addPropertyRecursive($field, 'validate', ['ignore' => true]);
  485. return;
  486. }
  487. }
  488. /**
  489. * @param array $field
  490. * @param string $property
  491. * @param mixed $value
  492. * @return void
  493. */
  494. public static function addPropertyRecursive(array &$field, $property, $value)
  495. {
  496. if (is_array($value) && isset($field[$property]) && is_array($field[$property])) {
  497. $field[$property] = array_merge_recursive($field[$property], $value);
  498. } else {
  499. $field[$property] = $value;
  500. }
  501. if (!empty($field['fields'])) {
  502. foreach ($field['fields'] as $key => &$child) {
  503. static::addPropertyRecursive($child, $property, $value);
  504. }
  505. }
  506. }
  507. }