array( 'name' => Messages::get( 'revisions.post_type_name' ), 'singular_name' => Messages::get( 'revisions.post_type_singular' ), ), 'public' => false, 'show_ui' => true, 'show_in_menu' => false, 'show_in_admin_bar' => false, 'show_in_nav_menus' => false, 'exclude_from_search' => true, 'publicly_queryable' => false, 'rewrite' => false, 'supports' => array( 'title', 'editor', 'revisions' ), ) ); } /** * Add Parallax meta keys to revision tracking. * * @param array $meta_keys Revision meta keys. * @return array */ public static function register_revision_meta_keys( array $meta_keys ): array { $meta_keys[] = SeoMetaFields::META_TITLE_KEY; $meta_keys[] = SeoMetaFields::META_DESC_KEY; return array_values( array_unique( $meta_keys ) ); } /** * Append Parallax meta diffs to the revision UI. * * Uses the `wp_get_revision_ui_diff` filter in `wp-admin/includes/revision.php` * to add custom diff blocks for Parallax meta fields in the core revisions UI. * * @param array[] $return_array Array of revision UI fields. * @param \WP_Post|false $compare_from Revision post to compare from, or false. * @param \WP_Post $compare_to Revision post or current post to compare to. * @return array[] */ public static function add_parallax_meta_to_revision_ui_diff( array $return_array, $compare_from, \WP_Post $compare_to ): array { $parent_post = $compare_to; if ( 'revision' === $compare_to->post_type ) { $parent_post = get_post( $compare_to->post_parent ); } if ( ! $parent_post instanceof \WP_Post ) { return $return_array; } if ( ! in_array( $parent_post->post_type, self::get_supported_post_types(), true ) ) { return $return_array; } $from_title = self::get_revision_meta_value( $compare_from, SeoMetaFields::META_TITLE_KEY ); $to_title = self::get_revision_meta_value( $compare_to, SeoMetaFields::META_TITLE_KEY ); $from_desc = self::get_revision_meta_value( $compare_from, SeoMetaFields::META_DESC_KEY ); $to_desc = self::get_revision_meta_value( $compare_to, SeoMetaFields::META_DESC_KEY ); if ( $from_title === $to_title && $from_desc === $to_desc ) { return $return_array; } $title_diff = self::build_meta_diff( 'parallax_meta_title', $from_title, $to_title, $compare_from, $compare_to ); if ( $title_diff ) { $return_array[] = array( 'id' => 'parallax_meta_title', 'name' => Messages::get( 'revisions.meta_title' ), 'diff' => $title_diff, ); } $desc_diff = self::build_meta_diff( 'parallax_meta_desc', $from_desc, $to_desc, $compare_from, $compare_to ); if ( $desc_diff ) { $return_array[] = array( 'id' => 'parallax_meta_desc', 'name' => Messages::get( 'revisions.meta_desc' ), 'diff' => $desc_diff, ); } return $return_array; } /** * Restore Parallax meta fields when a revision is restored. * * @param int $post_id Parent post ID. * @param int $revision_id Revision post ID. */ public static function restore_revision_meta( int $post_id, int $revision_id ): void { $post = get_post( $post_id ); if ( ! $post instanceof \WP_Post ) { return; } if ( ! in_array( $post->post_type, self::get_supported_post_types(), true ) ) { return; } foreach ( self::get_revision_meta_keys() as $meta_key ) { $value = get_post_meta( $revision_id, $meta_key, true ); if ( '' !== $value && null !== $value ) { update_post_meta( $post_id, $meta_key, $value ); } else { delete_post_meta( $post_id, $meta_key ); } } if ( self::SHADOW_POST_TYPE === $post->post_type ) { self::restore_term_from_shadow_post( $post_id ); } } /** * Sync term data to a shadow post and create a revision if changes occurred. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param mixed $taxonomy Taxonomy slug (string expected). */ public static function sync_term_to_shadow_post( int $term_id, int $tt_id = 0, $taxonomy = '' ): void { if ( self::$is_restoring ) { return; } $taxonomy_original = $taxonomy; if ( is_array( $taxonomy ) ) { $taxonomy = 'product_cat'; if ( Settings::get( 'enable_debug_support', false ) ) { try { Logger::write_log( 'warning', '[Revisions] sync_term_to_shadow_post taxonomy array; defaulted to product_cat.', array( 'term_id' => $term_id, 'tt_id' => $tt_id, 'taxonomy_type' => gettype( $taxonomy_original ), ) ); } catch ( \Throwable $e ) { unset( $e ); // Avoid failing taxonomy hooks when logging is unavailable. } } } elseif ( is_string( $taxonomy ) ) { $taxonomy = sanitize_key( $taxonomy ); if ( '' === $taxonomy ) { $taxonomy = 'product_cat'; } } else { $taxonomy = 'product_cat'; if ( Settings::get( 'enable_debug_support', false ) ) { try { Logger::write_log( 'warning', '[Revisions] sync_term_to_shadow_post invalid taxonomy; defaulted to product_cat.', array( 'term_id' => $term_id, 'tt_id' => $tt_id, 'taxonomy_type' => gettype( $taxonomy_original ), ) ); } catch ( \Throwable $e ) { unset( $e ); // Avoid failing taxonomy hooks when logging is unavailable. } } } if ( ! self::is_supported_taxonomy( $taxonomy ) ) { return; } if ( ! current_user_can( 'edit_term', $term_id ) ) { return; } $term = get_term( $term_id, $taxonomy ); if ( ! $term instanceof \WP_Term ) { return; } $shadow_post_id = self::get_shadow_post_id( $term_id, $taxonomy, true ); if ( ! $shadow_post_id ) { return; } $shadow_post = get_post( $shadow_post_id ); if ( ! $shadow_post instanceof \WP_Post ) { return; } $new_title = sanitize_text_field( $term->name ); $new_content = wp_kses_post( $term->description ); $meta_title = get_term_meta( $term_id, SeoMetaFields::TERM_META_TITLE_KEY, true ); $meta_title = is_string( $meta_title ) ? sanitize_text_field( $meta_title ) : ''; $meta_desc = get_term_meta( $term_id, SeoMetaFields::TERM_META_DESC_KEY, true ); $meta_desc = self::sanitize_meta_description( $meta_desc ); $changed = false; if ( $shadow_post->post_title !== $new_title || $shadow_post->post_content !== $new_content ) { $updated = wp_update_post( array( 'ID' => $shadow_post_id, 'post_title' => $new_title, 'post_content' => $new_content, ), true ); if ( ! is_wp_error( $updated ) ) { $changed = true; } } $shadow_meta_title = (string) get_post_meta( $shadow_post_id, SeoMetaFields::META_TITLE_KEY, true ); if ( $shadow_meta_title !== $meta_title ) { if ( '' !== $meta_title ) { update_post_meta( $shadow_post_id, SeoMetaFields::META_TITLE_KEY, $meta_title ); } else { delete_post_meta( $shadow_post_id, SeoMetaFields::META_TITLE_KEY ); } $changed = true; } $shadow_meta_desc = (string) get_post_meta( $shadow_post_id, SeoMetaFields::META_DESC_KEY, true ); if ( $shadow_meta_desc !== $meta_desc ) { if ( '' !== $meta_desc ) { update_post_meta( $shadow_post_id, SeoMetaFields::META_DESC_KEY, $meta_desc ); } else { delete_post_meta( $shadow_post_id, SeoMetaFields::META_DESC_KEY ); } $changed = true; } if ( $changed && post_type_supports( $shadow_post->post_type, 'revisions' ) ) { $force_revision_for_shadow = static function ( $check, $last_revision, $post = null ) use ( $shadow_post_id ) { // WordPress 6.4+ passes the actual post as the third parameter. if ( $post instanceof \WP_Post && (int) $post->ID === (int) $shadow_post_id ) { // Always create a revision for the shadow post when syncing term SEO data. return false; } // On older WordPress versions, the second parameter is the last revision. if ( $last_revision instanceof \WP_Post && (int) $last_revision->post_parent === (int) $shadow_post_id ) { // Always create a revision for the shadow post when syncing term SEO data. return false; } return $check; }; add_filter( 'wp_save_post_revision_check_for_changes', $force_revision_for_shadow, 10, 3 ); wp_save_post_revision( $shadow_post_id ); remove_filter( 'wp_save_post_revision_check_for_changes', $force_revision_for_shadow, 10 ); } } /** * Render a "View Revisions" link on the term edit screen. * * @param \WP_Term $term Term object. */ public static function render_term_revisions_link( \WP_Term $term ): void { if ( ! self::is_supported_taxonomy( $term->taxonomy ) ) { return; } if ( ! current_user_can( 'edit_term', $term->term_id ) ) { return; } $shadow_post_id = self::get_shadow_post_id( $term->term_id, $term->taxonomy, true ); if ( ! $shadow_post_id ) { return; } $url = add_query_arg( array( 'post' => $shadow_post_id, 'action' => 'edit', ), admin_url( 'post.php' ) ); ?>