ACF_Local_JSON{}ACF 1.0

Usage

$ACF_Local_JSON = new ACF_Local_JSON();
// use class methods

Methods

  1. public __construct()
  2. public is_enabled()
  3. public has_save_file_failure()
  4. public redirect_post_location( $location )
  5. public maybe_show_save_failure_notice( $current_screen )
  6. public get_load_paths()
  7. public get_save_paths( $key = '', $post = array() )
  8. public update_field_group( $field_group )
  9. public update_internal_post_type( $post )
  10. public delete_field_group( $field_group )
  11. public delete_internal_post_type( $post )
  12. public include_fields()
  13. public include_post_types()
  14. public include_taxonomies()
  15. public scan_field_groups()
  16. public scan_files( $post_type = 'acf-field-group' )
  17. public get_files( $post_type = 'acf-field-group' )
  18. public get_filename( $key, $post )
  19. public save_file( $key, $post )
  20. public delete_file( $key, $post = array() )
  21. public include_json_folders()
  22. public include_json_folder( $path = '' )
  23. private record_save_file_failure()

ACF_Local_JSON{} code ACF 6.8.8

class ACF_Local_JSON {

	/**
	 * The found JSON field group files.
	 *
	 * @since 5.9.0
	 * @var array
	 */
	private $files = array();

	/**
	 * Whether an expected Local JSON write failed during the current request.
	 *
	 * @var boolean
	 */
	private $save_file_failure = false;

	/**
	 * Constructor.
	 *
	 * @date    14/4/20
	 * @since   5.9.0
	 *
	 * @param   void
	 * @return  void
	 */
	public function __construct() {

		// Update settings.
		acf_update_setting( 'save_json', get_stylesheet_directory() . '/acf-json' );
		acf_append_setting( 'load_json', get_stylesheet_directory() . '/acf-json' );

		// Add listeners.
		add_action( 'acf/update_field_group', array( $this, 'update_field_group' ) );
		add_action( 'acf/untrash_field_group', array( $this, 'update_field_group' ) );
		add_action( 'acf/trash_field_group', array( $this, 'delete_field_group' ) );
		add_action( 'acf/delete_field_group', array( $this, 'delete_field_group' ) );
		add_action( 'acf/update_post_type', array( $this, 'update_internal_post_type' ) );
		add_action( 'acf/untrash_post_type', array( $this, 'update_internal_post_type' ) );
		add_action( 'acf/trash_post_type', array( $this, 'delete_internal_post_type' ) );
		add_action( 'acf/delete_post_type', array( $this, 'delete_internal_post_type' ) );
		add_action( 'acf/update_taxonomy', array( $this, 'update_internal_post_type' ) );
		add_action( 'acf/untrash_taxonomy', array( $this, 'update_internal_post_type' ) );
		add_action( 'acf/trash_taxonomy', array( $this, 'delete_internal_post_type' ) );
		add_action( 'acf/delete_taxonomy', array( $this, 'delete_internal_post_type' ) );

		// Include fields.
		add_action( 'acf/include_fields', array( $this, 'include_fields' ) );
		add_action( 'acf/include_post_types', array( $this, 'include_post_types' ) );
		add_action( 'acf/include_taxonomies', array( $this, 'include_taxonomies' ) );

		if ( is_admin() ) {
			add_filter( 'redirect_post_location', array( $this, 'redirect_post_location' ) );
			add_action( 'current_screen', array( $this, 'maybe_show_save_failure_notice' ) );
		}
	}

	/**
	 * Returns true if this component is enabled.
	 *
	 * @date    14/4/20
	 * @since   5.9.0
	 *
	 * @param   void
	 * @return  boolean
	 */
	public function is_enabled() {
		return (bool) acf_get_setting( 'json' );
	}

	/**
	 * Returns true if a Local JSON save failure has been recorded for this request.
	 *
	 * @since 6.8.1
	 *
	 * @return boolean
	 */
	public function has_save_file_failure() {
		return $this->save_file_failure;
	}

	/**
	 * Records a Local JSON save failure for this request.
	 *
	 * @since 6.8.1
	 *
	 * @return void
	 */
	private function record_save_file_failure() {
		$this->save_file_failure = true;
	}

	/**
	 * Appends a Local JSON save failure query arg to the post save redirect.
	 *
	 * @since 6.8.1
	 *
	 * @param string $location The redirect location.
	 * @return string
	 */
	public function redirect_post_location( $location ) {
		if ( ! $this->has_save_file_failure() ) {
			return $location;
		}

		// Only users who can manage ACF should see ACF admin save state.
		if ( ! current_user_can( acf_get_setting( 'capability' ) ) ) {
			return $location;
		}

		return add_query_arg( 'acf_local_json_save_failed', 1, $location );
	}

	/**
	 * Adds an admin notice when a Local JSON save failure is present in the request.
	 *
	 * @since 6.8.1
	 *
	 * @param WP_Screen $current_screen The current WP_Screen object.
	 * @return void
	 */
	public function maybe_show_save_failure_notice( $current_screen ) {
		if ( ! acf_maybe_get_GET( 'acf_local_json_save_failed', false ) ) {
			return;
		}

		if ( empty( $current_screen->post_type ) || ! in_array( $current_screen->post_type, acf_get_internal_post_types(), true ) ) {
			return;
		}

		// Match the capability used by ACF internal post type save handlers.
		if ( ! current_user_can( acf_get_setting( 'capability' ) ) ) {
			return;
		}

		acf_add_admin_notice(
			__( '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' ),
			'warning'
		);
	}

	/**
	 * Gets the path(s) to load JSON from.
	 *
	 * @since 6.2
	 *
	 * @return array
	 */
	public function get_load_paths() {
		$paths = (array) acf_get_setting( 'load_json' );

		/**
		 * Filters the path(s) used to load JSON from.
		 *
		 * @since 6.2
		 *
		 * @param array $paths An array of potential paths to load JSON from.
		 * @return array
		 */
		return (array) apply_filters( 'acf/json/load_paths', $paths );
	}

	/**
	 * Gets the path(s) to save JSON to.
	 *
	 * @since 6.2
	 *
	 * @param string $key  The key to get paths for (optional).
	 * @param array  $post The main ACF post array (optional).
	 * @return array
	 */
	public function get_save_paths( $key = '', $post = array() ) {
		$name      = ! empty( $post['title'] ) ? (string) $post['title'] : '';
		$post_type = acf_determine_internal_post_type( $key );
		$paths     = array();

		// Paths are sorted by priority, with key overriding name, etc.
		$paths[] = acf_get_setting( "save_json/key={$key}" );
		$paths[] = acf_get_setting( "save_json/name={$name}" );
		$paths[] = acf_get_setting( "save_json/type={$post_type}" );
		$paths[] = acf_get_setting( 'save_json' );
		$paths   = array_values( array_filter( $paths ) );

		/**
		 * Filters the paths used to save JSON.
		 *
		 * @since 6.2
		 *
		 * @param array $paths An array of the potential paths to save JSON to.
		 * @param array $post  The ACF field group, post type, or taxonomy array.
		 * @return array
		 */
		return (array) apply_filters( 'acf/json/save_paths', $paths, $post );
	}

	/**
	 * Writes field group data to JSON file.
	 *
	 * @date    14/4/20
	 * @since   5.9.0
	 *
	 * @param   array $field_group The field group.
	 * @return  void
	 */
	public function update_field_group( $field_group ) {

		// Bail early if disabled.
		if ( ! $this->is_enabled() ) {
			return false;
		}

		// Append fields.
		$field_group['fields'] = acf_get_fields( $field_group );

		// Save to file.
		$this->save_file( $field_group['key'], $field_group );
	}

	/**
	 * Writes ACF posts to the JSON file.
	 *
	 * @since 6.1
	 *
	 * @param array $post The main ACF post array.
	 * @return boolean
	 */
	public function update_internal_post_type( $post ) {
		if ( ! $this->is_enabled() ) {
			return false;
		}

		/**
		 * Filters the ACF post before saving it to the file.
		 *
		 * @since 6.1
		 *
		 * @param array $post The main ACF post array
		 */
		$post = apply_filters( 'acf/pre_save_json_file', $post );

		return $this->save_file( $post['key'], $post );
	}

	/**
	 * Deletes a field group JSON file.
	 *
	 * @date 14/4/20
	 * @since 5.9.0
	 *
	 * @param  array $field_group The field group.
	 * @return boolean
	 */
	public function delete_field_group( $field_group ) {
		return $this->delete_internal_post_type( $field_group );
	}

	/**
	 * Deletes an ACF JSON file.
	 *
	 * @since 6.1
	 *
	 * @param array $post The main ACF post array.
	 * @return boolean
	 */
	public function delete_internal_post_type( $post ) {
		if ( ! $this->is_enabled() ) {
			return false;
		}

		// WP appends '__trashed' to the end of 'key' (post_name).
		$key = str_replace( '__trashed', '', $post['key'] );

		return $this->delete_file( $key, $post );
	}

	/**
	 * Includes all local JSON fields.
	 *
	 * @date    14/4/20
	 * @since   5.9.0
	 *
	 * @param   void
	 * @return  void
	 */
	public function include_fields() {

		// Bail early if disabled.
		if ( ! $this->is_enabled() ) {
			return false;
		}

		// Get load paths.
		$files = $this->scan_files( 'acf-field-group' );
		foreach ( $files as $key => $file ) {
			$json               = json_decode( file_get_contents( $file ), true );
			$json['local']      = 'json';
			$json['local_file'] = $file;
			acf_add_local_field_group( $json );
		}
	}

	/**
	 * Includes all local JSON post types.
	 *
	 * @since 6.1
	 */
	public function include_post_types() {
		// Bail early if disabled.
		if ( ! $this->is_enabled() ) {
			return false;
		}

		// Get load paths.
		$files = $this->scan_files( 'acf-post-type' );
		foreach ( $files as $key => $file ) {
			$json               = json_decode( file_get_contents( $file ), true );
			$json['local']      = 'json';
			$json['local_file'] = $file;
			acf_add_local_internal_post_type( $json, 'acf-post-type' );
		}
	}

	/**
	 * Includes all local JSON taxonomies.
	 *
	 * @since 6.1
	 */
	public function include_taxonomies() {
		// Bail early if disabled.
		if ( ! $this->is_enabled() ) {
			return false;
		}

		// Get load paths.
		$files = $this->scan_files( 'acf-taxonomy' );
		foreach ( $files as $key => $file ) {
			$json               = json_decode( file_get_contents( $file ), true );
			$json['local']      = 'json';
			$json['local_file'] = $file;
			acf_add_local_internal_post_type( $json, 'acf-taxonomy' );
		}
	}

	/**
	 * Scans for JSON field groups.
	 *
	 * @date    14/4/20
	 * @since   5.9.0
	 *
	 * @return array
	 */
	function scan_field_groups() {
		return $this->scan_files( 'acf-field-group' );
	}

	/**
	 * Scans for JSON files.
	 *
	 * @since 6.1
	 *
	 * @param string $post_type The ACF post type to scan for.
	 * @return array
	 */
	function scan_files( $post_type = 'acf-field-group' ) {
		$json_files = array();

		// Loop over "local_json" paths and parse JSON files.
		foreach ( $this->get_load_paths() as $path ) {
			if ( is_dir( $path ) ) {
				$files = scandir( $path );
				if ( $files ) {
					foreach ( $files as $filename ) {

						// Ignore hidden files.
						if ( $filename[0] === '.' ) {
							continue;
						}

						// Ignore sub directories.
						$file = untrailingslashit( $path ) . '/' . $filename;
						if ( is_dir( $file ) ) {
							continue;
						}

						// Ignore non JSON files.
						$ext = pathinfo( $filename, PATHINFO_EXTENSION );
						if ( $ext !== 'json' ) {
							continue;
						}

						// Read JSON data.
						$json = json_decode( file_get_contents( $file ), true );
						if ( ! is_array( $json ) || ! isset( $json['key'] ) ) {
							continue;
						}

						// Append data.
						$json_files[ $json['key'] ] = $file;
					}
				}
			}
		}

		// Store data and return.
		$this->files = $json_files;
		return $this->get_files( $post_type );
	}

	/**
	 * Returns an array of found JSON files.
	 *
	 * @date 14/4/20
	 * @since 5.9.0
	 *
	 * @param string $post_type The ACF post type to get files for.
	 * @return array
	 */
	public function get_files( $post_type = 'acf-field-group' ) {
		$files = array();

		foreach ( $this->files as $key => $path ) {
			$internal_post_type = acf_determine_internal_post_type( $key );

			if ( $internal_post_type === $post_type ) {
				$files[ $key ] = $path;
			} elseif ( 'acf-field-group' === $post_type ) {
				// If we can't figure out the ACF post type, make an educated guess that it's a field group.
				$json = json_decode( file_get_contents( $path ), true );
				if ( ! is_array( $json ) ) {
					continue;
				}

				if ( isset( $json['fields'] ) ) {
					$files[ $key ] = $path;
				}
			}
		}

		return $files;
	}

	/**
	 * Gets the filename for an ACF JSON file.
	 *
	 * @since 6.3
	 *
	 * @param string $key  The ACF post key.
	 * @param array  $post The main ACF post array.
	 * @return string|boolean
	 */
	public function get_filename( $key, $post ) {
		$load_path = '';

		if ( is_array( $this->files ) && isset( $this->files[ $key ] ) ) {
			$load_path = $this->files[ $key ];
		}

		/**
		 * Filters the filename used when saving JSON.
		 *
		 * @since 6.2
		 *
		 * @param string $filename  The default filename.
		 * @param array  $post      The main post array for the item being saved.
		 * @param string $load_path The path that the item was loaded from.
		 */
		$filename = apply_filters( 'acf/json/save_file_name', $key . '.json', $post, $load_path );

		if ( ! is_string( $filename ) ) {
			return false;
		}

		$filename = sanitize_file_name( $filename );

		// sanitize_file_name() can potentially remove all characters.
		if ( empty( $filename ) ) {
			return false;
		}

		return $filename;
	}

	/**
	 * Saves an ACF JSON file.
	 *
	 * @date 17/4/20
	 * @since 5.9.0
	 *
	 * @param string $key  The ACF post key.
	 * @param array  $post The main ACF post array.
	 * @return boolean
	 */
	public function save_file( $key, $post ) {
		$paths             = $this->get_save_paths( $key, $post );
		$filename          = $this->get_filename( $key, $post );
		$file              = false;
		$first_writable    = false;
		$has_existing_file = is_array( $this->files ) && isset( $this->files[ $key ] );

		if ( ! $filename ) {
			return false;
		}

		foreach ( $paths as $path ) {
			if ( ! is_string( $path ) || '' === $path ) {
				continue;
			}

			$file_to_check = trailingslashit( $path ) . $filename;

			if ( is_file( $file_to_check ) ) {
				$has_existing_file = true;
			}

			if ( ! is_writable( $path ) ) { //phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- non-compatible function for this purpose.
				continue;
			}

			if ( false === $first_writable ) {
				$first_writable = $path;
			}

			if ( is_file( $file_to_check ) ) {
				$file = $file_to_check;
			}
		}

		if ( ! $file ) {
			if ( $first_writable ) {
				$file = trailingslashit( $first_writable ) . $filename;
			} else {
				if ( $has_existing_file ) {
					$this->record_save_file_failure();
				}

				return false;
			}
		}

		// Make sure this is a valid ACF post type.
		$post_type = acf_determine_internal_post_type( $key );
		if ( ! $post_type ) {
			return false;
		}

		// Append modified time.
		if ( $post['ID'] ) {
			$post['modified'] = get_post_modified_time( 'U', true, $post['ID'] );
		} else {
			$post['modified'] = strtotime( 'now' );
		}

		// Prepare for export and save the file.
		$post   = acf_prepare_internal_post_type_for_export( $post, $post_type );
		$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.

		if ( ! is_int( $result ) && $has_existing_file ) {
			$this->record_save_file_failure();
		}

		// Return true if bytes were written.
		return is_int( $result );
	}

	/**
	 * Deletes an ACF JSON file.
	 *
	 * @date 17/4/20
	 * @since 5.9.0
	 *
	 * @param string $key  The ACF post key.
	 * @param array  $post The main ACF post array.
	 * @return boolean
	 */
	public function delete_file( $key, $post = array() ) {
		$paths    = $this->get_save_paths( $key, $post );
		$filename = $this->get_filename( $key, $post );

		if ( ! $filename ) {
			return false;
		}

		foreach ( $paths as $path_to_check ) {
			$file = untrailingslashit( $path_to_check ) . '/' . $filename;

			if ( is_writable( $file ) ) { //phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- non-compatible function for this purpose.
				wp_delete_file( $file );
			}
		}

		return true;
	}

	/**
	 * Includes all local JSON files.
	 *
	 * @date       10/03/2014
	 * @since      5.0.0
	 * @deprecated 5.9.0
	 */
	public function include_json_folders() {
		_deprecated_function( __METHOD__, '5.9.0', 'ACF_Local_JSON::include_fields()' );
		$this->include_fields();
	}

	/**
	 * Includes local JSON files within a specific folder.
	 *
	 * @date    01/05/2017
	 * @since   5.5.13
	 * @deprecated 5.9.0
	 *
	 * @param   string $path The path to a specific JSON folder.
	 * @return  void
	 */
	public function include_json_folder( $path = '' ) {
		_deprecated_function( __METHOD__, '5.9.0' );
		// Do nothing.
	}
}