Linux webm002.cluster126.gra.hosting.ovh.net 6.18.39-ovh-vps-grsec-zfs+ #1 SMP PREEMPT_DYNAMIC Tue Jul 21 12:03:15 CEST 2026 x86_64
/
home
/
ariannadhf
/
www
/
wp-content
/
plugins
/
simple-history
/
loggers
/
/home/ariannadhf/www/wp-content/plugins/simple-history/loggers/class-plugin-acf-logger.php
<?php //phpcs:disable Squiz.Commenting.InlineComment.InvalidEndChar namespace Simple_History\Loggers; /** * Logger for the Advanced Custom Fields (ACF) plugin * https://sv.wordpress.org/plugins/advanced-custom-fields/ * * @package SimpleHistory * @since 2.21 */ class Plugin_ACF_Logger extends Logger { /** * The slug for this logger. * * @var string $slug */ public $slug = 'Plugin_ACF'; /** * Will contain field groups and fields, before and after post save. * * @var array $old_and_new_field_groups_and_fields */ private array $old_and_new_field_groups_and_fields = array( 'fieldGroup' => array( 'old' => null, 'new' => null, ), 'modifiedFields' => array( 'old' => null, 'new' => null, ), 'addedFields' => array(), 'deletedFields' => array(), ); /** * Will contain the post data before save, i.e. the previous version of the post. * * @var array $old_post_data */ private array $old_post_data = array(); /** * Get info for this logger. * * @return array Array with info about the logger. */ public function get_info() { return array( 'name' => _x( 'Plugin: Advanced Custom Fields Logger', 'Logger: Plugin ACF', 'simple-history' ), 'description' => _x( 'Logs ACF stuff', 'Logger: Plugin ACF', 'simple-history' ), 'name_via' => _x( 'Using plugin ACF', 'Logger: Plugin ACF', 'simple-history' ), 'capability' => 'manage_options', ); } /** * Check if ACF is installed. * * @return bool */ private function isACFInstalled() { return defined( 'ACF' ) && ACF; } /** * Method called when logger is loaded. */ public function loaded() { // Bail if no ACF found. if ( ! $this->isACFInstalled() ) { return; } // Remove ACF Fields from the post types that postlogger logs. add_filter( 'simple_history/post_logger/skip_posttypes', array( $this, 'remove_acf_from_postlogger' ) ); // Get prev version of acf field group. // This is called before transition_post_status. add_filter( 'wp_insert_post_data', array( $this, 'on_wp_insert_post_data' ), 10, 2 ); // Store old and new field data when a post is saved. add_action( 'transition_post_status', array( $this, 'on_transition_post_status' ), 5, 3 ); // Append ACF data to post context. add_filter( 'simple_history/post_logger/post_updated/context', array( $this, 'on_post_updated_context' ), 10, 2 ); // Add ACF diff data to activity feed detailed output. add_filter( 'simple_history/post_logger/post_updated/diff_table_output', array( $this, 'on_diff_table_output_field_group' ), 10, 2 ); // Store prev ACF field values before new values are added. // Called from filter admin_action_editpost that is fired at top of admin.php. add_action( 'admin_action_editpost', array( $this, 'on_admin_action_editpost' ) ); // Fired when ACF saves a post. Adds ACF context to logged row. add_action( 'acf/save_post', array( $this, 'on_acf_save_post' ), 50 ); // Fired after a log row is inserted. Add filter so field group save is is not logged again. add_action( 'simple_history/log/inserted', array( $this, 'on_log_inserted' ), 10, 3 ); } /** * Fired after a log row is inserted. * * @param array $context Context. * @param array $data_parent_row Data parent row. * @param \Simple_History\Loggers\Logger $simple_history_instance Simple History logger instance. */ public function on_log_inserted( $context, $data_parent_row, $simple_history_instance ) { $message_key = empty( $context['_message_key'] ) ? false : $context['_message_key']; $logger = empty( $data_parent_row['logger'] ) ? false : $data_parent_row['logger']; $post_id = empty( $context['post_id'] ) ? false : $context['post_id']; $post_type = empty( $context['post_type'] ) ? false : $context['post_type']; // Bail if not all required vars are set. if ( ! $message_key || ! $logger || ! $post_id || ! $post_type ) { return; } // Only act when logger was SimplePostLogger. if ( $logger !== 'SimplePostLogger' ) { return; } // Only act when the saved type was a ACF Field Group. if ( $post_type !== 'acf-field-group' ) { return; } // Ok, a row was inserted using the log function on SimplePostLogger, // now ACF will call save_post again and trigger // another log of the same row. To prevent this we // now add a filter to prevent the next log. add_filter( 'simple_history/post_logger/post_updated/ok_to_log', array( $this, 'prevent_second_acf_field_group_post_save_log' ), 10, 4 ); } /** * Fired from SimpleLogger action 'simple_history/post_logger/post_updated/ok_to_log' and added only after * a row already has been logged. * * This function checks if post type logged by SimplePostLogger is a ACF Field Group, and if it is * then don't log that log. This way we prevent the post logger from logging the field group changes twice. * * @param bool $ok_to_log Ok to log. * @param string $new_status New status. * @param string $old_status Old status. * @param \WP_Post $post Post. */ public function prevent_second_acf_field_group_post_save_log( $ok_to_log, $new_status, $old_status, $post ) { if ( isset( $post->post_type ) && $post->post_type === 'acf-field-group' ) { $ok_to_log = false; } return $ok_to_log; } /** * Append info about changes in ACF fields input, * i.e. store all info we later use to show changes that a user has done. * * Called when ACF saves a post. * * @param mixed $post_id ID of post that is being saved. String "option" or "options" when saving an options page. */ public function on_acf_save_post( $post_id ) { if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; } // Only act when $post_id is numeric, can be "options" too when // ACF saves an options page. if ( ! is_numeric( $post_id ) ) { return; } // Don't act on post revision. if ( wp_is_post_revision( $post_id ) ) { return; } // phpcs:disable Squiz.PHP.CommentedOutCode.Found -- Documentation showing data structure. /* * Meta values look like [product_images_0_image] => 625 [_product_images_0_image] => field_59a091044812e [product_images_0_image_caption] => Image row yes [_product_images_0_image_caption] => field_59a0910f4812f [product_images_0_image_related_0_related_name] => Related one [_product_images_0_image_related_0_related_name] => field_59aaedd43ae11 [product_images_0_image_related_0_related_item_post] => [_product_images_0_image_related_0_related_item_post] => field_59aaede43ae12 [product_images_0_image_related_1_related_name] => Another related [_product_images_0_image_related_1_related_name] => field_59aaedd43ae11 [product_images_0_image_related_1_related_item_post] => [_product_images_0_image_related_1_related_item_post] => field_59aaede43ae12 [product_images_0_image_related] => 2 [_product_images_0_image_related] => field_59aaedbc3ae10 [product_images_1_image] => 574 */ // phpcs:enable Squiz.PHP.CommentedOutCode.Found $prev_post_meta = $this->old_post_data['prev_post_meta'] ?? array(); $new_post_meta = get_post_custom( $post_id ); array_walk( $new_post_meta, // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed function ( &$value, $key ) { $value = reset( $value ); } ); // New and old post meta can contain different amount of keys, // join them so we have the name of all post meta that have been added, removed, or modified. $new_and_old_post_meta = array_merge( $prev_post_meta, $new_post_meta ); ksort( $new_and_old_post_meta, SORT_REGULAR ); // array1 - The array to compare from // array2 - An array to compare against // Returns an array containing all the values from array1 that are not present in any of the other arrays. // Keep only ACF fields in prev and new post meta. $prev_post_meta = $this->keep_only_acf_stuff_in_array( $prev_post_meta, $new_and_old_post_meta ); $new_post_meta = $this->keep_only_acf_stuff_in_array( $new_post_meta, $new_and_old_post_meta ); $new_and_old_post_meta_acf_fields = array_merge( $prev_post_meta, $new_post_meta ); // Map field name with fieldkey so we can get field objects when needed. // Final array have values like: // [product_images_0_image] => field_59a091044812e // [product_images_0_image_caption] => field_59a0910f4812f // [product_images_0_image_related_0_related_name] => field_59aaedd43ae11. $fieldnames_to_field_keys = array(); foreach ( $new_and_old_post_meta_acf_fields as $meta_key => $meta_value ) { // $key is like [product_images_0_image_related_1_related_name]. // Get ACF fieldkey for that value. Will be in $new_and_old_post_meta // as the same as key but with underscore first $meta_key_to_look_for = "_{$meta_key}"; if ( ! isset( $new_and_old_post_meta[ $meta_key_to_look_for ] ) ) { continue; } $fieldnames_to_field_keys[ $meta_key ] = $new_and_old_post_meta[ $meta_key_to_look_for ]; } // Compare old with new = get only changed, not added, deleted are here. $post_meta_diff1 = array_diff_assoc( $prev_post_meta, $new_post_meta ); // Compare new with old = get an diff with added and changed stuff. $post_meta_diff2 = array_diff_assoc( $new_post_meta, $prev_post_meta ); // Compare keys, gets added fields. $post_meta_added_fields = array_diff( array_keys( $post_meta_diff2 ), array_keys( $post_meta_diff1 ) ); $post_meta_added_fields = array_values( $post_meta_added_fields ); // Keys that exist in diff1 but not in diff2 = deleted. $post_meta_removed_fields = array_diff_assoc( array_keys( $post_meta_diff1 ), array_keys( $post_meta_diff2 ) ); $post_meta_removed_fields = array_values( $post_meta_removed_fields ); $post_meta_changed_fields = array_keys( $post_meta_diff1 ); /* * value is changed: added to both diff and diff2 * value is added, like in repeater: added to diff2 (not to diff) * $diff3: contains only added things. * Compare old and new values * Loop all keys in $new_and_old_post_meta * But act only on those whose keys begins with "_" and where the value begins with "field_" and ends with alphanum. */ /* * We have the diff, now add it to the context * This is called after Simple History already has added its row * So... we must add to the context late somehow * Get the latest inserted row from the SimplePostLogger, check if that postID is * same as the */ $post_logger = $this->simple_history->get_instantiated_logger_by_slug( 'SimplePostLogger' ); // Save ACF diff if detected post here is same as the last one used in Postlogger. if ( ! isset( $post_logger->last_insert_context['post_id'] ) || $post_id !== $post_logger->last_insert_context['post_id'] ) { return; } $last_insert_id = $post_logger->last_insert_id; // Append new info to the context of history item with id $post_logger->last_insert_id. $acf_context = array(); $acf_context = $this->add_acf_context( $acf_context, 'added', $post_meta_added_fields, $prev_post_meta, $new_post_meta, $fieldnames_to_field_keys ); $acf_context = $this->add_acf_context( $acf_context, 'changed', $post_meta_changed_fields, $prev_post_meta, $new_post_meta, $fieldnames_to_field_keys ); $acf_context = $this->add_acf_context( $acf_context, 'removed', $post_meta_removed_fields, $prev_post_meta, $new_post_meta, $fieldnames_to_field_keys ); $post_logger->append_context( $last_insert_id, $acf_context ); } /** * Add ACF context for added, removed, or changed fields. * * @param array $context Context. * @param string $modify_type Type. added | removed | changed. * @param array $relevant_acf_fields Fields. * @param array $prev_post_meta Prev meta. * @param array $new_post_meta New meta. * @param array $fieldnames_to_field_keys Fieldnames to field keys mapping. * @return array Modified context. */ public function add_acf_context( $context = array(), $modify_type = '', $relevant_acf_fields = array(), $prev_post_meta = array(), $new_post_meta = array(), $fieldnames_to_field_keys = array() ) { if ( ! is_array( $context ) || empty( $modify_type ) || empty( $relevant_acf_fields ) ) { return $context; } $loopnum = 0; foreach ( $relevant_acf_fields as $field_slug ) { /* Store just the names to begin with acf_field_added_0 = url. acf_field_added_1 = first_name. If field slug contains a number, like in "product_images_2_image" that probably means that that field is a repeater with name "product_images" with a sub field called "image" and that the image is the 2:and among it's selected sub fields. Example of how fields can look: acf_field_added_0 product_images_2_image acf_field_added_1 product_images_2_image_caption acf_field_added_2 product_images_2_image_related acf_field_changed_0 my_field_in_acf acf_field_changed_1 product_images acf_field_changed_2 price acf_field_changed_3 description */ $context_key = "acf_field_{$modify_type}_{$loopnum}"; $context[ "{$context_key}/slug" ] = $field_slug; /* * Try to get some extra info, like display name and type for this field. * For a nice context in the feed we want: parent field group name and type? */ if ( isset( $fieldnames_to_field_keys[ $field_slug ] ) ) { $field_key = $fieldnames_to_field_keys[ $field_slug ]; $context[ "{$context_key}/key" ] = $field_key; // Interesting stuff in field object: // - Label = the human readable name of the field // - Type = the type of the field // - Parent = id of parent field post id. $field_object = get_field_object( $field_key ); if ( is_array( $field_object ) ) { $context[ "{$context_key}/label" ] = $field_object['label']; if ( ! empty( $field_object['type'] ) ) { $context[ "{$context_key}/type" ] = $field_object['type']; } // If no parent just continue to next field. if ( empty( $field_object['parent'] ) ) { continue; } // We have at least one parent, get them all, including the field group // $context[ "{$context_key}/field_parent_object" ] = $parent_field;. $field_parents = array(); $field_field_group = null; // Begin with the direct parent. $parent_field = $field_object; while ( ! empty( $parent_field['parent'] ) ) { $parentFieldParent = $parent_field['parent']; // acf-field | acf-field-group. $parent_field_post_type = get_post_type( $parentFieldParent ); if ( $parent_field_post_type === false ) { break; } if ( $parent_field_post_type === 'acf-field' ) { // Field is when field is for example a sub field of a repeater. if ( function_exists( 'acf_get_field' ) ) { // Since ACF 5.7.10 the acf_get_field() function is available. $parent_field = acf_get_field( $parentFieldParent ); } elseif ( function_exists( '_acf_get_field_by_id' ) ) { // ACF function _acf_get_field_by_id() is available before ACF 5.7.10. $parent_field = _acf_get_field_by_id( $parentFieldParent ); } } elseif ( $parent_field_post_type === 'acf-field-group' ) { $parent_field = acf_get_field_group( $parentFieldParent ); } else { // Unknown post type. break; } if ( $parent_field === false ) { break; } if ( $parent_field_post_type === 'acf-field' ) { $field_parents[] = $parent_field; } elseif ( $parent_field_post_type === 'acf-field-group' ) { $field_field_group = $parent_field; } } $field_parents = array_reverse( $field_parents ); // Array with info about each parent. $arr_field_path = array(); if ( ! empty( $field_field_group['title'] ) ) { $arr_field_path[] = array( 'name' => $field_field_group['title'], 'type' => 'field_group', ); } foreach ( $field_parents as $one_field_parent ) { $arr_field_path[] = array( 'name' => $one_field_parent['label'], 'type' => 'field', 'field_type' => $one_field_parent['type'], ); } if ( ! empty( $arr_field_path ) ) { $path_loop_num = 0; foreach ( $arr_field_path as $one_field_path ) { $context[ "{$context_key}/path_{$path_loop_num}/name" ] = $one_field_path['name']; $context[ "{$context_key}/path_{$path_loop_num}/type" ] = $one_field_path['type']; if ( ! empty( $one_field_path['field_type'] ) ) { $context[ "{$context_key}/path_{$path_loop_num}/field_type" ] = $one_field_path['field_type']; } ++$path_loop_num; } } } } ++$loopnum; } return $context; } /** * Clean array and keep only ACF related things. * * Remove * - underscore fields * - fields with value field_* * * Keep * - vals that are acf * * @param array $arr Array. * @param array $all_fields Array fields. */ public function keep_only_acf_stuff_in_array( $arr, $all_fields ) { $new_arr = array(); if ( ! is_array( $arr ) ) { return $new_arr; } foreach ( $arr as $key => $val ) { // Bail if not a string. if ( ! is_string( $key ) || ! is_string( $val ) ) { continue; } // Don't keep keys that begin with underscore "_". if ( strpos( $key, '_' ) === 0 ) { continue; } // Don't keep keys that begin with "field_". if ( strpos( $val, 'field_' ) === 0 ) { continue; } // Don't keep fields that does not have a corresponding _field value. // Each key has both the name, for example 'color' and another // key called '_color'. We check that the underscore version exists // and contains 'field_'. After this check only ACF fields should exist // in the array.. if ( ! isset( $all_fields[ "_{$key}" ] ) ) { continue; } if ( strpos( $all_fields[ "_{$key}" ], 'field_' ) !== 0 ) { continue; } $new_arr[ $key ] = $val; } return $new_arr; } /** * Store prev post meta when post is saved. * Stores data in $this->old_post_data. */ public function on_admin_action_editpost() { // phpcs:ignore WordPress.Security.NonceVerification.Missing $post_ID = isset( $_POST['post_ID'] ) ? (int) $_POST['post_ID'] : 0; if ( $post_ID === 0 ) { return; } $prev_post = get_post( $post_ID ); if ( ! $prev_post instanceof \WP_Post ) { return; } $post_meta = get_post_custom( $post_ID ); // Meta is array of arrays, get first value of each array value. array_walk( $post_meta, // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed function ( &$value, $key ) { $value = reset( $value ); } ); $this->old_post_data['prev_post_meta'] = $post_meta; } /** * Called from PostLogger and its diff table output using filter 'simple_history/post_logger/post_updated/diff_table_output'. * Diff table is generated only for post type 'acf-field-group'. * * @param string $diff_table_output Diff table output. * @param array $context Context. * @return string */ public function on_diff_table_output_field_group( $diff_table_output, $context ) { $post_type = empty( $context['post_type'] ) ? false : $context['post_type']; // Bail if not ACF Field Group. if ( $post_type !== 'acf-field-group' ) { return $diff_table_output; } // Field group fields to check for and output if found. $arrKeys = array( 'instruction_placement' => array( 'name' => _x( 'Instruction placement', 'Logger: Plugin ACF', 'simple-history' ), ), 'label_placement' => array( 'name' => _x( 'Label placement', 'Logger: Plugin ACF', 'simple-history' ), ), 'description' => array( 'name' => _x( 'Description', 'Logger: Plugin ACF', 'simple-history' ), ), 'menu_order' => array( 'name' => _x( 'Menu order', 'Logger: Plugin ACF', 'simple-history' ), ), 'position' => array( 'name' => _x( 'Position', 'Logger: Plugin ACF', 'simple-history' ), ), 'active' => array( 'name' => _x( 'Active', 'Logger: Plugin ACF', 'simple-history' ), ), 'style' => array( 'name' => _x( 'Style', 'Logger: Plugin ACF', 'simple-history' ), ), ); foreach ( $arrKeys as $acf_key => $acfVals ) { if ( ! isset( $context[ "acf_new_$acf_key" ] ) || ! isset( $context[ "acf_prev_$acf_key" ] ) ) { continue; } $diff_table_output .= sprintf( '<tr> <td>%1$s</td> <td> <ins class="SimpleHistoryLogitem__keyValueTable__addedThing">%2$s</ins> <del class="SimpleHistoryLogitem__keyValueTable__removedThing">%3$s</del> </td> </tr>', $acfVals['name'], esc_html( $context[ "acf_new_$acf_key" ] ), esc_html( $context[ "acf_prev_$acf_key" ] ) ); } // If only acf_hide_on_screen_removed exists nothing is outputted. $acf_hide_on_screen_added = empty( $context['acf_hide_on_screen_added'] ) ? null : $context['acf_hide_on_screen_added']; $acf_hide_on_screen_removed = empty( $context['acf_hide_on_screen_removed'] ) ? null : $context['acf_hide_on_screen_removed']; if ( $acf_hide_on_screen_added || $acf_hide_on_screen_removed ) { $strCheckedHideOnScreen = ''; $strUncheckedHideOnScreen = ''; if ( $acf_hide_on_screen_added ) { $strCheckedHideOnScreen = sprintf( '%1$s %2$s', _x( 'Checked', 'Logger: Plugin ACF', 'simple-history' ), // 1 esc_html( $acf_hide_on_screen_added ) // 2 ); } if ( $acf_hide_on_screen_removed ) { $strUncheckedHideOnScreen = sprintf( '%1$s %2$s', _x( 'Unchecked', 'Logger: Plugin ACF', 'simple-history' ), // 1 esc_html( $acf_hide_on_screen_removed ) // 2 ); } $diff_table_output .= sprintf( '<tr> <td>%1$s</td> <td> %2$s %3$s </td> </tr>', _x( 'Hide on screen', 'Logger: Plugin ACF', 'simple-history' ), // 1 $strCheckedHideOnScreen, // 2 $strUncheckedHideOnScreen // 3 ); } // Check for deleted fields. if ( isset( $context['acf_deleted_fields_0_key'] ) ) { // 1 or more deleted fields exist in context. $loopnum = 0; $strDeletedFields = ''; while ( isset( $context[ "acf_deleted_fields_{$loopnum}_key" ] ) ) { $strDeletedFields .= sprintf( '%1$s (%3$s), ', esc_html( $context[ "acf_deleted_fields_{$loopnum}_label" ] ), esc_html( $context[ "acf_deleted_fields_{$loopnum}_name" ] ), esc_html( $context[ "acf_deleted_fields_{$loopnum}_type" ] ) ); ++$loopnum; } $strDeletedFields = trim( $strDeletedFields, ', ' ); $diff_table_output .= sprintf( '<tr> <td>%1$s</td> <td>%2$s</td> </tr>', _nx( 'Deleted field', 'Deleted fields', $loopnum, 'Logger: Plugin ACF', 'simple-history' ), // 1 $strDeletedFields ); } // if deleted fields // Check for added fields. if ( isset( $context['acf_added_fields_0_key'] ) ) { // 1 or more deleted fields exist in context $loopnum = 0; $strAddedFields = ''; while ( isset( $context[ "acf_added_fields_{$loopnum}_key" ] ) ) { $strAddedFields .= sprintf( '%1$s (%3$s), ', esc_html( $context[ "acf_added_fields_{$loopnum}_label" ] ), // 1 esc_html( $context[ "acf_added_fields_{$loopnum}_name" ] ), // 2 esc_html( $context[ "acf_added_fields_{$loopnum}_type" ] ) // 3 ); ++$loopnum; } $strAddedFields = trim( $strAddedFields, ', ' ); $diff_table_output .= sprintf( '<tr> <td>%1$s</td> <td>%2$s</td> </tr>', _nx( 'Added field', 'Added fields', $loopnum, 'Logger: Plugin ACF', 'simple-history' ), // 1 $strAddedFields ); } // if deleted fields // Check for modified fields. if ( isset( $context['acf_modified_fields_0_ID_prev'] ) ) { // 1 or more modifiedfields exist in context $loopnum = 0; $strModifiedFields = ''; $arrAddedFieldsKeysToCheck = array( 'name' => array( 'name' => _x( 'Name: ', 'Logger: Plugin ACF', 'simple-history' ), ), 'parent' => array( 'name' => _x( 'Parent: ', 'Logger: Plugin ACF', 'simple-history' ), ), 'key' => array( 'name' => _x( 'Key: ', 'Logger: Plugin ACF', 'simple-history' ), ), 'label' => array( 'name' => _x( 'Label: ', 'Logger: Plugin ACF', 'simple-history' ), ), 'type' => array( 'name' => _x( 'Type: ', 'Logger: Plugin ACF', 'simple-history' ), ), ); while ( isset( $context[ "acf_modified_fields_{$loopnum}_name_prev" ] ) ) { // One modified field, with one or more changed things. $strOneModifiedField = ''; // Add the label name manually, if it is not among the changed field, // or we don't know what field the other changed values belongs to. if ( empty( $context[ "acf_modified_fields_{$loopnum}_label_new" ] ) ) { $strOneModifiedField .= sprintf( // translators: %1$s is the label name. _x( 'Label: %1$s', 'Logger: Plugin ACF', 'simple-history' ), // 1 esc_html( $context[ "acf_modified_fields_{$loopnum}_label_prev" ] ) // 2 ); } // Check for other keys changed for this field. foreach ( $arrAddedFieldsKeysToCheck as $one_added_field_key_to_check => $one_added_field_key_to_check_vals ) { $newAndOldValsExists = isset( $context[ "acf_modified_fields_{$loopnum}_{$one_added_field_key_to_check}_new" ] ) && isset( $context[ "acf_modified_fields_{$loopnum}_{$one_added_field_key_to_check}_new" ] ); if ( ! $newAndOldValsExists ) { continue; } $strOneModifiedField .= sprintf( ' %4$s %3$s <ins class="SimpleHistoryLogitem__keyValueTable__addedThing">%1$s</ins> <del class="SimpleHistoryLogitem__keyValueTable__removedThing">%2$s</del> ', esc_html( $context[ "acf_modified_fields_{$loopnum}_{$one_added_field_key_to_check}_new" ] ), // 1 esc_html( $context[ "acf_modified_fields_{$loopnum}_{$one_added_field_key_to_check}_prev" ] ), // 2 esc_html( $one_added_field_key_to_check_vals['name'] ), // 3 empty( $strOneModifiedField ) ? '' : '<br>' // 4 new line ); } $strOneModifiedField = trim( $strOneModifiedField, ", \n\r\t" ); if ( $strOneModifiedField !== '' && $strOneModifiedField !== '0' ) { $strModifiedFields .= sprintf( '<tr> <td>%1$s</td> <td>%2$s</td> </tr>', _x( 'Modified field', 'Logger: Plugin ACF', 'simple-history' ), // 1 $strOneModifiedField ); } ++$loopnum; } $diff_table_output .= $strModifiedFields; } // if deleted fields return $diff_table_output; } /** * Append ACF data to post context. * * Called via filter `simple_history/post_logger/post_updated/context`. * * @param array $context Context. * @param \WP_Post $post Post. */ public function on_post_updated_context( $context, $post ) { // Only act if this is a ACF field group that is saved. if ( $post->post_type !== 'acf-field-group' ) { return $context; } // Remove some keys that we don't want, // for example the content because that's just a json string // in acf-field-group posts. unset( $context['post_prev_post_content'], $context['post_new_post_content'], $context['post_prev_post_name'], $context['post_new_post_name'], $context['post_prev_post_date'], $context['post_new_post_date'], $context['post_prev_post_date_gmt'], $context['post_new_post_date_gmt'] ); $acf_data_diff = array(); // 'fieldGroup' fields to check. $arr_field_group_keys_to_diff = array( 'menu_order', 'position', 'style', 'label_placement', 'instruction_placement', 'active', 'description', ); $fieldGroup = $this->old_and_new_field_groups_and_fields['fieldGroup']; foreach ( $arr_field_group_keys_to_diff as $key ) { if ( ! isset( $fieldGroup['old'][ $key ] ) || ! isset( $fieldGroup['new'][ $key ] ) ) { continue; } $acf_data_diff = $this->add_diff( $acf_data_diff, $key, (string) $fieldGroup['old'][ $key ], (string) $fieldGroup['new'][ $key ] ); } foreach ( $acf_data_diff as $diff_key => $diff_values ) { $context[ "acf_prev_{$diff_key}" ] = $diff_values['old']; $context[ "acf_new_{$diff_key}" ] = $diff_values['new']; } // Add checked or uncheckd hide on screen-items to context. $arrhHideOnScreenAdded = array(); $arrHideOnScreenRemoved = array(); $fieldGroup['new']['hide_on_screen'] = isset( $fieldGroup['new']['hide_on_screen'] ) && is_array( $fieldGroup['new']['hide_on_screen'] ) ? $fieldGroup['new']['hide_on_screen'] : array(); $fieldGroup['old']['hide_on_screen'] = isset( $fieldGroup['old']['hide_on_screen'] ) && is_array( $fieldGroup['old']['hide_on_screen'] ) ? $fieldGroup['old']['hide_on_screen'] : array(); $arrhHideOnScreenAdded = array_diff( $fieldGroup['new']['hide_on_screen'], $fieldGroup['old']['hide_on_screen'] ); $arrHideOnScreenRemoved = array_diff( $fieldGroup['old']['hide_on_screen'], $fieldGroup['new']['hide_on_screen'] ); if ( $arrhHideOnScreenAdded !== [] ) { $context['acf_hide_on_screen_added'] = implode( ',', $arrhHideOnScreenAdded ); } // Act when new or old hide_on_screen is set if ( $arrHideOnScreenRemoved !== [] ) { $context['acf_hide_on_screen_removed'] = implode( ',', $arrHideOnScreenRemoved ); } // Add removed fields to context if ( ! empty( $this->old_and_new_field_groups_and_fields['deletedFields'] ) && is_array( $this->old_and_new_field_groups_and_fields['deletedFields'] ) ) { $loopnum = 0; foreach ( $this->old_and_new_field_groups_and_fields['deletedFields'] as $oneDeletedField ) { $context[ "acf_deleted_fields_{$loopnum}_key" ] = $oneDeletedField['key']; $context[ "acf_deleted_fields_{$loopnum}_name" ] = $oneDeletedField['name']; $context[ "acf_deleted_fields_{$loopnum}_label" ] = $oneDeletedField['label']; $context[ "acf_deleted_fields_{$loopnum}_type" ] = $oneDeletedField['type']; ++$loopnum; } } // Add added fields to context if ( ! empty( $this->old_and_new_field_groups_and_fields['addedFields'] ) && is_array( $this->old_and_new_field_groups_and_fields['addedFields'] ) ) { $loopnum = 0; foreach ( $this->old_and_new_field_groups_and_fields['addedFields'] as $oneAddedField ) { // Id not available here, wold be nice to have // $context["acf_added_fields_{$loopnum}_ID"] = $oneAddedField['ID']; $context[ "acf_added_fields_{$loopnum}_key" ] = $oneAddedField['key']; $context[ "acf_added_fields_{$loopnum}_name" ] = $oneAddedField['name']; $context[ "acf_added_fields_{$loopnum}_label" ] = $oneAddedField['label']; $context[ "acf_added_fields_{$loopnum}_type" ] = $oneAddedField['type']; ++$loopnum; } } // Add modified fields to context. if ( ! empty( $this->old_and_new_field_groups_and_fields['modifiedFields']['old'] ) && ! empty( $this->old_and_new_field_groups_and_fields['modifiedFields']['new'] ) ) { $modifiedFields = $this->old_and_new_field_groups_and_fields['modifiedFields']; $arr_added_fields_keys_to_add = array( 'parent', 'key', 'label', 'name', 'type', ); $loopnum = 0; foreach ( $modifiedFields['old'] as $modifiedFieldId => $modifiedFieldValues ) { // Both old and new values mest exist. if ( empty( $modifiedFields['new'][ $modifiedFieldId ] ) ) { continue; } // Always add ID, name, and label. $context[ "acf_modified_fields_{$loopnum}_ID_prev" ] = $modifiedFields['old'][ $modifiedFieldId ]['ID']; $context[ "acf_modified_fields_{$loopnum}_name_prev" ] = $modifiedFields['old'][ $modifiedFieldId ]['name']; $context[ "acf_modified_fields_{$loopnum}_label_prev" ] = $modifiedFields['old'][ $modifiedFieldId ]['label']; foreach ( $arr_added_fields_keys_to_add as $one_key_to_add ) { // Check that new and old exist. $new_exists = isset( $modifiedFields['new'][ $modifiedFieldId ][ $one_key_to_add ] ); $old_exists = isset( $modifiedFields['old'][ $modifiedFieldId ][ $one_key_to_add ] ); if ( ! $new_exists || ! $old_exists ) { continue; } // Only add to context if modified. // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- Loose comparison intentional to avoid false diffs when types differ. if ( $modifiedFields['new'][ $modifiedFieldId ][ $one_key_to_add ] == $modifiedFields['old'][ $modifiedFieldId ][ $one_key_to_add ] ) { continue; } $context[ "acf_modified_fields_{$loopnum}_{$one_key_to_add}_prev" ] = $modifiedFields['old'][ $modifiedFieldId ][ $one_key_to_add ]; $context[ "acf_modified_fields_{$loopnum}_{$one_key_to_add}_new" ] = $modifiedFields['new'][ $modifiedFieldId ][ $one_key_to_add ]; } ++$loopnum; } } return $context; } /** * Add diff to array. * * @param array $post_data_diff Diff. * @param string $key Key. * @param string $old_value Old value. * @param string $new_value New value. * @return array */ public function add_diff( $post_data_diff, $key, $old_value, $new_value ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual -- Loose comparison intentional to avoid false diffs when types differ. if ( $old_value != $new_value ) { $post_data_diff[ $key ] = array( 'old' => $old_value, 'new' => $new_value, ); } return $post_data_diff; } /** * Store a version of the field group as it was before the save * Called before field group post/values is added to db * * @param array $data Post data. * @param array $postarr Post data. */ public function on_wp_insert_post_data( $data, $postarr ) { // Only do this if ACF field group is being saved. if ( $postarr['post_type'] !== 'acf-field-group' ) { return $data; } if ( empty( $postarr['ID'] ) ) { return $data; } // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( empty( $_POST['acf_field_group'] ) ) { return $data; } $this->old_and_new_field_groups_and_fields['fieldGroup']['old'] = acf_get_field_group( $postarr['ID'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing $this->old_and_new_field_groups_and_fields['fieldGroup']['new'] = acf_get_valid_field_group( sanitize_text_field( wp_unslash( $_POST['acf_field_group'] ) ) ); return $data; } /** * ACF field group is saved * Called before ACF calls its save_post filter * Here we save the new fields values and also get the old values so we can compare * * @param string $new_status New status. * @param string $old_status Old status. * @param \WP_Post $post Post. */ public function on_transition_post_status( $new_status, $old_status, $post ) { static $isCalled = false; if ( $isCalled ) { return; } $isCalled = true; $post_id = $post->ID; // do not act if this is an auto save routine. if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; } // bail early if not acf-field-group. if ( $post->post_type !== 'acf-field-group' ) { return; } // only save once! WordPress save's a revision as well. if ( wp_is_post_revision( $post_id ) ) { return; } // Store info about fields that are going to be deleted. // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( ! empty( $_POST['_acf_delete_fields'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $deletedFieldsIDs = explode( '|', (string) sanitize_text_field( wp_unslash( $_POST['_acf_delete_fields'] ) ) ); $deletedFieldsIDs = array_map( 'intval', $deletedFieldsIDs ); foreach ( $deletedFieldsIDs as $id ) { if ( $id === 0 ) { continue; } $field_info = acf_get_field( $id ); if ( ! $field_info ) { continue; } $this->old_and_new_field_groups_and_fields['deletedFields'][ $id ] = $field_info; } } // Store info about added or modified fields. // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( ! empty( $_POST['acf_fields'] ) && is_array( $_POST['acf_fields'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized foreach ( wp_unslash( $_POST['acf_fields'] ) as $oneFieldAddedOrUpdated ) { if ( empty( $oneFieldAddedOrUpdated['ID'] ) ) { // New fields have no id // 'ID' => string(0) "". $this->old_and_new_field_groups_and_fields['addedFields'][] = $oneFieldAddedOrUpdated; } else { // Existing fields have an id // 'ID' => string(3) "383". $this->old_and_new_field_groups_and_fields['modifiedFields']['old'][ $oneFieldAddedOrUpdated['ID'] ] = acf_get_field( $oneFieldAddedOrUpdated['ID'] ); $this->old_and_new_field_groups_and_fields['modifiedFields']['new'][ $oneFieldAddedOrUpdated['ID'] ] = $oneFieldAddedOrUpdated; } } } // We don't do anything else here, but we make the actual logging // in filter 'acf/update_field_group' because it's safer because // ACF has done it's validation and it's after ACF has saved the fields, // so less likely that we make some critical error. } /** * Add the post types that ACF uses for fields to the array of post types * that the default post logger should not log. If not each field will cause one * post update log message. * * @param array $skip_posttypes Array with post types to skip. * @return array Array with post types to skip. */ public function remove_acf_from_postlogger( $skip_posttypes ) { $skip_posttypes[] = 'acf-field'; return $skip_posttypes; } }