blocks.php 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132
  1. <?php
  2. /**
  3. * The ACF Blocks PHP code.
  4. *
  5. * @package ACF
  6. */
  7. // Exit if accessed directly.
  8. defined( 'ABSPATH' ) || exit;
  9. // Register store.
  10. acf_register_store( 'block-types' );
  11. acf_register_store( 'block-cache' );
  12. // Register block.json support handlers.
  13. add_filter( 'block_type_metadata', 'acf_add_block_namespace' );
  14. add_filter( 'block_type_metadata_settings', 'acf_handle_json_block_registration', 99, 2 );
  15. add_action( 'acf_block_render_template', 'acf_block_render_template', 10, 6 );
  16. /**
  17. * Prefix block names for ACF blocks registered through block.json
  18. *
  19. * @since 6.0.0
  20. *
  21. * @param array $metadata The block metadata array.
  22. * @return array The original array with a prefixed block name if it's an ACF block.
  23. */
  24. function acf_add_block_namespace( $metadata ) {
  25. if ( acf_is_acf_block_json( $metadata ) ) {
  26. // If the block doesn't already have a namespace, append ACF's.
  27. if ( strpos( $metadata['name'], '/' ) === false ) {
  28. $metadata['name'] = 'acf/' . acf_slugify( $metadata['name'] );
  29. }
  30. }
  31. return $metadata;
  32. }
  33. /**
  34. * Handle an ACF block registered through block.json
  35. *
  36. * @since 6.0.0
  37. *
  38. * @param array $settings The compiled block settings.
  39. * @param array $metadata The raw json metadata.
  40. *
  41. * @return array Block registration settings with ACF required additions.
  42. */
  43. function acf_handle_json_block_registration( $settings, $metadata ) {
  44. if ( ! acf_is_acf_block_json( $metadata ) ) {
  45. return $settings;
  46. }
  47. // Setup ACF defaults.
  48. $settings = wp_parse_args(
  49. $settings,
  50. array(
  51. 'render_template' => false,
  52. 'render_callback' => false,
  53. 'enqueue_style' => false,
  54. 'enqueue_script' => false,
  55. 'enqueue_assets' => false,
  56. 'post_types' => array(),
  57. 'uses_context' => array(),
  58. 'supports' => array(),
  59. 'attributes' => array(),
  60. 'acf_block_version' => 2,
  61. 'api_version' => 2,
  62. )
  63. );
  64. // Add user provided attributes to ACF's required defaults.
  65. $settings['attributes'] = wp_parse_args(
  66. acf_get_block_type_default_attributes( $metadata ),
  67. $settings['attributes']
  68. );
  69. // Add default ACF 'supports' settings.
  70. $settings['supports'] = wp_parse_args(
  71. $settings['supports'],
  72. array(
  73. 'align' => true,
  74. 'html' => false,
  75. 'mode' => true,
  76. 'jsx' => true,
  77. )
  78. );
  79. // Add default ACF 'uses_context' settings.
  80. $settings['uses_context'] = array_unique(
  81. array_merge(
  82. $settings['uses_context'],
  83. array(
  84. 'postId',
  85. 'postType',
  86. )
  87. )
  88. );
  89. // Map custom ACF properties from the ACF key, with localization.
  90. $property_mappings = array(
  91. 'renderCallback' => 'render_callback',
  92. 'renderTemplate' => 'render_template',
  93. 'mode' => 'mode',
  94. 'blockVersion' => 'acf_block_version',
  95. 'postTypes' => 'post_types',
  96. );
  97. $textdomain = ! empty( $metadata['textdomain'] ) ? $metadata['textdomain'] : 'acf';
  98. $i18n_schema = get_block_metadata_i18n_schema();
  99. foreach ( $property_mappings as $key => $mapped_key ) {
  100. if ( isset( $metadata['acf'][ $key ] ) ) {
  101. unset( $settings[ $key ] );
  102. $settings[ $mapped_key ] = $metadata['acf'][ $key ];
  103. if ( $textdomain && isset( $i18n_schema->$key ) ) {
  104. $settings[ $mapped_key ] = translate_settings_using_i18n_schema( $i18n_schema->$key, $settings[ $key ], $textdomain );
  105. }
  106. }
  107. }
  108. // Add the block name and registration path to settings.
  109. $settings['name'] = $metadata['name'];
  110. $settings['path'] = dirname( $metadata['file'] );
  111. acf_get_store( 'block-types' )->set( $metadata['name'], $settings );
  112. add_action( 'enqueue_block_editor_assets', 'acf_enqueue_block_assets' );
  113. // Ensure our render callback is used.
  114. $settings['render_callback'] = 'acf_render_block_callback';
  115. return $settings;
  116. }
  117. /**
  118. * Check if a block.json block is an ACF block.
  119. *
  120. * @since 6.0.0
  121. *
  122. * @param array $metadata The raw block metadata array.
  123. * @return bool
  124. */
  125. function acf_is_acf_block_json( $metadata ) {
  126. return ( isset( $metadata['acf'] ) && $metadata['acf'] );
  127. }
  128. /**
  129. * Registers a block type.
  130. *
  131. * @date 18/2/19
  132. * @since 5.8.0
  133. *
  134. * @param array $block The block settings.
  135. * @return (array|false)
  136. */
  137. function acf_register_block_type( $block ) {
  138. // Validate block type settings.
  139. $block = acf_validate_block_type( $block );
  140. /**
  141. * Filters the arguments for registering a block type.
  142. *
  143. * @since 5.8.9
  144. *
  145. * @param array $block The array of arguments for registering a block type.
  146. */
  147. $block = apply_filters( 'acf/register_block_type_args', $block );
  148. // Require name.
  149. if ( ! $block['name'] ) {
  150. $message = __( 'Block type name is required.', 'acf' );
  151. _doing_it_wrong( __FUNCTION__, $message, '5.8.0' ); //phpcs:ignore -- escape not required.
  152. return false;
  153. }
  154. // Bail early if already exists.
  155. if ( acf_has_block_type( $block['name'] ) ) {
  156. /* translators: The name of the block type */
  157. $message = sprintf( __( 'Block type "%s" is already registered.', 'acf' ), $block['name'] );
  158. _doing_it_wrong( __FUNCTION__, $message, '5.8.0' ); //phpcs:ignore -- escape not required.
  159. return false;
  160. }
  161. // Set ACF required attributes.
  162. $block['attributes'] = acf_get_block_type_default_attributes( $block );
  163. if ( ! isset( $block['api_version'] ) ) {
  164. $block['api_version'] = 2;
  165. }
  166. if ( ! isset( $block['acf_block_version'] ) ) {
  167. $block['acf_block_version'] = 1;
  168. }
  169. // Add to storage.
  170. acf_get_store( 'block-types' )->set( $block['name'], $block );
  171. // Overwrite callback for WordPress registration.
  172. $block['render_callback'] = 'acf_render_block_callback';
  173. // Register block type in WP.
  174. if ( function_exists( 'register_block_type' ) ) {
  175. register_block_type(
  176. $block['name'],
  177. $block
  178. );
  179. }
  180. // Register action.
  181. add_action( 'enqueue_block_editor_assets', 'acf_enqueue_block_assets' );
  182. // Return block.
  183. return $block;
  184. }
  185. /**
  186. * See acf_register_block_type().
  187. *
  188. * @date 18/2/19
  189. * @since 5.7.12
  190. *
  191. * @param array $block The block settings.
  192. * @return (array|false)
  193. */
  194. function acf_register_block( $block ) {
  195. return acf_register_block_type( $block );
  196. }
  197. /**
  198. * Returns true if a block type exists for the given name.
  199. *
  200. * @since 5.7.12
  201. *
  202. * @param string $name The block type name.
  203. * @return bool
  204. */
  205. function acf_has_block_type( $name ) {
  206. return acf_get_store( 'block-types' )->has( $name );
  207. }
  208. /**
  209. * Returns an array of all registered block types.
  210. *
  211. * @since 5.7.12
  212. *
  213. * @return array
  214. */
  215. function acf_get_block_types() {
  216. return acf_get_store( 'block-types' )->get();
  217. }
  218. /**
  219. * Returns a block type for the given name.
  220. *
  221. * @since 5.7.12
  222. *
  223. * @param string $name The block type name.
  224. * @return (array|null)
  225. */
  226. function acf_get_block_type( $name ) {
  227. return acf_get_store( 'block-types' )->get( $name );
  228. }
  229. /**
  230. * Removes a block type for the given name.
  231. *
  232. * @since 5.7.12
  233. *
  234. * @param string $name The block type name.
  235. * @return void
  236. */
  237. function acf_remove_block_type( $name ) {
  238. acf_get_store( 'block-types' )->remove( $name );
  239. }
  240. /**
  241. * Returns an array of default attribute settings for a block type.
  242. *
  243. * @date 19/11/18
  244. * @since 5.8.0
  245. *
  246. * @param array $block_type A block configuration array.
  247. * @return array
  248. */
  249. function acf_get_block_type_default_attributes( $block_type ) {
  250. $attributes = array(
  251. 'name' => array(
  252. 'type' => 'string',
  253. 'default' => '',
  254. ),
  255. 'data' => array(
  256. 'type' => 'object',
  257. 'default' => array(),
  258. ),
  259. 'align' => array(
  260. 'type' => 'string',
  261. 'default' => '',
  262. ),
  263. 'mode' => array(
  264. 'type' => 'string',
  265. 'default' => '',
  266. ),
  267. );
  268. foreach ( acf_get_block_back_compat_attribute_key_array() as $new => $old ) {
  269. if ( isset( $block_type['supports'][ $old ] ) ) {
  270. $block_type['supports'][ $new ] = $block_type['supports'][ $old ];
  271. unset( $block_type['supports'][ $old ] );
  272. }
  273. }
  274. if ( ! empty( $block_type['supports']['alignText'] ) ) {
  275. $attributes['alignText'] = array(
  276. 'type' => 'string',
  277. 'default' => '',
  278. );
  279. }
  280. if ( ! empty( $block_type['supports']['alignContent'] ) ) {
  281. $attributes['alignContent'] = array(
  282. 'type' => 'string',
  283. 'default' => '',
  284. );
  285. }
  286. if ( ! empty( $block_type['supports']['fullHeight'] ) ) {
  287. $attributes['fullHeight'] = array(
  288. 'type' => 'boolean',
  289. 'default' => '',
  290. );
  291. }
  292. // For each of ACF's block attributes, check if the user's block attributes contains a default value we should use.
  293. if ( isset( $block_type['attributes'] ) && is_array( $block_type['attributes'] ) ) {
  294. foreach ( array_keys( $attributes ) as $key ) {
  295. if ( isset( $block_type['attributes'][ $key ] ) && is_array( $block_type['attributes'][ $key ] ) && isset( $block_type['attributes'][ $key ]['default'] ) ) {
  296. $attributes[ $key ]['default'] = $block_type['attributes'][ $key ]['default'];
  297. }
  298. }
  299. }
  300. return $attributes;
  301. }
  302. /**
  303. * Validates a block type ensuring all settings exist.
  304. *
  305. * @since 5.8.0
  306. *
  307. * @param array $block The block settings.
  308. * @return array
  309. */
  310. function acf_validate_block_type( $block ) {
  311. // Add default settings.
  312. $block = wp_parse_args(
  313. $block,
  314. array(
  315. 'name' => '',
  316. 'title' => '',
  317. 'description' => '',
  318. 'category' => 'common',
  319. 'icon' => '',
  320. 'mode' => 'preview',
  321. 'keywords' => array(),
  322. 'supports' => array(),
  323. 'post_types' => array(),
  324. 'uses_context' => array(),
  325. 'render_template' => false,
  326. 'render_callback' => false,
  327. 'enqueue_style' => false,
  328. 'enqueue_script' => false,
  329. 'enqueue_assets' => false,
  330. )
  331. );
  332. // Restrict keywords to 3 max to avoid JS error in older versions.
  333. if ( acf_version_compare( 'wp', '<', '5.2' ) ) {
  334. $block['keywords'] = array_slice( $block['keywords'], 0, 3 );
  335. }
  336. // Generate name with prefix.
  337. if ( $block['name'] ) {
  338. $block['name'] = 'acf/' . acf_slugify( $block['name'] );
  339. }
  340. // Add default 'supports' settings.
  341. $block['supports'] = wp_parse_args(
  342. $block['supports'],
  343. array(
  344. 'align' => true,
  345. 'html' => false,
  346. 'mode' => true,
  347. )
  348. );
  349. // Add default 'uses_context' settings.
  350. $block['uses_context'] = wp_parse_args(
  351. $block['uses_context'],
  352. array(
  353. 'postId',
  354. 'postType',
  355. )
  356. );
  357. // Correct "Experimental" flags.
  358. if ( isset( $block['supports']['__experimental_jsx'] ) ) {
  359. $block['supports']['jsx'] = $block['supports']['__experimental_jsx'];
  360. }
  361. // Return block.
  362. return $block;
  363. }
  364. /**
  365. * Prepares a block for use in render_callback by merging in all settings and attributes.
  366. *
  367. * @since 5.8.0
  368. *
  369. * @param array $block The block props.
  370. * @return array
  371. */
  372. function acf_prepare_block( $block ) {
  373. // Bail early if no name.
  374. if ( ! isset( $block['name'] ) ) {
  375. return false;
  376. }
  377. // Get block type and return false if doesn't exist.
  378. $block_type = acf_get_block_type( $block['name'] );
  379. if ( ! $block_type ) {
  380. return false;
  381. }
  382. // Generate default attributes.
  383. $attributes = array();
  384. foreach ( acf_get_block_type_default_attributes( $block_type ) as $k => $v ) {
  385. $attributes[ $k ] = $v['default'];
  386. }
  387. // Merge together arrays in order of least to most specific.
  388. $block = array_merge( $block_type, $attributes, $block );
  389. // Add backward compatibility attributes.
  390. $block = acf_add_back_compat_attributes( $block );
  391. // Return block.
  392. return $block;
  393. }
  394. /**
  395. * Add backwards compatible attribute values.
  396. *
  397. * @since 6.0.0
  398. *
  399. * @param array $block The original block.
  400. * @return array Modified block array with backwards compatibility attributes.
  401. */
  402. function acf_add_back_compat_attributes( $block ) {
  403. foreach ( acf_get_block_back_compat_attribute_key_array() as $new => $old ) {
  404. if ( ! empty( $block[ $new ] ) || ( isset( $block[ $new ] ) && ! isset( $block[ $old ] ) ) ) {
  405. $block[ $old ] = $block[ $new ];
  406. }
  407. }
  408. return $block;
  409. }
  410. /**
  411. * Get back compat new values and old values.
  412. *
  413. * @since 6.0.0
  414. *
  415. * @return array back compat key array.
  416. */
  417. function acf_get_block_back_compat_attribute_key_array() {
  418. return array(
  419. 'fullHeight' => 'full_height',
  420. 'alignText' => 'align_text',
  421. 'alignContent' => 'align_content',
  422. );
  423. }
  424. /**
  425. * The render callback for all ACF blocks.
  426. *
  427. * @date 28/10/20
  428. * @since 5.9.2
  429. *
  430. * @param array $attributes The block attributes.
  431. * @param string $content The block content.
  432. * @param WP_Block $wp_block The block instance (since WP 5.5).
  433. * @return string The block HTML.
  434. */
  435. function acf_render_block_callback( $attributes, $content = '', $wp_block = null ) {
  436. $is_preview = false;
  437. $post_id = get_the_ID();
  438. // Set preview flag to true when rendering for the block editor.
  439. if ( is_admin() && acf_is_block_editor() ) {
  440. $is_preview = true;
  441. }
  442. // Return rendered block HTML.
  443. return acf_rendered_block( $attributes, $content, $is_preview, $post_id, $wp_block );
  444. }
  445. /**
  446. * Returns the rendered block HTML.
  447. *
  448. * @date 28/2/19
  449. * @since 5.7.13
  450. *
  451. * @param array $attributes The block attributes.
  452. * @param string $content The block content.
  453. * @param bool $is_preview Whether or not the block is being rendered for editing preview.
  454. * @param int $post_id The current post being edited or viewed.
  455. * @param WP_Block $wp_block The block instance (since WP 5.5).
  456. * @param array $context The block context array.
  457. * @return string The block HTML.
  458. */
  459. function acf_rendered_block( $attributes, $content = '', $is_preview = false, $post_id = 0, $wp_block = null, $context = false ) {
  460. $mode = isset( $attributes['mode'] ) ? $attributes['mode'] : 'auto';
  461. $form = ( 'edit' === $mode && $is_preview );
  462. // If context is available from the WP_Block class object and we have no context of our own, use that.
  463. if ( empty( $context ) && ! empty( $wp_block->context ) ) {
  464. $context = $wp_block->context;
  465. }
  466. // Check if we need to generate a block ID.
  467. $attributes['id'] = acf_get_block_id( $attributes, $context );
  468. // Check if we've already got a cache of this block ID and return it to save rendering if we're in the backend.
  469. if ( $is_preview ) {
  470. $cached_block = acf_get_store( 'block-cache' )->get( $attributes['id'] );
  471. if ( $cached_block ) {
  472. if ( $form ) {
  473. if ( $cached_block['form'] ) {
  474. return $cached_block['html'];
  475. }
  476. } else {
  477. if ( ! $cached_block['form'] ) {
  478. return $cached_block['html'];
  479. }
  480. }
  481. }
  482. }
  483. ob_start();
  484. if ( $form ) {
  485. // Load the block form since we're in edit mode.
  486. // Set flag for post REST cleanup of media enqueue count during preloads.
  487. acf_set_data( 'acf_did_render_block_form', true );
  488. $block = acf_prepare_block( $attributes );
  489. acf_setup_meta( $block['data'], $block['id'], true );
  490. $fields = acf_get_block_fields( $block );
  491. if ( $fields ) {
  492. acf_prefix_fields( $fields, "acf-{$block['id']}" );
  493. echo '<div class="acf-block-fields acf-fields">';
  494. acf_render_fields( $fields, $block['id'], 'div', 'field' );
  495. echo '</div>';
  496. } else {
  497. echo acf_get_empty_block_form_html( $attributes['name'] ); //phpcs:ignore -- escaped in function.
  498. }
  499. } else {
  500. // Capture block render output.
  501. acf_render_block( $attributes, $content, $is_preview, $post_id, $wp_block, $context );
  502. }
  503. $html = ob_get_clean();
  504. // Replace <InnerBlocks /> placeholder on front-end, or if we're rendering an ACF block inside another ACF block template.
  505. if ( ! $is_preview || doing_action( 'acf_block_render_template' ) ) {
  506. // Escape "$" character to avoid "capture group" interpretation.
  507. $content = str_replace( '$', '\$', $content );
  508. // Wrap content in our acf-inner-container wrapper if necessary.
  509. if ( $wp_block && $wp_block->block_type->acf_block_version > 1 && apply_filters( 'acf/blocks/wrap_frontend_innerblocks', true, $attributes['name'] ) ) {
  510. // Check for a class (or className) provided in the template to become the InnerBlocks wrapper class.
  511. $matches = array();
  512. if ( preg_match( '/<InnerBlocks(?:[^<]+?)(?:class|className)=(?:["\']\W+\s*(?:\w+)\()?["\']([^\'"]+)[\'"]/', $html, $matches ) ) {
  513. $class = isset( $matches[1] ) ? $matches[1] : 'acf-innerblocks-container';
  514. } else {
  515. $class = 'acf-innerblocks-container';
  516. }
  517. $content = '<div class="' . $class . '">' . $content . '</div>';
  518. }
  519. $html = preg_replace( '/<InnerBlocks([\S\s]*?)\/>/', $content, $html );
  520. }
  521. // Store in cache for preloading if we're in the backend.
  522. acf_get_store( 'block-cache' )->set(
  523. $attributes['id'],
  524. array(
  525. 'form' => $form,
  526. 'html' => $html,
  527. )
  528. );
  529. // Prevent edit forms being output to rest endpoints.
  530. if ( $form && acf_get_data( 'acf_inside_rest_call' ) && apply_filters( 'acf/blocks/prevent_edit_forms_on_rest_endpoints', true ) ) {
  531. return '';
  532. }
  533. return $html;
  534. }
  535. /**
  536. * Renders the block HTML.
  537. *
  538. * @since 5.7.12
  539. *
  540. * @param array $attributes The block attributes.
  541. * @param string $content The block content.
  542. * @param bool $is_preview Whether or not the block is being rendered for editing preview.
  543. * @param int $post_id The current post being edited or viewed.
  544. * @param WP_Block $wp_block The block instance (since WP 5.5).
  545. * @param array $context The block context array.
  546. * @return void|string
  547. */
  548. function acf_render_block( $attributes, $content = '', $is_preview = false, $post_id = 0, $wp_block = null, $context = false ) {
  549. // Prepare block ensuring all settings and attributes exist.
  550. $block = acf_prepare_block( $attributes );
  551. if ( ! $block ) {
  552. return '';
  553. }
  554. // Find post_id if not defined.
  555. if ( ! $post_id ) {
  556. $post_id = get_the_ID();
  557. }
  558. // Enqueue block type assets.
  559. acf_enqueue_block_type_assets( $block );
  560. // Ensure block ID is prefixed for render.
  561. $block['id'] = acf_ensure_block_id_prefix( $block['id'] );
  562. // Setup postdata allowing get_field() to work.
  563. acf_setup_meta( $block['data'], $block['id'], true );
  564. // Call render_callback.
  565. if ( is_callable( $block['render_callback'] ) ) {
  566. call_user_func( $block['render_callback'], $block, $content, $is_preview, $post_id, $wp_block, $context );
  567. // Or include template.
  568. } elseif ( $block['render_template'] ) {
  569. do_action( 'acf_block_render_template', $block, $content, $is_preview, $post_id, $wp_block, $context );
  570. }
  571. // Reset postdata.
  572. acf_reset_meta( $block['id'] );
  573. }
  574. /**
  575. * Locate and include an ACF block's template.
  576. *
  577. * @since 6.0.4
  578. *
  579. * @param array $block The block props.
  580. */
  581. function acf_block_render_template( $block, $content, $is_preview, $post_id, $wp_block, $context ) {
  582. // Locate template.
  583. if ( isset( $block['path'] ) && file_exists( $block['path'] . '/' . $block['render_template'] ) ) {
  584. $path = $block['path'] . '/' . $block['render_template'];
  585. } elseif ( file_exists( $block['render_template'] ) ) {
  586. $path = $block['render_template'];
  587. } else {
  588. $path = locate_template( $block['render_template'] );
  589. }
  590. // Include template.
  591. if ( file_exists( $path ) ) {
  592. include $path;
  593. }
  594. }
  595. /**
  596. * Returns an array of all fields for the given block.
  597. *
  598. * @date 24/10/18
  599. * @since 5.8.0
  600. *
  601. * @param array $block The block props.
  602. * @return array
  603. */
  604. function acf_get_block_fields( $block ) {
  605. // Vars.
  606. $fields = array();
  607. // Get field groups for this block.
  608. $field_groups = acf_get_field_groups(
  609. array(
  610. 'block' => $block['name'],
  611. )
  612. );
  613. // Loop over results and append fields.
  614. if ( $field_groups ) {
  615. foreach ( $field_groups as $field_group ) {
  616. $fields = array_merge( $fields, acf_get_fields( $field_group ) );
  617. }
  618. }
  619. // Return fields.
  620. return $fields;
  621. }
  622. /**
  623. * Enqueues and localizes block scripts and styles.
  624. *
  625. * @since 5.7.13
  626. *
  627. * @return void
  628. */
  629. function acf_enqueue_block_assets() {
  630. // Localize text.
  631. acf_localize_text(
  632. array(
  633. 'Switch to Edit' => __( 'Switch to Edit', 'acf' ),
  634. 'Switch to Preview' => __( 'Switch to Preview', 'acf' ),
  635. 'Change content alignment' => __( 'Change content alignment', 'acf' ),
  636. /* translators: %s: Block type title */
  637. '%s settings' => __( '%s settings', 'acf' ),
  638. )
  639. );
  640. // Get block types.
  641. $block_types = acf_get_block_types();
  642. // Localize data.
  643. acf_localize_data(
  644. array(
  645. 'blockTypes' => array_values( $block_types ),
  646. 'postType' => get_post_type(),
  647. )
  648. );
  649. // Enqueue script.
  650. $min = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
  651. if ( acf_version_compare( 'wp', '<', '5.6' ) ) {
  652. $blocks_js_path = acf_get_url( "assets/build/js/pro/acf-pro-blocks-legacy{$min}.js" );
  653. } else {
  654. $blocks_js_path = acf_get_url( "assets/build/js/pro/acf-pro-blocks{$min}.js" );
  655. }
  656. wp_enqueue_script( 'acf-blocks', $blocks_js_path, array( 'acf-input', 'wp-blocks' ), ACF_VERSION, true );
  657. // Enqueue block assets.
  658. array_map( 'acf_enqueue_block_type_assets', $block_types );
  659. // During the edit screen loading, WordPress renders all blocks in its own attempt to preload data.
  660. // Retrieve any cached block HTML and include this in the localized data.
  661. if ( acf_get_setting( 'preload_blocks' ) ) {
  662. $preloaded_blocks = acf_get_store( 'block-cache' )->get_data();
  663. acf_localize_data(
  664. array(
  665. 'preloadedBlocks' => $preloaded_blocks,
  666. )
  667. );
  668. }
  669. }
  670. /**
  671. * Enqueues scripts and styles for a specific block type.
  672. *
  673. * @since 5.7.13
  674. *
  675. * @param array $block_type The block type settings.
  676. * @return void
  677. */
  678. function acf_enqueue_block_type_assets( $block_type ) {
  679. // Generate handle from name.
  680. $handle = 'block-' . acf_slugify( $block_type['name'] );
  681. // Enqueue style.
  682. if ( $block_type['enqueue_style'] ) {
  683. wp_enqueue_style( $handle, $block_type['enqueue_style'], array(), ACF_VERSION, 'all' );
  684. }
  685. // Enqueue script.
  686. if ( $block_type['enqueue_script'] ) {
  687. wp_enqueue_script( $handle, $block_type['enqueue_script'], array(), ACF_VERSION, true );
  688. }
  689. // Enqueue assets callback.
  690. if ( $block_type['enqueue_assets'] && is_callable( $block_type['enqueue_assets'] ) ) {
  691. call_user_func( $block_type['enqueue_assets'], $block_type );
  692. }
  693. }
  694. /**
  695. * Handles the ajax request for block data.
  696. *
  697. * @since 5.7.13
  698. *
  699. * @return void
  700. */
  701. function acf_ajax_fetch_block() {
  702. // Validate ajax request.
  703. if ( ! acf_verify_ajax() ) {
  704. wp_send_json_error();
  705. }
  706. // Get request args.
  707. $args = acf_request_args(
  708. array(
  709. 'post_id' => 0,
  710. 'clientId' => null,
  711. 'query' => array(),
  712. )
  713. );
  714. $args['block'] = isset( $_REQUEST['block'] ) ? $_REQUEST['block'] : false; //phpcs:ignore -- requires auth; designed to contain unescaped html.
  715. $args['context'] = isset( $_REQUEST['context'] ) ? $_REQUEST['context'] : array(); //phpcs:ignore -- requires auth; designed to contain unescaped html.
  716. $block = $args['block'];
  717. $query = $args['query'];
  718. $client_id = $args['clientId'];
  719. $raw_context = $args['context'];
  720. $post_id = $args['post_id'];
  721. // Bail early if no block.
  722. if ( ! $block ) {
  723. wp_send_json_error();
  724. }
  725. // Unslash and decode $_POST data for block and context.
  726. $block = wp_unslash( $block );
  727. $block = json_decode( $block, true );
  728. $context = false;
  729. if ( ! empty( $raw_context ) ) {
  730. $raw_context = wp_unslash( $raw_context );
  731. $raw_context = json_decode( $raw_context, true );
  732. if ( is_array( $raw_context ) ) {
  733. $context = $raw_context;
  734. // Check if a postId is set in the context, otherwise try and use it the default post_id.
  735. $post_id = isset( $context['postId'] ) ? intval( $context['postId'] ) : intval( $args['post_id'] );
  736. }
  737. }
  738. // Check if clientId should become $block['id'].
  739. if ( empty( $block['id'] ) && ! empty( $client_id ) ) {
  740. $block['id'] = $client_id;
  741. }
  742. // Prepare block ensuring all settings and attributes exist.
  743. $block = acf_prepare_block( $block );
  744. if ( ! $block ) {
  745. wp_send_json_error();
  746. }
  747. // Load field defaults when first previewing a block.
  748. if ( ! empty( $query['preview'] ) && ! $block['data'] ) {
  749. $fields = acf_get_block_fields( $block );
  750. foreach ( $fields as $field ) {
  751. $block['data'][ "_{$field['name']}" ] = $field['key'];
  752. }
  753. }
  754. // Setup postdata allowing form to load meta.
  755. acf_setup_meta( $block['data'], acf_ensure_block_id_prefix( $block['id'] ), true );
  756. // Setup main postdata for post_id.
  757. global $post;
  758. //phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- required for block template rendering.
  759. $post = get_post( $post_id );
  760. setup_postdata( $post );
  761. // Vars.
  762. $response = array( 'clientId' => $client_id );
  763. // Query form.
  764. if ( ! empty( $query['form'] ) ) {
  765. // Load fields for form.
  766. $fields = acf_get_block_fields( $block );
  767. // Prefix field inputs to avoid multiple blocks using the same name/id attributes.
  768. acf_prefix_fields( $fields, "acf-{$block['id']}" );
  769. if ( $fields ) {
  770. // Start Capture.
  771. ob_start();
  772. // Render.
  773. echo '<div class="acf-block-fields acf-fields">';
  774. acf_render_fields( $fields, acf_ensure_block_id_prefix( $block['id'] ), 'div', 'field' );
  775. echo '</div>';
  776. // Store Capture.
  777. $response['form'] = ob_get_clean();
  778. } else {
  779. // There are no fields on this block.
  780. $response['form'] = acf_get_empty_block_form_html( $block['name'] ); //phpcs:ignore -- escaped in function.
  781. }
  782. }
  783. // Query preview.
  784. if ( ! empty( $query['preview'] ) ) {
  785. // Render_callback vars.
  786. $content = '';
  787. $is_preview = true;
  788. // Render and store HTML.
  789. $response['preview'] = acf_rendered_block( $block, $content, $is_preview, $post_id, null, $context );
  790. }
  791. // Send response.
  792. wp_send_json_success( $response );
  793. }
  794. // Register ajax action.
  795. acf_register_ajax( 'fetch-block', 'acf_ajax_fetch_block' );
  796. /**
  797. * Render the empty block form for when a block has no fields assigned.
  798. *
  799. * @since 6.0.0
  800. *
  801. * @param string $block_name The block name current being rendered.
  802. * @return string The html that makes up a block form with no fields.
  803. */
  804. function acf_get_empty_block_form_html( $block_name ) {
  805. $message = __( 'This block contains no editable fields.', 'acf' );
  806. if ( acf_current_user_can_admin() ) {
  807. $message .= ' ';
  808. $message .= sprintf(
  809. /* translators: %s: an admin URL to the field group edit screen */
  810. __( 'Assign a <a href="%s" target="_blank">field group</a> to add fields to this block.', 'acf' ),
  811. admin_url( 'edit.php?post_type=acf-field-group' )
  812. );
  813. }
  814. $message = apply_filters( 'acf/blocks/no_fields_assigned_message', $message, $block_name );
  815. return empty( $message ) ? '' : acf_esc_html( '<div class="acf-block-fields acf-fields acf-empty-block-fields">' . $message . '</div>' );
  816. }
  817. /**
  818. * Parse content that may contain HTML block comments and saves ACF block meta.
  819. *
  820. * @since 5.7.13
  821. *
  822. * @param string $text Content that may contain HTML block comments.
  823. * @return string
  824. */
  825. function acf_parse_save_blocks( $text = '' ) {
  826. // Search text for dynamic blocks and modify attrs.
  827. return addslashes(
  828. preg_replace_callback(
  829. '/<!--\s+wp:(?P<name>[\S]+)\s+(?P<attrs>{[\S\s]+?})\s+(?P<void>\/)?-->/',
  830. 'acf_parse_save_blocks_callback',
  831. stripslashes( $text )
  832. )
  833. );
  834. }
  835. // Hook into saving process.
  836. add_filter( 'content_save_pre', 'acf_parse_save_blocks', 5, 1 );
  837. /**
  838. * Callback used in preg_replace to modify ACF Block comment.
  839. *
  840. * @since 5.7.13
  841. *
  842. * @param array $matches The preg matches.
  843. * @return string
  844. */
  845. function acf_parse_save_blocks_callback( $matches ) {
  846. // Defaults.
  847. $name = isset( $matches['name'] ) ? $matches['name'] : '';
  848. $attrs = isset( $matches['attrs'] ) ? json_decode( $matches['attrs'], true ) : '';
  849. $void = isset( $matches['void'] ) ? $matches['void'] : '';
  850. // Bail early if missing data or not an ACF Block.
  851. if ( ! $name || ! $attrs || ! acf_has_block_type( $name ) ) {
  852. return $matches[0];
  853. }
  854. // Check if we need to generate a block ID.
  855. $block_id = acf_get_block_id( $attrs );
  856. // Convert "data" to "meta".
  857. // No need to check if already in meta format. Local Meta will do this for us.
  858. if ( isset( $attrs['data'] ) ) {
  859. $attrs['data'] = acf_setup_meta( $attrs['data'], acf_ensure_block_id_prefix( $block_id ) );
  860. }
  861. /**
  862. * Filters the block attributes before saving.
  863. *
  864. * @date 18/3/19
  865. * @since 5.7.14
  866. *
  867. * @param array $attrs The block attributes.
  868. */
  869. $attrs = apply_filters( 'acf/pre_save_block', $attrs );
  870. // Gutenberg expects a specific encoding format.
  871. $attrs = acf_serialize_block_attributes( $attrs );
  872. return '<!-- wp:' . $name . ' ' . $attrs . ' ' . $void . '-->';
  873. }
  874. /**
  875. * Return or generate a block ID.
  876. *
  877. * @since 6.0.0
  878. *
  879. * @param array $attributes A block attributes array.
  880. * @param array $context The block context array, defaults to an empty array.
  881. * @return string A block ID.
  882. */
  883. function acf_get_block_id( $attributes, $context = array() ) {
  884. $attributes['_acf_context'] = $context;
  885. if ( empty( $attributes['id'] ) ) {
  886. unset( $attributes['id'] );
  887. // Remove all empty string values as they're not present in JS hash building.
  888. foreach ( $attributes as $key => $value ) {
  889. if ( '' === $value ) {
  890. unset( $attributes[ $key ] );
  891. }
  892. }
  893. // Check if data is empty and remove it if so to match JS hash building.
  894. if ( isset( $attributes['data'] ) && empty( $attributes['data'] ) ) {
  895. unset( $attributes['data'] );
  896. }
  897. ksort( $attributes );
  898. return md5( wp_json_encode( $attributes, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) );
  899. }
  900. return $attributes['id'];
  901. }
  902. /**
  903. * Ensure a block ID always has a block_ prefix for post meta internals.
  904. *
  905. * @since 6.0.0
  906. *
  907. * @param string $block_id A possibly non-prefixed block ID.
  908. * @return string A prefixed block ID.
  909. */
  910. function acf_ensure_block_id_prefix( $block_id ) {
  911. if ( substr( $block_id, 0, 6 ) === 'block_' ) {
  912. return $block_id;
  913. }
  914. return 'block_' . $block_id;
  915. }
  916. /**
  917. * This directly copied from the WordPress core `serialize_block_attributes()` function.
  918. *
  919. * We need this in order to make sure that block attributes are stored in a way that is
  920. * consistent with how Gutenberg sends them over from JS, and so that things like wp_kses()
  921. * work as expected. Copied from core to get around a bug that was fixed in 5.8.1 or on the off chance
  922. * that folks are still using WP 5.3 or below.
  923. *
  924. * TODO: Remove this when we refactor `acf_parse_save_blocks_callback()` to use `serialize_block()`,
  925. * or when we're confident that folks aren't using WP versions prior to 5.8.
  926. *
  927. * @since 5.12
  928. *
  929. * @param array $block_attributes Attributes object.
  930. * @return string Serialized attributes.
  931. */
  932. function acf_serialize_block_attributes( $block_attributes ) {
  933. $encoded_attributes = wp_json_encode( $block_attributes, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
  934. $encoded_attributes = preg_replace( '/--/', '\\u002d\\u002d', $encoded_attributes );
  935. $encoded_attributes = preg_replace( '/</', '\\u003c', $encoded_attributes );
  936. $encoded_attributes = preg_replace( '/>/', '\\u003e', $encoded_attributes );
  937. $encoded_attributes = preg_replace( '/&/', '\\u0026', $encoded_attributes );
  938. // Regex: /\\"/.
  939. $encoded_attributes = preg_replace( '/\\\\"/', '\\u0022', $encoded_attributes );
  940. return $encoded_attributes;
  941. }
  942. /**
  943. * Set ACF data before a rest call if media scripts have not been enqueued yet for after REST reset.
  944. *
  945. * @date 07/06/22
  946. * @since 6.0
  947. *
  948. * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response The WordPress response object.
  949. * @return mixed
  950. */
  951. function acf_set_after_rest_media_enqueue_reset_flag( $response ) {
  952. global $wp_actions;
  953. acf_set_data( 'acf_inside_rest_call', true );
  954. acf_set_data( 'acf_should_reset_media_enqueue', empty( $wp_actions['wp_enqueue_media'] ) );
  955. acf_set_data( 'acf_did_render_block_form', false );
  956. return $response;
  957. }
  958. add_filter( 'rest_request_before_callbacks', 'acf_set_after_rest_media_enqueue_reset_flag' );
  959. /**
  960. * Reset wp_enqueue_media action count after REST call so it can happen inside the main execution if required.
  961. *
  962. * @date 07/06/22
  963. * @since 6.0
  964. *
  965. * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response The WordPress response object.
  966. * @return mixed
  967. */
  968. function acf_reset_media_enqueue_after_rest( $response ) {
  969. acf_set_data( 'acf_inside_rest_call', false );
  970. if ( acf_get_data( 'acf_should_reset_media_enqueue' ) && acf_get_data( 'acf_did_render_block_form' ) ) {
  971. global $wp_actions;
  972. //phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- no other option here as this works around a breaking WordPress change with REST preload scopes.
  973. $wp_actions['wp_enqueue_media'] = 0;
  974. }
  975. return $response;
  976. }
  977. add_filter( 'rest_request_after_callbacks', 'acf_reset_media_enqueue_after_rest' );