PluginProbe ʕ •ᴥ•ʔ
Secure Custom Fields / trunk
Secure Custom Fields vtrunk
6.9.0 6.8.9 6.8.7 6.8.8 6.8.6 6.8.4 6.8.5 trunk 6.4.0-beta1 6.4.0-beta2 6.4.1 6.4.1-beta3 6.4.1-beta4 6.4.1-beta5 6.4.1-beta6 6.4.1-beta7 6.4.2 6.5.0 6.5.1 6.5.2 6.5.3 6.5.4 6.5.5 6.5.6 6.5.7 6.6.0 6.7.0 6.7.1 6.8.0 6.8.1 6.8.2 6.8.3
secure-custom-fields / includes / local-json.php
secure-custom-fields / includes Last commit date
Blocks 1 week ago Datastore 1 month ago Meta 1 year ago abilities 1 week ago admin 1 week ago ajax 1 month ago api 2 hours ago fields 2 hours ago forms 2 hours ago legacy 1 year ago locations 1 year ago post-types 2 months ago rest-api 1 week ago walkers 1 year ago acf-bidirectional-functions.php 1 year ago acf-field-functions.php 2 months ago acf-field-group-functions.php 7 months ago acf-form-functions.php 1 year ago acf-helper-functions.php 1 year ago acf-hook-functions.php 1 year ago acf-input-functions.php 7 months ago acf-internal-post-type-functions.php 7 months ago acf-meta-functions.php 2 weeks ago acf-post-functions.php 1 year ago acf-post-type-functions.php 1 year ago acf-taxonomy-functions.php 1 year ago acf-user-functions.php 1 week ago acf-utility-functions.php 1 year ago acf-value-functions.php 1 year ago acf-wp-functions.php 2 hours ago assets.php 1 week ago blocks-auto-inline-editing.php 2 months ago blocks.php 2 weeks ago class-acf-data.php 10 months ago class-acf-internal-post-type.php 1 week ago class-acf-options-page.php 1 year ago class-acf-site-health.php 3 months ago class-scf-json-schema-validator.php 6 months ago class-scf-schema-builder.php 2 months ago compatibility.php 1 year ago datastore.php 1 month ago deprecated.php 1 year ago fields.php 10 months ago index.php 1 year ago l10n.php 1 year ago local-fields.php 1 year ago local-json.php 1 month ago local-meta.php 1 year ago locations.php 1 year ago loop.php 10 months ago media.php 1 year ago rest-api.php 10 months ago revisions.php 1 month ago scf-ui-options-page-functions.php 1 year ago third-party.php 7 months ago upgrades.php 2 weeks ago validation.php 10 months ago wpml.php 1 year ago
local-json.php
669 lines
1 <?php
2
3 if ( ! defined( 'ABSPATH' ) ) {
4 exit; // Exit if accessed directly
5 }
6
7 if ( ! class_exists( 'ACF_Local_JSON' ) ) :
8
9 class ACF_Local_JSON {
10
11 /**
12 * The found JSON field group files.
13 *
14 * @since ACF 5.9.0
15 * @var array
16 */
17 private $files = array();
18
19 /**
20 * Whether an expected Local JSON write failed during the current request.
21 *
22 * @var boolean
23 */
24 private $save_file_failure = false;
25
26 /**
27 * Constructor.
28 *
29 * @date 14/4/20
30 * @since ACF 5.9.0
31 *
32 * @return void
33 */
34 public function __construct() {
35
36 // Update settings.
37 acf_update_setting( 'save_json', get_stylesheet_directory() . '/acf-json' );
38 acf_append_setting( 'load_json', get_stylesheet_directory() . '/acf-json' );
39
40 // Add listeners.
41 add_action( 'acf/update_field_group', array( $this, 'update_field_group' ) );
42 add_action( 'acf/untrash_field_group', array( $this, 'update_field_group' ) );
43 add_filter( 'acf/trash_field_group', array( $this, 'delete_field_group' ) );
44 add_filter( 'acf/delete_field_group', array( $this, 'delete_field_group' ) );
45 add_filter( 'acf/update_post_type', array( $this, 'update_internal_post_type' ) );
46 add_filter( 'acf/untrash_post_type', array( $this, 'update_internal_post_type' ) );
47 add_filter( 'acf/trash_post_type', array( $this, 'delete_internal_post_type' ) );
48 add_filter( 'acf/delete_post_type', array( $this, 'delete_internal_post_type' ) );
49 add_filter( 'acf/update_taxonomy', array( $this, 'update_internal_post_type' ) );
50 add_filter( 'acf/untrash_taxonomy', array( $this, 'update_internal_post_type' ) );
51 add_filter( 'acf/trash_taxonomy', array( $this, 'delete_internal_post_type' ) );
52 add_filter( 'acf/delete_taxonomy', array( $this, 'delete_internal_post_type' ) );
53
54 // Include fields.
55 add_action( 'acf/include_fields', array( $this, 'include_fields' ) );
56 add_action( 'acf/include_post_types', array( $this, 'include_post_types' ) );
57 add_action( 'acf/include_taxonomies', array( $this, 'include_taxonomies' ) );
58
59 if ( is_admin() ) {
60 add_filter( 'redirect_post_location', array( $this, 'redirect_post_location' ) );
61 add_action( 'current_screen', array( $this, 'maybe_show_save_failure_notice' ) );
62 }
63 }
64
65 /**
66 * Returns true if this component is enabled.
67 *
68 * @date 14/4/20
69 * @since ACF 5.9.0
70 *
71 * @return boolean
72 */
73 public function is_enabled() {
74 return (bool) acf_get_setting( 'json' );
75 }
76
77 /**
78 * Returns true if a Local JSON save failure has been recorded for this request.
79 *
80 * @since ACF 6.8.1
81 *
82 * @return boolean
83 */
84 public function has_save_file_failure() {
85 return $this->save_file_failure;
86 }
87
88 /**
89 * Records a Local JSON save failure for this request.
90 *
91 * @since ACF 6.8.1
92 *
93 * @return void
94 */
95 private function record_save_file_failure() {
96 $this->save_file_failure = true;
97 }
98
99 /**
100 * Appends a Local JSON save failure query arg to the post save redirect.
101 *
102 * @since ACF 6.8.1
103 *
104 * @param string $location The redirect location.
105 * @return string
106 */
107 public function redirect_post_location( $location ) {
108 if ( ! $this->has_save_file_failure() ) {
109 return $location;
110 }
111
112 // Only users who can manage SCF should see SCF admin save state.
113 if ( ! current_user_can( acf_get_setting( 'capability' ) ) ) {
114 return $location;
115 }
116
117 return add_query_arg( 'acf_local_json_save_failed', 1, $location );
118 }
119
120 /**
121 * Adds an admin notice when a Local JSON save failure is present in the request.
122 *
123 * @since ACF 6.8.1
124 *
125 * @param WP_Screen $current_screen The current WP_Screen object.
126 * @return void
127 */
128 public function maybe_show_save_failure_notice( $current_screen ) {
129 if ( ! acf_maybe_get_GET( 'acf_local_json_save_failed', false ) ) {
130 return;
131 }
132
133 if ( empty( $current_screen->post_type ) || ! in_array( $current_screen->post_type, acf_get_internal_post_types(), true ) ) {
134 return;
135 }
136
137 // Match the capability used by SCF internal post type save handlers.
138 if ( ! current_user_can( acf_get_setting( 'capability' ) ) ) {
139 return;
140 }
141
142 acf_add_admin_notice(
143 __( 'SCF saved your changes to the database, but could not update the Local JSON file. Check that the configured Local JSON save path is writable.', 'secure-custom-fields' ),
144 'warning'
145 );
146 }
147
148 /**
149 * Gets the path(s) to load JSON from.
150 *
151 * @since ACF 6.2
152 *
153 * @return array
154 */
155 public function get_load_paths() {
156 $paths = (array) acf_get_setting( 'load_json' );
157
158 /**
159 * Filters the path(s) used to load JSON from.
160 *
161 * @since ACF 6.2
162 *
163 * @param array $paths An array of potential paths to load JSON from.
164 * @return array
165 */
166 return (array) apply_filters( 'acf/json/load_paths', $paths );
167 }
168
169 /**
170 * Gets the path(s) to save JSON to.
171 *
172 * @since ACF 6.2
173 *
174 * @param string $key The key to get paths for (optional).
175 * @param array $post The main ACF post array (optional).
176 * @return array
177 */
178 public function get_save_paths( $key = '', $post = array() ) {
179 $name = ! empty( $post['title'] ) ? (string) $post['title'] : '';
180 $post_type = acf_determine_internal_post_type( $key );
181 $paths = array();
182
183 // Paths are sorted by priority, with key overriding name, etc.
184 $paths[] = acf_get_setting( "save_json/key={$key}" );
185 $paths[] = acf_get_setting( "save_json/name={$name}" );
186 $paths[] = acf_get_setting( "save_json/type={$post_type}" );
187 $paths[] = acf_get_setting( 'save_json' );
188 $paths = array_values( array_filter( $paths ) );
189
190 /**
191 * Filters the paths used to save JSON.
192 *
193 * @since ACF 6.2
194 *
195 * @param array $paths An array of the potential paths to save JSON to.
196 * @param array $post The ACF field group, post type, or taxonomy array.
197 * @return array
198 */
199 return (array) apply_filters( 'acf/json/save_paths', $paths, $post );
200 }
201
202 /**
203 * Writes field group data to JSON file.
204 *
205 * @date 14/4/20
206 * @since ACF 5.9.0
207 *
208 * @param array $field_group The field group.
209 * @return void
210 */
211 public function update_field_group( $field_group ) {
212
213 // Bail early if disabled.
214 if ( ! $this->is_enabled() ) {
215 return false;
216 }
217
218 // Append fields.
219 $field_group['fields'] = acf_get_fields( $field_group );
220
221 // Save to file.
222 $this->save_file( $field_group['key'], $field_group );
223 }
224
225 /**
226 * Writes ACF posts to the JSON file.
227 *
228 * @since ACF 6.1
229 *
230 * @param array $post The main ACF post array.
231 * @return boolean
232 */
233 public function update_internal_post_type( $post ) {
234 if ( ! $this->is_enabled() ) {
235 return false;
236 }
237
238 /**
239 * Filters the ACF post before saving it to the file.
240 *
241 * @since ACF 6.1
242 *
243 * @param array $post The main ACF post array
244 */
245 $post = apply_filters( 'acf/pre_save_json_file', $post );
246
247 return $this->save_file( $post['key'], $post );
248 }
249
250 /**
251 * Deletes a field group JSON file.
252 *
253 * @date 14/4/20
254 * @since ACF 5.9.0
255 *
256 * @param array $field_group The field group.
257 * @return boolean
258 */
259 public function delete_field_group( $field_group ) {
260 return $this->delete_internal_post_type( $field_group );
261 }
262
263 /**
264 * Deletes an ACF JSON file.
265 *
266 * @since ACF 6.1
267 *
268 * @param array $post The main ACF post array.
269 * @return boolean
270 */
271 public function delete_internal_post_type( $post ) {
272 if ( ! $this->is_enabled() ) {
273 return false;
274 }
275
276 // WP appends '__trashed' to the end of 'key' (post_name).
277 $key = str_replace( '__trashed', '', $post['key'] );
278
279 return $this->delete_file( $key, $post );
280 }
281
282 /**
283 * Includes all local JSON fields.
284 *
285 * @date 14/4/20
286 * @since ACF 5.9.0
287 *
288 * @return void
289 */
290 public function include_fields() {
291
292 // Bail early if disabled.
293 if ( ! $this->is_enabled() ) {
294 return false;
295 }
296
297 // Get load paths.
298 $files = $this->scan_files( 'acf-field-group' );
299 foreach ( $files as $key => $file ) {
300 $json = json_decode( file_get_contents( $file ), true );
301 $json['local'] = 'json';
302 $json['local_file'] = $file;
303 acf_add_local_field_group( $json );
304 }
305 }
306
307 /**
308 * Includes all local JSON post types.
309 *
310 * @since ACF 6.1
311 */
312 public function include_post_types() {
313 // Bail early if disabled.
314 if ( ! $this->is_enabled() ) {
315 return false;
316 }
317
318 // Get load paths.
319 $files = $this->scan_files( 'acf-post-type' );
320 foreach ( $files as $key => $file ) {
321 $json = json_decode( file_get_contents( $file ), true );
322 $json['local'] = 'json';
323 $json['local_file'] = $file;
324 acf_add_local_internal_post_type( $json, 'acf-post-type' );
325 }
326 }
327
328 /**
329 * Includes all local JSON taxonomies.
330 *
331 * @since ACF 6.1
332 */
333 public function include_taxonomies() {
334 // Bail early if disabled.
335 if ( ! $this->is_enabled() ) {
336 return false;
337 }
338
339 // Get load paths.
340 $files = $this->scan_files( 'acf-taxonomy' );
341 foreach ( $files as $key => $file ) {
342 $json = json_decode( file_get_contents( $file ), true );
343 $json['local'] = 'json';
344 $json['local_file'] = $file;
345 acf_add_local_internal_post_type( $json, 'acf-taxonomy' );
346 }
347 }
348
349 /**
350 * Scans for JSON field groups.
351 *
352 * @date 14/4/20
353 * @since ACF 5.9.0
354 *
355 * @return array
356 */
357 function scan_field_groups() {
358 return $this->scan_files( 'acf-field-group' );
359 }
360
361 /**
362 * Scans for JSON files.
363 *
364 * @since ACF 6.1
365 *
366 * @param string $post_type The ACF post type to scan for.
367 * @return array
368 */
369 function scan_files( $post_type = 'acf-field-group' ) {
370 $json_files = array();
371
372 // Loop over "local_json" paths and parse JSON files.
373 foreach ( $this->get_load_paths() as $path ) {
374 if ( is_dir( $path ) ) {
375 $files = scandir( $path );
376 if ( $files ) {
377 foreach ( $files as $filename ) {
378
379 // Ignore hidden files.
380 if ( $filename[0] === '.' ) {
381 continue;
382 }
383
384 // Ignore sub directories.
385 $file = untrailingslashit( $path ) . '/' . $filename;
386 if ( is_dir( $file ) ) {
387 continue;
388 }
389
390 // Ignore non JSON files.
391 $ext = pathinfo( $filename, PATHINFO_EXTENSION );
392 if ( $ext !== 'json' ) {
393 continue;
394 }
395
396 // Read JSON data.
397 $json = json_decode( file_get_contents( $file ), true );
398 if ( ! is_array( $json ) || ! isset( $json['key'] ) ) {
399 continue;
400 }
401
402 // Append data.
403 $json_files[ $json['key'] ] = $file;
404 }
405 }
406 }
407 }
408
409 // Store data and return.
410 $this->files = $json_files;
411 return $this->get_files( $post_type );
412 }
413
414 /**
415 * Returns an array of found JSON files.
416 *
417 * @date 14/4/20
418 * @since ACF 5.9.0
419 *
420 * @param string $post_type The ACF post type to get files for.
421 * @return array
422 */
423 public function get_files( $post_type = 'acf-field-group' ) {
424 $files = array();
425
426 foreach ( $this->files as $key => $path ) {
427 $internal_post_type = acf_determine_internal_post_type( $key );
428
429 if ( $internal_post_type === $post_type ) {
430 $files[ $key ] = $path;
431 } elseif ( 'acf-field-group' === $post_type ) {
432 // If we can't figure out the ACF post type, make an educated guess that it's a field group.
433 $json = json_decode( file_get_contents( $path ), true );
434 if ( ! is_array( $json ) ) {
435 continue;
436 }
437
438 if ( isset( $json['fields'] ) ) {
439 $files[ $key ] = $path;
440 }
441 }
442 }
443
444 return $files;
445 }
446
447 /**
448 * Gets the filename for an ACF JSON file.
449 *
450 * @since ACF 6.3
451 *
452 * @param string $key The ACF post key.
453 * @param array $post The main ACF post array.
454 * @return string|boolean
455 */
456 public function get_filename( $key, $post ) {
457 $load_path = '';
458
459 if ( is_array( $this->files ) && isset( $this->files[ $key ] ) ) {
460 $load_path = $this->files[ $key ];
461 }
462
463 /**
464 * Filters the filename used when saving JSON.
465 *
466 * @since ACF 6.2
467 *
468 * @param string $filename The default filename.
469 * @param array $post The main post array for the item being saved.
470 * @param string $load_path The path that the item was loaded from.
471 */
472 $filename = apply_filters( 'acf/json/save_file_name', $key . '.json', $post, $load_path );
473
474 if ( ! is_string( $filename ) ) {
475 return false;
476 }
477
478 $filename = sanitize_file_name( $filename );
479
480 // sanitize_file_name() can potentially remove all characters.
481 if ( empty( $filename ) ) {
482 return false;
483 }
484
485 return $filename;
486 }
487
488 /**
489 * Saves an ACF JSON file.
490 *
491 * @date 17/4/20
492 * @since ACF 5.9.0
493 *
494 * @param string $key The ACF post key.
495 * @param array $post The main ACF post array.
496 * @return boolean
497 */
498 public function save_file( $key, $post ) {
499 $paths = $this->get_save_paths( $key, $post );
500 $filename = $this->get_filename( $key, $post );
501 $file = false;
502 $first_writable = false;
503 $has_existing_file = is_array( $this->files ) && isset( $this->files[ $key ] );
504
505 if ( ! $filename ) {
506 return false;
507 }
508
509 foreach ( $paths as $path ) {
510 if ( ! is_string( $path ) || '' === $path ) {
511 continue;
512 }
513
514 $file_to_check = trailingslashit( $path ) . $filename;
515
516 if ( is_file( $file_to_check ) ) {
517 $has_existing_file = true;
518 }
519
520 if ( ! is_writable( $path ) ) { //phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- non-compatible function for this purpose.
521 continue;
522 }
523
524 if ( false === $first_writable ) {
525 $first_writable = $path;
526 }
527
528 if ( is_file( $file_to_check ) ) {
529 $file = $file_to_check;
530 }
531 }
532
533 if ( ! $file ) {
534 if ( $first_writable ) {
535 $file = trailingslashit( $first_writable ) . $filename;
536 } else {
537 if ( $has_existing_file ) {
538 $this->record_save_file_failure();
539 }
540
541 return false;
542 }
543 }
544
545 // Make sure this is a valid ACF post type.
546 $post_type = acf_determine_internal_post_type( $key );
547 if ( ! $post_type ) {
548 return false;
549 }
550
551 // Append modified time.
552 if ( $post['ID'] ) {
553 $post['modified'] = get_post_modified_time( 'U', true, $post['ID'] );
554 } else {
555 $post['modified'] = strtotime( 'now' );
556 }
557
558 // Prepare for export and save the file.
559 $post = acf_prepare_internal_post_type_for_export( $post, $post_type );
560 $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.
561
562 if ( ! is_int( $result ) && $has_existing_file ) {
563 $this->record_save_file_failure();
564 }
565
566 // Return true if bytes were written.
567 return is_int( $result );
568 }
569
570 /**
571 * Deletes an ACF JSON file.
572 *
573 * @date 17/4/20
574 * @since ACF 5.9.0
575 *
576 * @param string $key The ACF post key.
577 * @param array $post The main ACF post array.
578 * @return boolean
579 */
580 public function delete_file( $key, $post = array() ) {
581 $paths = $this->get_save_paths( $key, $post );
582 $filename = $this->get_filename( $key, $post );
583
584 if ( ! $filename ) {
585 return false;
586 }
587
588 foreach ( $paths as $path_to_check ) {
589 $file = untrailingslashit( $path_to_check ) . '/' . $filename;
590
591 if ( is_writable( $file ) ) { //phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- non-compatible function for this purpose.
592 wp_delete_file( $file );
593 }
594 }
595
596 return true;
597 }
598
599 /**
600 * Includes all local JSON files.
601 *
602 * @date 10/03/2014
603 * @since ACF 5.0.0
604 * @deprecated 5.9.0
605 */
606 public function include_json_folders() {
607 _deprecated_function( __METHOD__, '5.9.0', 'ACF_Local_JSON::include_fields()' );
608 $this->include_fields();
609 }
610
611 /**
612 * Includes local JSON files within a specific folder.
613 *
614 * @date 01/05/2017
615 * @since ACF 5.5.13
616 * @deprecated 5.9.0
617 *
618 * @param string $path The path to a specific JSON folder.
619 * @return void
620 */
621 public function include_json_folder( $path = '' ) {
622 _deprecated_function( __METHOD__, '5.9.0' );
623 // Do nothing.
624 }
625 }
626
627 // Initialize.
628 acf_new_instance( 'ACF_Local_JSON' );
629 endif; // class_exists check
630
631 /**
632 * Returns an array of found JSON field group files.
633 *
634 * @date 14/4/20
635 * @since ACF 5.9.0
636 *
637 * @param string $post_type The ACF post type to get files for.
638 * @return array
639 */
640 function acf_get_local_json_files( $post_type = 'acf-field-group' ) {
641 return acf_get_instance( 'ACF_Local_JSON' )->get_files( $post_type );
642 }
643
644 /**
645 * Saves a field group JSON file.
646 *
647 * @date 5/12/2014
648 * @since ACF 5.1.5
649 *
650 * @param array $field_group The field group.
651 * @return boolean
652 */
653 function acf_write_json_field_group( $field_group ) {
654 return acf_get_instance( 'ACF_Local_JSON' )->save_file( $field_group['key'], $field_group );
655 }
656
657 /**
658 * Deletes a field group JSON file.
659 *
660 * @date 5/12/2014
661 * @since ACF 5.1.5
662 *
663 * @param string $key The field group key.
664 * @return boolean True on success.
665 */
666 function acf_delete_json_field_group( $key ) {
667 return acf_get_instance( 'ACF_Local_JSON' )->delete_file( $key );
668 }
669