diff --git a/classes/connector.php b/classes/connector.php index 842a9fc7d..2ebf6f0e6 100644 --- a/classes/connector.php +++ b/classes/connector.php @@ -147,4 +147,51 @@ public static function delayed_log_commit() { } } + /** + * Compare two values and return changed keys if they are arrays + * @param mixed $old_value Value before change + * @param mixed $new_value Value after change + * @return array + */ + public static function get_changed_keys( $old_value, $new_value ) { + if ( ! is_array( $old_value ) && ! is_array( $new_value ) ) { + return array(); + } + + if ( ! is_array( $old_value ) ) { + return array_keys( $new_value ); + } + + if ( ! is_array( $new_value ) ) { + return array_keys( $old_value ); + } + + $diff = array_udiff_assoc( + $old_value, + $new_value, + function( $value1, $value2 ) { + return maybe_serialize( $value1 ) !== maybe_serialize( $value2 ); + } + ); + + $result = array_keys( $diff ); + + // find unexisting keys in old or new value + $common_keys = array_keys( array_intersect_key( $old_value, $new_value ) ); + $unique_keys_old = array_values( array_diff( array_keys( $old_value ), $common_keys ) ); + $unique_keys_new = array_values( array_diff( array_keys( $new_value ), $common_keys ) ); + $result = array_merge( $result, $unique_keys_old, $unique_keys_new ); + + // remove numeric indexes + $result = array_filter( + $result, + function( $value ) { + // check if is not valid number (is_int, is_numeric and ctype_digit are not enough) + return (string) (int) $value !== (string) $value; + } + ); + + return array_values( array_unique( $result ) ); + } + } diff --git a/connectors/settings.php b/connectors/settings.php index 119618375..a0ed17780 100644 --- a/connectors/settings.php +++ b/connectors/settings.php @@ -44,6 +44,14 @@ public static function register() { add_action( 'admin_head', array( __CLASS__, 'highlight_field' ) ); add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_jquery_color' ) ); + add_action( sprintf( 'update_option_theme_mods_%s', get_option( 'stylesheet' ) ), array( __CLASS__, 'log_theme_modification' ), 10, 2 ); + } + + /** + * @action update_option_theme_mods_{name} + */ + public static function log_theme_modification( $old_value, $new_value ) { + self::callback_updated_option( 'theme_mods', $old_value, $new_value ); } /** @@ -73,17 +81,72 @@ public static function get_action_labels() { */ public static function get_context_labels() { return array( - 'settings' => __( 'Settings', 'stream' ), - 'general' => __( 'General', 'stream' ), - 'writing' => __( 'Writing', 'stream' ), - 'reading' => __( 'Reading', 'stream' ), - 'discussion' => __( 'Discussion', 'stream' ), - 'media' => __( 'Media', 'stream' ), - 'permalink' => __( 'Permalinks', 'stream' ), - 'wp_stream' => __( 'Stream', 'stream' ), + 'settings' => __( 'Settings', 'stream' ), + 'general' => __( 'General', 'stream' ), + 'writing' => __( 'Writing', 'stream' ), + 'reading' => __( 'Reading', 'stream' ), + 'discussion' => __( 'Discussion', 'stream' ), + 'media' => __( 'Media', 'stream' ), + 'permalink' => __( 'Permalinks', 'stream' ), + 'wp_stream' => __( 'Stream', 'stream' ), + 'custom_background' => __( 'Custom Background', 'stream' ), + 'custom_header' => __( 'Custom Header', 'stream' ), ); } + /** + * Return context by option name and key + * + * @return string Context slug + */ + public static function get_context_by_key( $option_name, $key ) { + $contexts = array( + 'theme_mods' => array( + 'custom_background' => array( + 'background_image', + 'background_position_x', + 'background_repeat', + 'background_attachment', + 'background_color', + ), + 'custom_header' => array( + 'header_image', + 'header_textcolor', + ), + ), + ); + + if ( isset( $contexts[ $option_name ] ) ) { + foreach ( $contexts[ $option_name ] as $context => $keys ) { + if ( in_array( $key, $keys ) ) { + return $context; + } + } + } + + return false; + } + + /** + * Find out if the option key should be ignored and not logged + * + * @return bool Whether option key is ignored or not + */ + public static function is_key_ignored( $option_name, $key ) { + $ignored = array( + 'theme_mods' => array( + 'background_image_thumb', + 'header_image_data', + ), + ); + + if ( isset( $ignored[ $option_name ] ) ) { + return in_array( $key, $ignored[ $option_name ] ); + } + + return false; + } + /** * Return translated labels for all default Settings fields found in WordPress. * @@ -184,7 +247,17 @@ public static function enqueue_jquery_color() { */ public static function get_serialized_field_label( $option_name, $field_key ) { $labels = array( - // to be updated + 'theme_mods' => array( + // Custom Background + 'background_image' => __( 'Background Image', 'stream' ), + 'background_position_x' => __( 'Background Position', 'stream' ), + 'background_repeat' => __( 'Background Repeat', 'stream' ), + 'background_attachment' => __( 'Background Attachment', 'stream' ), + 'background_color' => __( 'Background Color', 'stream' ), + // Custom Header + 'header_image' => __( 'Header Image', 'stream' ), + 'header_textcolor' => __( 'Text Color', 'stream' ), + ), ); /** @@ -213,22 +286,61 @@ public static function get_serialized_field_label( $option_name, $field_key ) { public static function action_links( $links, $record ) { $context_labels = self::get_context_labels(); + $rules = array( + 'background_header' => array( + 'menu_slug' => 'themes.php', + 'submenu_slug' => function( $record ) { + return str_replace( '_', '-', $record->context ); + }, + 'url' => function( $rule, $record ) { + return add_query_arg( 'page', $rule['submenu_slug']( $record ), admin_url( $rule['menu_slug'] ) ); + }, + 'applicable' => function( $submenu, $record ) { + return in_array( $record->context, array( 'custom_header', 'custom_background' ) ); + } + ), + 'general' => array( + 'menu_slug' => 'options-general.php', + 'submenu_slug' => function( $record ) { + return sprintf( 'options-%s.php', $record->context ); + }, + 'url' => function( $rule, $record ) { + return admin_url( $rule['submenu_slug']( $record ) ); + }, + 'applicable' => function( $submenu, $record ) { + return ! empty( $submenu['options-general.php'] ); + }, + ), + ); + if ( 'settings' !== $record->context && in_array( $record->context, array_keys( $context_labels ) ) ) { global $submenu; - if ( ! empty( $submenu['options-general.php'] ) ) { - $submenu_slug = sprintf( 'options-%s.php', $record->context ); + $applicable_rules = array_filter( + $rules, + function( $rule ) use ( $submenu, $record ) { + return call_user_func( $rule['applicable'], $submenu, $record ); + } + ); + + if ( ! empty( $applicable_rules ) ) { + // The first applicable rule wins + $rule = array_shift( $applicable_rules ); + $menu_slug = $rule['menu_slug']; + $submenu_slug = $rule['submenu_slug']( $record ); + $url = $rule['url']( $rule, $record ); + $found_submenus = wp_list_filter( - $submenu['options-general.php'], + $submenu[ $menu_slug ], array( 2 => $submenu_slug ) ); if ( ! empty( $found_submenus ) ) { $target_submenu = array_pop( $found_submenus ); + list( $menu_title, $capability ) = $target_submenu; - if ( current_user_can( $target_submenu[1] ) ) { + if ( current_user_can( $capability ) ) { $text = sprintf( __( 'Edit %s Settings', 'stream' ), $context_labels[ $record->context ] ); - $url = admin_url( $submenu_slug ); $field_name = get_stream_meta( $record->ID, 'option', true ); if ( '' !== $field_name ) { @@ -288,7 +400,7 @@ public static function callback_update_option_tag_base( $old_value, $value ) { * @action updated_option */ public static function callback_updated_option( $option, $old_value, $value ) { - global $new_whitelist_options, $whitelist_options; + global $whitelist_options, $new_whitelist_options; if ( 0 === strpos( $option, '_transient_' ) ) { return; @@ -296,52 +408,42 @@ public static function callback_updated_option( $option, $old_value, $value ) { $options = array_merge( (array) $whitelist_options, - $new_whitelist_options, + (array) $new_whitelist_options, array( 'permalink' => self::$permalink_options ) ); foreach ( $options as $key => $opts ) { if ( in_array( $option, $opts ) ) { - $current_key = $key; + $context = $key; break; } } - if ( ! isset( $current_key ) ) { - $current_key = 'settings'; + if ( ! isset( $context ) ) { + $context = 'settings'; } $changed_options = array(); - if ( is_array( $old_value ) && is_array( $value ) ) { - $changed_keys = array(); - - // Added keys - $changed_keys = array_merge( $changed_keys, array_keys( array_diff_key( $value, $old_value ) ) ); - - // Deleted keys - $changed_keys = array_merge( $changed_keys, array_keys( array_diff_key( $old_value, $value ) ) ); - - // array_diff_assoc is not sufficient - foreach ( array_diff( array_keys( $value ), $changed_keys ) as $option_key ) { - if ( $value[ $option_key ] !== $old_value[ $option_key ] ) { - $changed_keys[] = $option_key; + if ( is_array( $old_value ) || is_array( $value ) ) { + foreach ( self::get_changed_keys( $old_value, $value ) as $field_key ) { + if ( ! self::is_key_ignored( $option, $field_key ) ) { + $key_context = self::get_context_by_key( $option, $field_key ); + $changed_options[] = array( + 'label' => self::get_serialized_field_label( $option, $field_key ), + 'option' => $option, + 'context' => ( false !== $key_context ? $key_context : $context ), + // Prevent fatal error when saving option as array + 'old_value' => isset( $old_value[ $field_key ] ) ? maybe_serialize( $old_value[ $field_key ] ) : null, + 'value' => isset( $value[ $field_key ] ) ? maybe_serialize( $value[ $field_key ] ) : null, + ); } } - - foreach ( $changed_keys as $field_key ) { - $changed_options[] = array( - 'label' => self::get_serialized_field_label( $option, $field_key ), - 'option' => $current_key, - // Prevent fatal error when saving option as array - 'old_value' => isset( $old_value[ $field_key ] ) ? maybe_serialize( $old_value[ $field_key ] ) : null, - 'value' => isset( $value[ $field_key ] ) ? maybe_serialize( $value[ $field_key ] ) : null, - ); - } } else { $changed_options[] = array( 'label' => self::get_field_label( $option ), 'option' => $option, + 'context' => $context, // Prevent fatal error when saving option as array 'old_value' => maybe_serialize( $old_value ), 'value' => maybe_serialize( $value ), @@ -349,11 +451,12 @@ public static function callback_updated_option( $option, $old_value, $value ) { } foreach ( $changed_options as $properties ) { + $context = $properties['context']; self::log( __( '"%s" setting was updated', 'stream' ), $properties, null, - array( $current_key => 'updated' ) + array( $context => 'updated' ) ); } }