✏️ 编辑:class-site-editor-logger.php
路径:
/home/forge/kingkrunch.com/wp/wp-content/plugins/simple-history/loggers/class-site-editor-logger.php
大小:26.4 KB · 修改:2026-09-25 00:42:29 · 权限:0644 · 可写
← 返回目录
👁 查看
⬇ 下载
<?php namespace Simple_History\Loggers; use Simple_History\Event_Details\Event_Details_Group; use Simple_History\Event_Details\Event_Details_Group_Table_Formatter; use Simple_History\Event_Details\Event_Details_Item; /** * Logs changes made in the Site Editor (Full Site Editing): * templates, template parts, site-wide styles (global styles), * patterns, navigation menus, and fonts. */ class Site_Editor_Logger extends Logger { /** @var string Logger slug */ public $slug = 'SiteEditorLogger'; /** * Post types used by the Site Editor that this logger handles. * * @var array<string> */ private const FSE_POST_TYPES = [ 'wp_template', 'wp_template_part', 'wp_global_styles', 'wp_block', 'wp_navigation', 'wp_font_family', 'wp_font_face', ]; /** * Taxonomies the Site Editor uses as internal plumbing. * * WordPress creates these terms itself to connect a template to its * theme and a template part to its area. They are not choices the user * made, so logging them alongside the Site Editor event is noise. * * @var array<string> */ private const FSE_TAXONOMIES = [ 'wp_theme', 'wp_template_part_area', ]; /** * IDs of font families installed during the current request. * * Installing a family posts the family and then each of its font faces * as separate requests. The faces are part of that one install, not * separate user actions, so they are not logged on their own. * * @var array<int,bool> */ private $created_font_family_ids = []; /** * IDs of font families being deleted during the current request, * used to skip logging the cascading deletion of their font faces. * * @var array<int,bool> */ private $deleted_font_family_ids = []; /** * How deeply nested the current REST dispatch is, if any. * * Saves made while serving a REST request are logged from * rest_after_insert_* instead of save_post, because meta and terms * are not stored yet when save_post runs. Tracked with a counter * rather than the REST_REQUEST constant, which is only defined when * WordPress serves an HTTP request and not when code dispatches a * request internally with rest_do_request(). * * @var int */ private $rest_dispatch_depth = 0; /** * Whether a post is currently being restored from the trash. * * Restoring updates the post status, which fires save_post. That save * is not an edit the user made, so it is skipped in favour of the * restore message. * * @var bool */ private $is_untrashing = false; /** * Term and meta data captured just before a post is deleted, * because WordPress removes terms and meta before the delete_post * action fires. Keyed by post ID. * * @var array<int,array<string,string>> */ private $pre_delete_data = []; /** * Get array with information about this logger. * * @return array */ public function get_info() { return [ 'name' => __( 'Site Editor Logger', 'simple-history' ), 'description' => __( 'Logs changes made in the Site Editor: templates, template parts, site-wide styles, patterns, navigation menus, and fonts', 'simple-history' ), // Site Editor post types were previously logged by Post_Logger, // which uses edit_pages. Keep the same capability so users who // could read these events before can still read them. 'capability' => 'edit_pages', 'messages' => [ 'template_created' => __( 'Created template "{post_title}"', 'simple-history' ), 'template_updated' => __( 'Updated template "{post_title}"', 'simple-history' ), 'template_deleted' => __( 'Deleted template "{post_title}"', 'simple-history' ), 'template_reset' => __( 'Reset template "{post_title}" to theme default', 'simple-history' ), 'template_part_created' => __( 'Created template part "{post_title}"', 'simple-history' ), 'template_part_updated' => __( 'Updated template part "{post_title}"', 'simple-history' ), 'template_part_deleted' => __( 'Deleted template part "{post_title}"', 'simple-history' ), 'template_part_reset' => __( 'Reset template part "{post_title}" to theme default', 'simple-history' ), 'global_styles_updated' => __( 'Updated site-wide styles (global styles)', 'simple-history' ), 'synced_pattern_created' => __( 'Created synced pattern "{post_title}"', 'simple-history' ), 'synced_pattern_updated' => __( 'Updated synced pattern "{post_title}"', 'simple-history' ), 'synced_pattern_deleted' => __( 'Deleted synced pattern "{post_title}"', 'simple-history' ), 'synced_pattern_trashed' => __( 'Moved synced pattern "{post_title}" to the trash', 'simple-history' ), 'synced_pattern_restored' => __( 'Restored synced pattern "{post_title}" from the trash', 'simple-history' ), 'unsynced_pattern_created' => __( 'Created pattern "{post_title}"', 'simple-history' ), 'unsynced_pattern_updated' => __( 'Updated pattern "{post_title}"', 'simple-history' ), 'unsynced_pattern_deleted' => __( 'Deleted pattern "{post_title}"', 'simple-history' ), 'unsynced_pattern_trashed' => __( 'Moved pattern "{post_title}" to the trash', 'simple-history' ), 'unsynced_pattern_restored' => __( 'Restored pattern "{post_title}" from the trash', 'simple-history' ), 'navigation_menu_created' => __( 'Created navigation menu "{post_title}"', 'simple-history' ), 'navigation_menu_updated' => __( 'Updated navigation menu "{post_title}"', 'simple-history' ), 'navigation_menu_deleted' => __( 'Deleted navigation menu "{post_title}"', 'simple-history' ), 'navigation_menu_trashed' => __( 'Moved navigation menu "{post_title}" to the trash', 'simple-history' ), 'navigation_menu_restored' => __( 'Restored navigation menu "{post_title}" from the trash', 'simple-history' ), 'font_family_created' => __( 'Installed font family "{font_family_name}"', 'simple-history' ), 'font_family_updated' => __( 'Updated font family "{font_family_name}"', 'simple-history' ), 'font_family_deleted' => __( 'Deleted font family "{font_family_name}"', 'simple-history' ), 'font_face_created' => __( 'Added font face "{font_face_name}" to font family "{font_family_name}"', 'simple-history' ), 'font_face_deleted' => __( 'Removed font face "{font_face_name}" from font family "{font_family_name}"', 'simple-history' ), ], 'labels' => [ 'search' => [ 'label' => _x( 'Site Editor', 'Site Editor logger: search', 'simple-history' ), 'label_all' => _x( 'All Site Editor activity', 'Site Editor logger: search', 'simple-history' ), 'options' => [ _x( 'Templates changed', 'Site Editor logger: search', 'simple-history' ) => [ 'template_created', 'template_updated', 'template_deleted', 'template_reset', 'template_part_created', 'template_part_updated', 'template_part_deleted', 'template_part_reset', ], _x( 'Site-wide styles changed', 'Site Editor logger: search', 'simple-history' ) => [ 'global_styles_updated', ], _x( 'Patterns changed', 'Site Editor logger: search', 'simple-history' ) => [ 'synced_pattern_created', 'synced_pattern_updated', 'synced_pattern_deleted', 'synced_pattern_trashed', 'unsynced_pattern_created', 'unsynced_pattern_updated', 'unsynced_pattern_deleted', 'unsynced_pattern_trashed', 'synced_pattern_restored', 'unsynced_pattern_restored', ], _x( 'Navigation menus changed', 'Site Editor logger: search', 'simple-history' ) => [ 'navigation_menu_created', 'navigation_menu_updated', 'navigation_menu_deleted', 'navigation_menu_trashed', 'navigation_menu_restored', ], _x( 'Fonts changed', 'Site Editor logger: search', 'simple-history' ) => [ 'font_family_created', 'font_family_updated', 'font_family_deleted', 'font_face_created', 'font_face_deleted', ], ], ], ], ]; } /** * Called when logger is loaded. */ public function loaded() { // Tell Post_Logger to not log Site Editor post types, this logger handles them. add_filter( 'simple_history/post_logger/skip_posttypes', [ $this, 'on_post_logger_skip_posttypes' ] ); add_filter( 'simple_history/categories_logger/skip_taxonomies', [ $this, 'on_categories_logger_skip_taxonomies' ] ); // The Site Editor saves everything through the REST API, but the same // post types can also be changed by WP-CLI, an importer or plugin code, // so listen for both and let the handlers agree on which one logs. foreach ( self::FSE_POST_TYPES as $post_type ) { add_action( "rest_after_insert_{$post_type}", [ $this, 'on_rest_after_insert' ], 10, 3 ); add_action( "save_post_{$post_type}", [ $this, 'on_save_post' ], 10, 3 ); } add_filter( 'rest_pre_dispatch', [ $this, 'on_rest_pre_dispatch' ] ); add_filter( 'rest_post_dispatch', [ $this, 'on_rest_post_dispatch' ] ); add_action( 'trashed_post', [ $this, 'on_trashed_post' ] ); add_action( 'untrash_post', [ $this, 'on_untrash_post' ] ); add_action( 'untrashed_post', [ $this, 'on_untrashed_post' ] ); add_action( 'before_delete_post', [ $this, 'on_before_delete_post' ], 10, 2 ); add_action( 'delete_post', [ $this, 'on_delete_post' ], 10, 2 ); } /** * Add Site Editor post types to the list of post types * that the regular post logger should not log. * * @param array<string> $skip_posttypes Post types to skip. * @return array<string> */ public function on_post_logger_skip_posttypes( $skip_posttypes ) { return array_merge( $skip_posttypes, self::FSE_POST_TYPES ); } /** * Add the Site Editor's internal taxonomies to the list of taxonomies * that the categories logger should not log. * * Without this the first Site Editor save on a site logs a term * creation for the active theme next to the Site Editor event itself. * * @param array<string> $skip_taxonomies Taxonomies to skip. * @return array<string> */ public function on_categories_logger_skip_taxonomies( $skip_taxonomies ) { return array_merge( $skip_taxonomies, self::FSE_TAXONOMIES ); } /** * Remember that a REST request started being dispatched. * * @param mixed $result Dispatch result, passed through untouched. * @return mixed */ public function on_rest_pre_dispatch( $result ) { ++$this->rest_dispatch_depth; return $result; } /** * Remember that a REST request finished being dispatched. * * @param mixed $response Dispatch response, passed through untouched. * @return mixed */ public function on_rest_post_dispatch( $response ) { if ( $this->rest_dispatch_depth > 0 ) { --$this->rest_dispatch_depth; } return $response; } /** * Fired before a post is restored from the trash. * * @param int $post_id Post ID. */ public function on_untrash_post( $post_id ) { $this->is_untrashing = true; } /** * Fired after a Site Editor post is created or updated via the REST API. * * This is the path the Site Editor itself uses. It runs after meta and * terms have been saved, so the area of a template part and the sync * status of a pattern are readable here but not yet during save_post. * * @param \WP_Post $post Inserted or updated post object. * @param \WP_REST_Request $request Request object. * @param bool $creating True when creating a post, false when updating. */ public function on_rest_after_insert( $post, $request, $creating ) { $this->log_post_change( $post, $creating ); } /** * Fired when a Site Editor post is saved outside the REST API, * for example by WP-CLI, an importer or plugin code. * * REST saves are skipped here because on_rest_after_insert() logs those * with fuller context. Without this hook such changes would go unlogged * entirely, since Post_Logger no longer handles these post types. * * @param int $post_id Post ID. * @param \WP_Post $post Post object. * @param bool $update True when updating an existing post. */ public function on_save_post( $post_id, $post, $update ) { if ( $this->rest_dispatch_depth > 0 ) { return; } if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) { return; } // Trashing and restoring both change the post status, which fires // save_post. Those are logged as trash and restore instead. if ( $post->post_status === 'trash' || $this->is_untrashing ) { return; } $this->log_post_change( $post, ! $update ); } /** * Log the creation or update of a Site Editor post. * * @param \WP_Post $post Inserted or updated post object. * @param bool $creating True when the post was just created. */ private function log_post_change( $post, $creating ) { if ( ! $post instanceof \WP_Post ) { return; } if ( in_array( $post->post_status, [ 'auto-draft', 'inherit' ], true ) ) { return; } switch ( $post->post_type ) { case 'wp_template': $this->info_message( $creating ? 'template_created' : 'template_updated', $this->get_template_context( $post ) ); break; case 'wp_template_part': $this->info_message( $creating ? 'template_part_created' : 'template_part_updated', $this->get_template_context( $post ) ); break; case 'wp_global_styles': // Global styles posts are auto-created by WordPress, // so the only user action is an update. $this->info_message( 'global_styles_updated', [ 'post_id' => $post->ID, 'theme' => $this->get_theme_name_for_post( $post ), ] ); break; case 'wp_block': $sync_status = $this->get_pattern_sync_status( $post ); $message_prefix = $sync_status === 'synced' ? 'synced_pattern' : 'unsynced_pattern'; $this->info_message( $message_prefix . ( $creating ? '_created' : '_updated' ), [ 'post_id' => $post->ID, 'post_title' => $post->post_title, 'pattern_sync_status' => $sync_status, ] ); break; case 'wp_navigation': $this->info_message( $creating ? 'navigation_menu_created' : 'navigation_menu_updated', [ 'post_id' => $post->ID, 'post_title' => $post->post_title, ] ); break; case 'wp_font_family': if ( $creating ) { $this->created_font_family_ids[ $post->ID ] = true; } $this->info_message( $creating ? 'font_family_created' : 'font_family_updated', [ 'post_id' => $post->ID, 'font_family_name' => $this->get_font_family_name( $post ), ] ); break; case 'wp_font_face': // A face added to a family installed moments ago is part of // that install, which is already logged as one event. if ( $creating && ! isset( $this->created_font_family_ids[ $post->post_parent ] ) ) { $this->info_message( 'font_face_created', $this->get_font_face_context( $post ) ); } break; } } /** * Fired after a post is moved to the trash. * * Patterns and navigation menus support trash, * the other Site Editor post types are deleted directly. * * @param int $post_id Post ID. */ public function on_trashed_post( $post_id ) { $post = get_post( $post_id ); if ( ! $post instanceof \WP_Post ) { return; } if ( $post->post_type === 'wp_block' ) { $sync_status = $this->get_pattern_sync_status( $post ); $message_prefix = $sync_status === 'synced' ? 'synced_pattern' : 'unsynced_pattern'; $this->info_message( $message_prefix . '_trashed', [ 'post_id' => $post->ID, 'post_title' => $post->post_title, 'pattern_sync_status' => $sync_status, ] ); } elseif ( $post->post_type === 'wp_navigation' ) { $this->info_message( 'navigation_menu_trashed', [ 'post_id' => $post->ID, 'post_title' => $post->post_title, ] ); } } /** * Fired after a post is restored from the trash. * * Mirrors on_trashed_post() so a restore is visible in the log, * the way the trashing of the same post is. * * @param int $post_id Post ID. */ public function on_untrashed_post( $post_id ) { $this->is_untrashing = false; $post = get_post( $post_id ); if ( ! $post instanceof \WP_Post ) { return; } if ( $post->post_type === 'wp_block' ) { $sync_status = $this->get_pattern_sync_status( $post ); $message_prefix = $sync_status === 'synced' ? 'synced_pattern' : 'unsynced_pattern'; $this->info_message( $message_prefix . '_restored', [ 'post_id' => $post->ID, 'post_title' => $post->post_title, 'pattern_sync_status' => $sync_status, ] ); } elseif ( $post->post_type === 'wp_navigation' ) { $this->info_message( 'navigation_menu_restored', [ 'post_id' => $post->ID, 'post_title' => $post->post_title, ] ); } } /** * Capture term and meta data for a Site Editor post before deletion starts, * because WordPress removes terms and meta before the delete_post action fires. * * @param int $post_id Post ID. * @param \WP_Post $post Post object. */ public function on_before_delete_post( $post_id, $post ) { if ( ! $post instanceof \WP_Post ) { return; } if ( in_array( $post->post_type, [ 'wp_template', 'wp_template_part' ], true ) ) { $this->pre_delete_data[ $post_id ] = [ 'theme_slug' => $this->get_post_term_slug( $post, 'wp_theme' ), 'template_part_area' => $post->post_type === 'wp_template_part' ? $this->get_post_term_slug( $post, 'wp_template_part_area' ) : '', ]; } elseif ( $post->post_type === 'wp_block' ) { $sync_status = get_post_meta( $post_id, 'wp_pattern_sync_status', true ); $this->pre_delete_data[ $post_id ] = [ 'pattern_sync_status' => $sync_status === 'unsynced' ? 'unsynced' : 'synced', ]; } } /** * Fired just before a post is deleted from the database. * * @param int $post_id Post ID. * @param \WP_Post $post Post object. */ public function on_delete_post( $post_id, $post ) { if ( ! $post instanceof \WP_Post ) { return; } if ( ! in_array( $post->post_type, self::FSE_POST_TYPES, true ) ) { return; } if ( in_array( $post->post_status, [ 'auto-draft', 'inherit' ], true ) ) { return; } switch ( $post->post_type ) { case 'wp_template': case 'wp_template_part': $this->log_template_deletion( $post ); break; case 'wp_global_styles': // Global styles posts are only deleted by WordPress itself, // for example during theme cleanup, so skip logging. break; case 'wp_block': $sync_status = $this->get_pattern_sync_status( $post ); $message_prefix = $sync_status === 'synced' ? 'synced_pattern' : 'unsynced_pattern'; $this->info_message( $message_prefix . '_deleted', [ 'post_id' => $post->ID, 'post_title' => $post->post_title, 'pattern_sync_status' => $sync_status, ] ); break; case 'wp_navigation': $this->info_message( 'navigation_menu_deleted', [ 'post_id' => $post->ID, 'post_title' => $post->post_title, ] ); break; case 'wp_font_family': // Remember the ID so the cascading deletion of the // font faces that belong to this family is not logged. $this->deleted_font_family_ids[ $post->ID ] = true; $this->info_message( 'font_family_deleted', [ 'post_id' => $post->ID, 'font_family_name' => $this->get_font_family_name( $post ), ] ); break; case 'wp_font_face': // When a whole font family is deleted WordPress also deletes // its font faces. Skip those to avoid logging one event per face. if ( ! isset( $this->deleted_font_family_ids[ $post->post_parent ] ) ) { $this->info_message( 'font_face_deleted', $this->get_font_face_context( $post ) ); } break; } // The captured data has served its purpose now that the event is // logged. Dropping it keeps the array from growing through a bulk // delete, and stops a stale entry being read later in the request. unset( $this->pre_delete_data[ $post_id ] ); } /** * Log deletion of a template or template part. * * Deleting a customized template that the theme provides a default for * means the template is reset to the theme default, not removed from the site. * * @param \WP_Post $post Template or template part post object. */ private function log_template_deletion( $post ) { $is_template_part = $post->post_type === 'wp_template_part'; if ( $this->is_theme_provided_template( $post ) ) { $message_key = $is_template_part ? 'template_part_reset' : 'template_reset'; } else { $message_key = $is_template_part ? 'template_part_deleted' : 'template_deleted'; } $this->info_message( $message_key, $this->get_template_context( $post ) ); } /** * Check if the active theme provides a default file for a template or template part, * meaning a deletion of the post is a "reset to theme default". * * @param \WP_Post $post Template or template part post object. * @return bool */ private function is_theme_provided_template( $post ) { if ( ! function_exists( '_get_block_template_file' ) ) { return false; } // Templates are connected to a theme via the wp_theme taxonomy. // Only the active theme's files can provide a default. $theme_slug = $this->get_theme_slug_for_post( $post ); if ( $theme_slug !== '' && get_stylesheet() !== $theme_slug ) { return false; } return _get_block_template_file( $post->post_type, $post->post_name ) !== null; } /** * Get log context for a template or template part. * * @param \WP_Post $post Template or template part post object. * @return array<string,mixed> */ private function get_template_context( $post ) { $context = [ 'post_id' => $post->ID, 'post_title' => $post->post_title, 'template_slug' => $post->post_name, 'theme' => $this->get_theme_name_for_post( $post ), ]; if ( $post->post_type === 'wp_template_part' ) { $area = $this->pre_delete_data[ $post->ID ]['template_part_area'] ?? $this->get_post_term_slug( $post, 'wp_template_part_area' ); if ( $area !== '' ) { $context['template_part_area'] = $area; } } return $context; } /** * Get log context for a font face, including the parent font family name * and a readable variant name like "400 italic". * * @param \WP_Post $post Font face post object. * @return array<string,mixed> */ private function get_font_face_context( $post ) { $settings = json_decode( $post->post_content, true ); $weight = is_array( $settings ) && isset( $settings['fontWeight'] ) ? (string) $settings['fontWeight'] : ''; $style = is_array( $settings ) && isset( $settings['fontStyle'] ) ? (string) $settings['fontStyle'] : ''; $font_face_name = trim( "{$weight} {$style}" ); if ( $font_face_name === '' ) { $font_face_name = $post->post_title; } $parent_font_family = get_post( $post->post_parent ); $context = [ 'post_id' => $post->ID, 'font_face_name' => $font_face_name, 'font_family_name' => $parent_font_family instanceof \WP_Post ? $this->get_font_family_name( $parent_font_family ) : '', ]; if ( $weight !== '' ) { $context['font_face_weight'] = $weight; } if ( $style !== '' ) { $context['font_face_style'] = $style; } return $context; } /** * Get the readable name of a font family. * * The post title holds the name, with the settings JSON * in post content as fallback. * * @param \WP_Post $post Font family post object. * @return string */ private function get_font_family_name( $post ) { if ( $post->post_title !== '' ) { return $post->post_title; } $settings = json_decode( $post->post_content, true ); if ( is_array( $settings ) && ! empty( $settings['name'] ) ) { return (string) $settings['name']; } return $post->post_name; } /** * Get the readable theme name for a Site Editor post, * based on its wp_theme taxonomy term. * * @param \WP_Post $post Post object. * @return string */ private function get_theme_name_for_post( $post ) { $theme_slug = $this->get_theme_slug_for_post( $post ); if ( $theme_slug === '' ) { $theme_slug = get_stylesheet(); } $theme = wp_get_theme( $theme_slug ); if ( $theme->exists() ) { return $theme->get( 'Name' ); } return $theme_slug; } /** * Get the theme slug (stylesheet) a Site Editor post belongs to, * preferring data captured before deletion. * * @param \WP_Post $post Post object. * @return string Theme slug or empty string. */ private function get_theme_slug_for_post( $post ) { if ( isset( $this->pre_delete_data[ $post->ID ]['theme_slug'] ) ) { return $this->pre_delete_data[ $post->ID ]['theme_slug']; } return $this->get_post_term_slug( $post, 'wp_theme' ); } /** * Get the slug of the first term of a taxonomy for a post. * * @param \WP_Post $post Post object. * @param string $taxonomy Taxonomy name. * @return string Term slug or empty string. */ private function get_post_term_slug( $post, $taxonomy ) { $terms = get_the_terms( $post, $taxonomy ); if ( ! is_array( $terms ) || empty( $terms ) ) { return ''; } return $terms[0]->slug; } /** * Get the sync status for a pattern (wp_block post). * * Patterns are synced unless the wp_pattern_sync_status meta is "unsynced". * * @param \WP_Post $post Pattern post object. * @return string "synced" or "unsynced". */ private function get_pattern_sync_status( $post ) { if ( isset( $this->pre_delete_data[ $post->ID ]['pattern_sync_status'] ) ) { return $this->pre_delete_data[ $post->ID ]['pattern_sync_status']; } $sync_status = get_post_meta( $post->ID, 'wp_pattern_sync_status', true ); return $sync_status === 'unsynced' ? 'unsynced' : 'synced'; } /** * Get output for detailed log section. * * @param object $row Log row. * @return Event_Details_Group */ public function get_log_row_details_output( $row ) { $group = new Event_Details_Group(); $group->set_formatter( new Event_Details_Group_Table_Formatter() ); $group->add_items( [ new Event_Details_Item( 'template_slug', __( 'Slug', 'simple-history' ) ), new Event_Details_Item( 'template_part_area', __( 'Area', 'simple-history' ) ), new Event_Details_Item( 'theme', __( 'Theme', 'simple-history' ) ), new Event_Details_Item( 'pattern_sync_status', __( 'Sync status', 'simple-history' ) ), new Event_Details_Item( 'font_face_weight', __( 'Font weight', 'simple-history' ) ), new Event_Details_Item( 'font_face_style', __( 'Font style', 'simple-history' ) ), ] ); return $group; } }
💾 保存
取消
保存为 UTF-8,换行统一为 LF