PluginProbe ʕ •ᴥ•ʔ
Advanced Custom Fields (ACF®) / 6.8.2
Advanced Custom Fields (ACF®) v6.8.2
6.8.3 6.8.2 6.8.1 5.8.5 5.8.6 5.8.7 5.8.8 5.8.9 5.9.0 5.9.1 5.9.2 5.9.3 5.9.4 5.9.5 5.9.6 5.9.7 5.9.8 5.9.9 6.0.0 6.0.1 6.0.2 6.0.3 6.0.4 6.0.5 6.0.6 6.0.7 6.1.0 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.1.6 6.1.7 6.1.8 6.2.0 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.2.6 6.2.6.1 6.2.7 6.2.8 6.2.9 6.3.0 6.3.1 6.3.10.2 6.3.11 6.3.12 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.6.1 6.4.0 6.4.0.1 6.4.1 6.4.2 6.4.3 6.5.0 6.5.1 6.6.0 6.6.1 6.6.2 6.7.0 6.7.1 6.7.2 6.8.0 trunk 1.0.0 1.0.2 1.0.3 1.0.5 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.1.1 2.1.3 2.1.4 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.6 3.0.7 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 3.2.0 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.2.7 3.2.8 3.2.9 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.3.8 3.3.9 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.5.4 3.5.5 3.5.6 3.5.7 3.5.8 4.0.0 4.0.1 4.0.2 4.0.3 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.8 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.3.7 4.3.8 4.3.9 4.4.0 4.4.1 4.4.10 4.4.11 4.4.12 4.4.2 4.4.3 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 5.10 5.10.1 5.10.2 5.11 5.11.1 5.11.2 5.11.3 5.11.4 5.12 5.12.1 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.6.10 5.6.2 5.6.3 5.6.4 5.6.5 5.6.6 5.6.7 5.6.8 5.6.9 5.7.0 5.7.1 5.7.10 5.7.12 5.7.13 5.7.2 5.7.3 5.7.4 5.7.5 5.7.6 5.7.7 5.7.8 5.7.9 5.8.0 5.8.1 5.8.10 5.8.11 5.8.12 5.8.13 5.8.14 5.8.2 5.8.3 5.8.4
advanced-custom-fields / includes / local-json.php
advanced-custom-fields / includes Last commit date
admin 2 months ago ajax 3 weeks ago api 3 weeks ago fields 2 months ago forms 1 week ago legacy 3 months ago locations 3 months ago post-types 2 months ago rest-api 2 months ago walkers 3 months ago acf-bidirectional-functions.php 3 months ago acf-field-functions.php 2 months ago acf-field-group-functions.php 3 months ago acf-form-functions.php 3 months ago acf-helper-functions.php 3 months ago acf-hook-functions.php 3 months ago acf-input-functions.php 3 months ago acf-internal-post-type-functions.php 3 months ago acf-meta-functions.php 3 months ago acf-post-functions.php 3 months ago acf-post-type-functions.php 3 months ago acf-taxonomy-functions.php 3 months ago acf-user-functions.php 3 months ago acf-utility-functions.php 3 months ago acf-value-functions.php 3 months ago acf-wp-functions.php 3 months ago assets.php 2 months ago class-acf-data.php 3 months ago class-acf-internal-post-type.php 3 months ago compatibility.php 3 months ago deprecated.php 3 months ago fields.php 3 months ago index.php 1 year ago l10n.php 3 months ago local-fields.php 3 months ago local-json.php 3 weeks ago local-meta.php 3 months ago locations.php 3 months ago loop.php 3 months ago media.php 3 months ago rest-api.php 3 months ago revisions.php 3 weeks ago third-party.php 3 months ago upgrades.php 3 months ago validation.php 3 months ago wpml.php 3 months ago
local-json.php
681 lines
1 <?php
2 /**
3 * @package ACF
4 * @author WP Engine
5 *
6 * © 2026 Advanced Custom Fields (ACF®). All rights reserved.
7 * "ACF" is a trademark of WP Engine.
8 * Licensed under the GNU General Public License v2 or later.
9 * https://www.gnu.org/licenses/gpl-2.0.html
10 */
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit; // Exit if accessed directly
14 }
15
16 if ( ! class_exists( 'ACF_Local_JSON' ) ) :
17
18 class ACF_Local_JSON {
19
20 /**
21 * The found JSON field group files.
22 *
23 * @since 5.9.0
24 * @var array
25 */
26 private $files = array();
27
28 /**
29 * Whether an expected Local JSON write failed during the current request.
30 *
31 * @var boolean
32 */
33 private $save_file_failure = false;
34
35 /**
36 * Constructor.
37 *
38 * @date 14/4/20
39 * @since 5.9.0
40 *
41 * @param void
42 * @return void
43 */
44 public function __construct() {
45
46 // Update settings.
47 acf_update_setting( 'save_json', get_stylesheet_directory() . '/acf-json' );
48 acf_append_setting( 'load_json', get_stylesheet_directory() . '/acf-json' );
49
50 // Add listeners.
51 add_action( 'acf/update_field_group', array( $this, 'update_field_group' ) );
52 add_action( 'acf/untrash_field_group', array( $this, 'update_field_group' ) );
53 add_action( 'acf/trash_field_group', array( $this, 'delete_field_group' ) );
54 add_action( 'acf/delete_field_group', array( $this, 'delete_field_group' ) );
55 add_action( 'acf/update_post_type', array( $this, 'update_internal_post_type' ) );
56 add_action( 'acf/untrash_post_type', array( $this, 'update_internal_post_type' ) );
57 add_action( 'acf/trash_post_type', array( $this, 'delete_internal_post_type' ) );
58 add_action( 'acf/delete_post_type', array( $this, 'delete_internal_post_type' ) );
59 add_action( 'acf/update_taxonomy', array( $this, 'update_internal_post_type' ) );
60 add_action( 'acf/untrash_taxonomy', array( $this, 'update_internal_post_type' ) );
61 add_action( 'acf/trash_taxonomy', array( $this, 'delete_internal_post_type' ) );
62 add_action( 'acf/delete_taxonomy', array( $this, 'delete_internal_post_type' ) );
63
64 // Include fields.
65 add_action( 'acf/include_fields', array( $this, 'include_fields' ) );
66 add_action( 'acf/include_post_types', array( $this, 'include_post_types' ) );
67 add_action( 'acf/include_taxonomies', array( $this, 'include_taxonomies' ) );
68
69 if ( is_admin() ) {
70 add_filter( 'redirect_post_location', array( $this, 'redirect_post_location' ) );
71 add_action( 'current_screen', array( $this, 'maybe_show_save_failure_notice' ) );
72 }
73 }
74
75 /**
76 * Returns true if this component is enabled.
77 *
78 * @date 14/4/20
79 * @since 5.9.0
80 *
81 * @param void
82 * @return boolean
83 */
84 public function is_enabled() {
85 return (bool) acf_get_setting( 'json' );
86 }
87
88 /**
89 * Returns true if a Local JSON save failure has been recorded for this request.
90 *
91 * @since 6.8.1
92 *
93 * @return boolean
94 */
95 public function has_save_file_failure() {
96 return $this->save_file_failure;
97 }
98
99 /**
100 * Records a Local JSON save failure for this request.
101 *
102 * @since 6.8.1
103 *
104 * @return void
105 */
106 private function record_save_file_failure() {
107 $this->save_file_failure = true;
108 }
109
110 /**
111 * Appends a Local JSON save failure query arg to the post save redirect.
112 *
113 * @since 6.8.1
114 *
115 * @param string $location The redirect location.
116 * @return string
117 */
118 public function redirect_post_location( $location ) {
119 if ( ! $this->has_save_file_failure() ) {
120 return $location;
121 }
122
123 // Only users who can manage ACF should see ACF admin save state.
124 if ( ! current_user_can( acf_get_setting( 'capability' ) ) ) {
125 return $location;
126 }
127
128 return add_query_arg( 'acf_local_json_save_failed', 1, $location );
129 }
130
131 /**
132 * Adds an admin notice when a Local JSON save failure is present in the request.
133 *
134 * @since 6.8.1
135 *
136 * @param WP_Screen $current_screen The current WP_Screen object.
137 * @return void
138 */
139 public function maybe_show_save_failure_notice( $current_screen ) {
140 if ( ! acf_maybe_get_GET( 'acf_local_json_save_failed', false ) ) {
141 return;
142 }
143
144 if ( empty( $current_screen->post_type ) || ! in_array( $current_screen->post_type, acf_get_internal_post_types(), true ) ) {
145 return;
146 }
147
148 // Match the capability used by ACF internal post type save handlers.
149 if ( ! current_user_can( acf_get_setting( 'capability' ) ) ) {
150 return;
151 }
152
153 acf_add_admin_notice(
154 __( 'ACF saved your changes to the database, but could not update the Local JSON file. Check that the configured Local JSON save path is writable.', 'acf' ),
155 'warning'
156 );
157 }
158
159 /**
160 * Gets the path(s) to load JSON from.
161 *
162 * @since 6.2
163 *
164 * @return array
165 */
166 public function get_load_paths() {
167 $paths = (array) acf_get_setting( 'load_json' );
168
169 /**
170 * Filters the path(s) used to load JSON from.
171 *
172 * @since 6.2
173 *
174 * @param array $paths An array of potential paths to load JSON from.
175 * @return array
176 */
177 return (array) apply_filters( 'acf/json/load_paths', $paths );
178 }
179
180 /**
181 * Gets the path(s) to save JSON to.
182 *
183 * @since 6.2
184 *
185 * @param string $key The key to get paths for (optional).
186 * @param array $post The main ACF post array (optional).
187 * @return array
188 */
189 public function get_save_paths( $key = '', $post = array() ) {
190 $name = ! empty( $post['title'] ) ? (string) $post['title'] : '';
191 $post_type = acf_determine_internal_post_type( $key );
192 $paths = array();
193
194 // Paths are sorted by priority, with key overriding name, etc.
195 $paths[] = acf_get_setting( "save_json/key={$key}" );
196 $paths[] = acf_get_setting( "save_json/name={$name}" );
197 $paths[] = acf_get_setting( "save_json/type={$post_type}" );
198 $paths[] = acf_get_setting( 'save_json' );
199 $paths = array_values( array_filter( $paths ) );
200
201 /**
202 * Filters the paths used to save JSON.
203 *
204 * @since 6.2
205 *
206 * @param array $paths An array of the potential paths to save JSON to.
207 * @param array $post The ACF field group, post type, or taxonomy array.
208 * @return array
209 */
210 return (array) apply_filters( 'acf/json/save_paths', $paths, $post );
211 }
212
213 /**
214 * Writes field group data to JSON file.
215 *
216 * @date 14/4/20
217 * @since 5.9.0
218 *
219 * @param array $field_group The field group.
220 * @return void
221 */
222 public function update_field_group( $field_group ) {
223
224 // Bail early if disabled.
225 if ( ! $this->is_enabled() ) {
226 return false;
227 }
228
229 // Append fields.
230 $field_group['fields'] = acf_get_fields( $field_group );
231
232 // Save to file.
233 $this->save_file( $field_group['key'], $field_group );
234 }
235
236 /**
237 * Writes ACF posts to the JSON file.
238 *
239 * @since 6.1
240 *
241 * @param array $post The main ACF post array.
242 * @return boolean
243 */
244 public function update_internal_post_type( $post ) {
245 if ( ! $this->is_enabled() ) {
246 return false;
247 }
248
249 /**
250 * Filters the ACF post before saving it to the file.
251 *
252 * @since 6.1
253 *
254 * @param array $post The main ACF post array
255 */
256 $post = apply_filters( 'acf/pre_save_json_file', $post );
257
258 return $this->save_file( $post['key'], $post );
259 }
260
261 /**
262 * Deletes a field group JSON file.
263 *
264 * @date 14/4/20
265 * @since 5.9.0
266 *
267 * @param array $field_group The field group.
268 * @return boolean
269 */
270 public function delete_field_group( $field_group ) {
271 return $this->delete_internal_post_type( $field_group );
272 }
273
274 /**
275 * Deletes an ACF JSON file.
276 *
277 * @since 6.1
278 *
279 * @param array $post The main ACF post array.
280 * @return boolean
281 */
282 public function delete_internal_post_type( $post ) {
283 if ( ! $this->is_enabled() ) {
284 return false;
285 }
286
287 // WP appends '__trashed' to the end of 'key' (post_name).
288 $key = str_replace( '__trashed', '', $post['key'] );
289
290 return $this->delete_file( $key, $post );
291 }
292
293 /**
294 * Includes all local JSON fields.
295 *
296 * @date 14/4/20
297 * @since 5.9.0
298 *
299 * @param void
300 * @return void
301 */
302 public function include_fields() {
303
304 // Bail early if disabled.
305 if ( ! $this->is_enabled() ) {
306 return false;
307 }
308
309 // Get load paths.
310 $files = $this->scan_files( 'acf-field-group' );
311 foreach ( $files as $key => $file ) {
312 $json = json_decode( file_get_contents( $file ), true );
313 $json['local'] = 'json';
314 $json['local_file'] = $file;
315 acf_add_local_field_group( $json );
316 }
317 }
318
319 /**
320 * Includes all local JSON post types.
321 *
322 * @since 6.1
323 */
324 public function include_post_types() {
325 // Bail early if disabled.
326 if ( ! $this->is_enabled() ) {
327 return false;
328 }
329
330 // Get load paths.
331 $files = $this->scan_files( 'acf-post-type' );
332 foreach ( $files as $key => $file ) {
333 $json = json_decode( file_get_contents( $file ), true );
334 $json['local'] = 'json';
335 $json['local_file'] = $file;
336 acf_add_local_internal_post_type( $json, 'acf-post-type' );
337 }
338 }
339
340 /**
341 * Includes all local JSON taxonomies.
342 *
343 * @since 6.1
344 */
345 public function include_taxonomies() {
346 // Bail early if disabled.
347 if ( ! $this->is_enabled() ) {
348 return false;
349 }
350
351 // Get load paths.
352 $files = $this->scan_files( 'acf-taxonomy' );
353 foreach ( $files as $key => $file ) {
354 $json = json_decode( file_get_contents( $file ), true );
355 $json['local'] = 'json';
356 $json['local_file'] = $file;
357 acf_add_local_internal_post_type( $json, 'acf-taxonomy' );
358 }
359 }
360
361 /**
362 * Scans for JSON field groups.
363 *
364 * @date 14/4/20
365 * @since 5.9.0
366 *
367 * @return array
368 */
369 function scan_field_groups() {
370 return $this->scan_files( 'acf-field-group' );
371 }
372
373 /**
374 * Scans for JSON files.
375 *
376 * @since 6.1
377 *
378 * @param string $post_type The ACF post type to scan for.
379 * @return array
380 */
381 function scan_files( $post_type = 'acf-field-group' ) {
382 $json_files = array();
383
384 // Loop over "local_json" paths and parse JSON files.
385 foreach ( $this->get_load_paths() as $path ) {
386 if ( is_dir( $path ) ) {
387 $files = scandir( $path );
388 if ( $files ) {
389 foreach ( $files as $filename ) {
390
391 // Ignore hidden files.
392 if ( $filename[0] === '.' ) {
393 continue;
394 }
395
396 // Ignore sub directories.
397 $file = untrailingslashit( $path ) . '/' . $filename;
398 if ( is_dir( $file ) ) {
399 continue;
400 }
401
402 // Ignore non JSON files.
403 $ext = pathinfo( $filename, PATHINFO_EXTENSION );
404 if ( $ext !== 'json' ) {
405 continue;
406 }
407
408 // Read JSON data.
409 $json = json_decode( file_get_contents( $file ), true );
410 if ( ! is_array( $json ) || ! isset( $json['key'] ) ) {
411 continue;
412 }
413
414 // Append data.
415 $json_files[ $json['key'] ] = $file;
416 }
417 }
418 }
419 }
420
421 // Store data and return.
422 $this->files = $json_files;
423 return $this->get_files( $post_type );
424 }
425
426 /**
427 * Returns an array of found JSON files.
428 *
429 * @date 14/4/20
430 * @since 5.9.0
431 *
432 * @param string $post_type The ACF post type to get files for.
433 * @return array
434 */
435 public function get_files( $post_type = 'acf-field-group' ) {
436 $files = array();
437
438 foreach ( $this->files as $key => $path ) {
439 $internal_post_type = acf_determine_internal_post_type( $key );
440
441 if ( $internal_post_type === $post_type ) {
442 $files[ $key ] = $path;
443 } elseif ( 'acf-field-group' === $post_type ) {
444 // If we can't figure out the ACF post type, make an educated guess that it's a field group.
445 $json = json_decode( file_get_contents( $path ), true );
446 if ( ! is_array( $json ) ) {
447 continue;
448 }
449
450 if ( isset( $json['fields'] ) ) {
451 $files[ $key ] = $path;
452 }
453 }
454 }
455
456 return $files;
457 }
458
459 /**
460 * Gets the filename for an ACF JSON file.
461 *
462 * @since 6.3
463 *
464 * @param string $key The ACF post key.
465 * @param array $post The main ACF post array.
466 * @return string|boolean
467 */
468 public function get_filename( $key, $post ) {
469 $load_path = '';
470
471 if ( is_array( $this->files ) && isset( $this->files[ $key ] ) ) {
472 $load_path = $this->files[ $key ];
473 }
474
475 /**
476 * Filters the filename used when saving JSON.
477 *
478 * @since 6.2
479 *
480 * @param string $filename The default filename.
481 * @param array $post The main post array for the item being saved.
482 * @param string $load_path The path that the item was loaded from.
483 */
484 $filename = apply_filters( 'acf/json/save_file_name', $key . '.json', $post, $load_path );
485
486 if ( ! is_string( $filename ) ) {
487 return false;
488 }
489
490 $filename = sanitize_file_name( $filename );
491
492 // sanitize_file_name() can potentially remove all characters.
493 if ( empty( $filename ) ) {
494 return false;
495 }
496
497 return $filename;
498 }
499
500 /**
501 * Saves an ACF JSON file.
502 *
503 * @date 17/4/20
504 * @since 5.9.0
505 *
506 * @param string $key The ACF post key.
507 * @param array $post The main ACF post array.
508 * @return boolean
509 */
510 public function save_file( $key, $post ) {
511 $paths = $this->get_save_paths( $key, $post );
512 $filename = $this->get_filename( $key, $post );
513 $file = false;
514 $first_writable = false;
515 $has_existing_file = is_array( $this->files ) && isset( $this->files[ $key ] );
516
517 if ( ! $filename ) {
518 return false;
519 }
520
521 foreach ( $paths as $path ) {
522 if ( ! is_string( $path ) || '' === $path ) {
523 continue;
524 }
525
526 $file_to_check = trailingslashit( $path ) . $filename;
527
528 if ( is_file( $file_to_check ) ) {
529 $has_existing_file = true;
530 }
531
532 if ( ! is_writable( $path ) ) { //phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- non-compatible function for this purpose.
533 continue;
534 }
535
536 if ( false === $first_writable ) {
537 $first_writable = $path;
538 }
539
540 if ( is_file( $file_to_check ) ) {
541 $file = $file_to_check;
542 }
543 }
544
545 if ( ! $file ) {
546 if ( $first_writable ) {
547 $file = trailingslashit( $first_writable ) . $filename;
548 } else {
549 if ( $has_existing_file ) {
550 $this->record_save_file_failure();
551 }
552
553 return false;
554 }
555 }
556
557 // Make sure this is a valid ACF post type.
558 $post_type = acf_determine_internal_post_type( $key );
559 if ( ! $post_type ) {
560 return false;
561 }
562
563 // Append modified time.
564 if ( $post['ID'] ) {
565 $post['modified'] = get_post_modified_time( 'U', true, $post['ID'] );
566 } else {
567 $post['modified'] = strtotime( 'now' );
568 }
569
570 // Prepare for export and save the file.
571 $post = acf_prepare_internal_post_type_for_export( $post, $post_type );
572 $result = file_put_contents( $file, acf_json_encode( $post ) . apply_filters( 'acf/json/eof_newline', PHP_EOL ) ); //phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- potentially could run outside of admin.
573
574 if ( ! is_int( $result ) && $has_existing_file ) {
575 $this->record_save_file_failure();
576 }
577
578 // Return true if bytes were written.
579 return is_int( $result );
580 }
581
582 /**
583 * Deletes an ACF JSON file.
584 *
585 * @date 17/4/20
586 * @since 5.9.0
587 *
588 * @param string $key The ACF post key.
589 * @param array $post The main ACF post array.
590 * @return boolean
591 */
592 public function delete_file( $key, $post = array() ) {
593 $paths = $this->get_save_paths( $key, $post );
594 $filename = $this->get_filename( $key, $post );
595
596 if ( ! $filename ) {
597 return false;
598 }
599
600 foreach ( $paths as $path_to_check ) {
601 $file = untrailingslashit( $path_to_check ) . '/' . $filename;
602
603 if ( is_writable( $file ) ) { //phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- non-compatible function for this purpose.
604 wp_delete_file( $file );
605 }
606 }
607
608 return true;
609 }
610
611 /**
612 * Includes all local JSON files.
613 *
614 * @date 10/03/2014
615 * @since 5.0.0
616 * @deprecated 5.9.0
617 */
618 public function include_json_folders() {
619 _deprecated_function( __METHOD__, '5.9.0', 'ACF_Local_JSON::include_fields()' );
620 $this->include_fields();
621 }
622
623 /**
624 * Includes local JSON files within a specific folder.
625 *
626 * @date 01/05/2017
627 * @since 5.5.13
628 * @deprecated 5.9.0
629 *
630 * @param string $path The path to a specific JSON folder.
631 * @return void
632 */
633 public function include_json_folder( $path = '' ) {
634 _deprecated_function( __METHOD__, '5.9.0' );
635 // Do nothing.
636 }
637 }
638
639 // Initialize.
640 acf_new_instance( 'ACF_Local_JSON' );
641 endif; // class_exists check
642
643 /**
644 * Returns an array of found JSON field group files.
645 *
646 * @date 14/4/20
647 * @since 5.9.0
648 *
649 * @param string $post_type The ACF post type to get files for.
650 * @return array
651 */
652 function acf_get_local_json_files( $post_type = 'acf-field-group' ) {
653 return acf_get_instance( 'ACF_Local_JSON' )->get_files( $post_type );
654 }
655
656 /**
657 * Saves a field group JSON file.
658 *
659 * @date 5/12/2014
660 * @since 5.1.5
661 *
662 * @param array $field_group The field group.
663 * @return boolean
664 */
665 function acf_write_json_field_group( $field_group ) {
666 return acf_get_instance( 'ACF_Local_JSON' )->save_file( $field_group['key'], $field_group );
667 }
668
669 /**
670 * Deletes a field group JSON file.
671 *
672 * @date 5/12/2014
673 * @since 5.1.5
674 *
675 * @param string $key The field group key.
676 * @return boolean True on success.
677 */
678 function acf_delete_json_field_group( $key ) {
679 return acf_get_instance( 'ACF_Local_JSON' )->delete_file( $key );
680 }
681