| Current File : /home/d/i/g/digitaw/www/wp-content/plugins/simple-history/loggers/class-notes-logger.php |
<?php
namespace Simple_History\Loggers;
use Simple_History\Event_Details\Event_Details_Group;
use Simple_History\Event_Details\Event_Details_Item;
/**
* Logger for WordPress 6.9+ Notes feature (block comments).
*
* Logs collaborative notes that can be added to blocks in the WordPress editor.
* Notes are stored as comments with comment_type='note' and linked to blocks
* via the block's metadata.noteId attribute.
*
* @package SimpleHistory
* @since 5.0.0
*/
class Notes_Logger extends Logger {
/**
* Logger slug.
*
* @var string
*/
public $slug = 'NotesLogger';
/**
* Return info about logger.
*
* @return array Array with logger info.
*/
public function get_info() {
$arr_info = [
'name' => _x( 'Notes Logger', 'Logger: Notes', 'simple-history' ),
'description' => _x( 'Logs WordPress block notes (collaborative comments)', 'Logger: Notes', 'simple-history' ),
'capability' => 'edit_posts',
'messages' => [
'note_added' => _x( 'Added a note to {post_type} "{post_title}"', 'Logger: Notes', 'simple-history' ),
'note_reply_added' => _x( 'Replied to a note in {post_type} "{post_title}"', 'Logger: Notes', 'simple-history' ),
'note_edited' => _x( 'Edited a note in {post_type} "{post_title}"', 'Logger: Notes', 'simple-history' ),
'note_deleted' => _x( 'Deleted a note from {post_type} "{post_title}"', 'Logger: Notes', 'simple-history' ),
'note_resolved' => _x( 'Marked a note as resolved in {post_type} "{post_title}"', 'Logger: Notes', 'simple-history' ),
'note_reopened' => _x( 'Reopened a resolved note in {post_type} "{post_title}"', 'Logger: Notes', 'simple-history' ),
],
'labels' => [
'search' => [
'label' => _x( 'Notes', 'Notes logger: search', 'simple-history' ),
'options' => [
_x( 'Added notes', 'Notes logger: search', 'simple-history' ) => [
'note_added',
],
_x( 'Replied to notes', 'Notes logger: search', 'simple-history' ) => [
'note_reply_added',
],
_x( 'Edited notes', 'Notes logger: search', 'simple-history' ) => [
'note_edited',
],
_x( 'Deleted notes', 'Notes logger: search', 'simple-history' ) => [
'note_deleted',
],
_x( 'Resolved notes', 'Notes logger: search', 'simple-history' ) => [
'note_resolved',
],
_x( 'Reopened notes', 'Notes logger: search', 'simple-history' ) => [
'note_reopened',
],
],
],
],
];
return $arr_info;
}
/**
* Called when logger is loaded.
*/
public function loaded() {
// Hook into comment actions to track notes.
add_action( 'wp_insert_comment', [ $this, 'on_wp_insert_comment' ], 10, 2 );
add_action( 'edit_comment', [ $this, 'on_edit_comment' ], 10, 1 );
add_action( 'updated_comment_meta', [ $this, 'on_updated_comment_meta' ], 10, 4 );
add_action( 'added_comment_meta', [ $this, 'on_updated_comment_meta' ], 10, 4 );
add_action( 'delete_comment', [ $this, 'on_delete_comment' ], 10, 2 );
add_action( 'trash_comment', [ $this, 'on_delete_comment' ], 10, 2 );
}
/**
* Get output for detailed log section.
*
* @param object $row Log row.
* @return Event_Details_Group
*/
public function get_log_row_details_output( $row ) {
return ( new Event_Details_Group() )
->add_items(
[
new Event_Details_Item(
'note_content',
_x( 'Content', 'Notes logger - detailed output', 'simple-history' ),
),
]
);
}
/**
* Log when a note is created.
*
* @param int $comment_id The comment ID.
* @param \WP_Comment|null $comment Comment object.
*/
public function on_wp_insert_comment( $comment_id, $comment = null ) {
if ( empty( $comment_id ) ) {
return;
}
if ( ! $comment ) {
$comment = get_comment( $comment_id );
}
if ( ! $this->is_note_comment( $comment ) ) {
return;
}
$comment_content = trim( $comment->comment_content );
// Skip if this comment has no content.
// Empty comments are status markers (resolved/reopened) that will be logged
// separately by on_updated_comment_meta when the _wp_note_status meta is added.
if ( empty( $comment_content ) ) {
return;
}
$post = get_post( $comment->comment_post_ID );
if ( ! $post ) {
return;
}
$is_reply = $comment->comment_parent > 0;
$block_info = $this->get_block_info_for_note( $comment_id, $comment->comment_post_ID );
$context = [
'note_id' => $comment_id,
'post_id' => $comment->comment_post_ID,
'post_type' => get_post_type( $post ),
'post_title' => $post->post_title,
'note_content' => $comment_content,
'is_reply' => $is_reply,
];
// Add block information if available.
if ( $block_info ) {
$context['block_type'] = $block_info['block_type'];
$context['block_content_preview'] = $block_info['content_preview'];
$context['block_count'] = $block_info['block_count'];
}
// Choose appropriate message key.
$message = $is_reply ? 'note_reply_added' : 'note_added';
$this->info_message( $message, $context );
}
/**
* Log when a note is edited.
*
* @param int $comment_id The comment ID.
*/
public function on_edit_comment( $comment_id ) {
if ( empty( $comment_id ) ) {
return;
}
$comment = get_comment( $comment_id );
if ( ! $this->is_note_comment( $comment ) ) {
return;
}
$post = get_post( $comment->comment_post_ID );
if ( ! $post ) {
return;
}
$context = [
'note_id' => $comment_id,
'post_id' => $comment->comment_post_ID,
'post_type' => get_post_type( $post ),
'post_title' => $post->post_title,
'note_content' => $comment->comment_content,
];
$this->info_message( 'note_edited', $context );
}
/**
* Log when a note is resolved or reopened.
*
* Fires when the _wp_note_status comment meta is added or updated.
* The status can be 'resolved' (note marked as done) or 'reopen' (note reopened).
*
* @param int $meta_id ID of updated metadata entry.
* @param int $comment_id Comment ID.
* @param string $meta_key Meta key.
* @param mixed $meta_value Meta value (either 'resolved' or 'reopen').
*/
public function on_updated_comment_meta( $meta_id, $comment_id, $meta_key, $meta_value ) {
if ( empty( $comment_id ) ) {
return;
}
if ( $meta_key !== '_wp_note_status' ) {
return;
}
$comment = get_comment( $comment_id );
if ( ! $this->is_note_comment( $comment ) ) {
return;
}
$post = get_post( $comment->comment_post_ID );
if ( ! $post ) {
return;
}
$context = [
'note_id' => $comment_id,
'post_id' => $comment->comment_post_ID,
'post_type' => get_post_type( $post ),
'post_title' => $post->post_title,
];
// Determine message based on new status.
if ( $meta_value === 'resolved' ) {
$this->info_message( 'note_resolved', $context );
} elseif ( $meta_value === 'reopen' ) {
$this->info_message( 'note_reopened', $context );
}
}
/**
* Log when a note is deleted or trashed.
*
* Handles both permanent deletion and trashing (e.g., via REST API).
*
* @param int $comment_id The comment ID.
* @param \WP_Comment|null $comment Comment object (optional, added in WP 6.2).
*/
public function on_delete_comment( $comment_id, $comment = null ) {
if ( empty( $comment_id ) ) {
return;
}
// The $comment parameter was added in WordPress 6.2.
// For backwards compatibility, fetch it if not provided.
if ( ! $comment ) {
$comment = get_comment( $comment_id );
}
if ( ! $this->is_note_comment( $comment ) ) {
return;
}
$post = get_post( $comment->comment_post_ID );
if ( ! $post ) {
return;
}
$block_info = $this->get_block_info_for_note( $comment_id, $comment->comment_post_ID );
$context = [
'note_id' => $comment_id,
'post_id' => $comment->comment_post_ID,
'post_type' => get_post_type( $post ),
'post_title' => $post->post_title,
];
// Add block information if available.
if ( $block_info ) {
$context['block_type'] = $block_info['block_type'];
$context['block_content_preview'] = $block_info['content_preview'];
}
$this->info_message( 'note_deleted', $context );
}
/**
* Check if a comment is a note.
*
* @param \WP_Comment|null $comment The comment object.
* @return bool True if this is a note, false otherwise.
*/
private function is_note_comment( $comment ) {
return $comment && 'note' === $comment->comment_type;
}
/**
* Get the root note ID by walking up the parent chain.
*
* WordPress stores only the root note ID in block metadata.noteId.
* For threaded notes (replies), we need to find the root.
*
* @param int $note_id The note (comment) ID.
* @return int The root note ID.
*/
private function get_root_note_id( $note_id ) {
$comment = get_comment( $note_id );
if ( ! $comment ) {
return $note_id;
}
// Walk up the parent chain until we find the root.
while ( $comment->comment_parent > 0 ) {
$parent = get_comment( $comment->comment_parent );
if ( ! $parent ) {
break;
}
$comment = $parent;
}
return (int) $comment->comment_ID;
}
/**
* Get block information for a note.
*
* Parses the post content to find the block(s) that reference this note
* via their metadata.noteId attribute. For threaded notes (replies), walks
* up the parent chain to find the root note ID, since only the root note
* ID is stored in the block metadata.
*
* @param int $note_id The note (comment) ID.
* @param int $post_id The post ID.
* @return array|null Array with block info, or null if block not found.
*/
private function get_block_info_for_note( $note_id, $post_id ) {
$post = get_post( $post_id );
if ( ! $post ) {
return null;
}
// Find the root note ID by walking up the parent chain.
// Only the root note ID is stored in block metadata.
$root_note_id = $this->get_root_note_id( $note_id );
$blocks = parse_blocks( $post->post_content );
$found_blocks = $this->find_blocks_by_note_id( $blocks, $root_note_id );
if ( empty( $found_blocks ) ) {
return null; // Block might have been deleted.
}
$block = $found_blocks[0];
// Extract block type (remove 'core/' prefix if present).
$block_type = $block['blockName'];
if ( strpos( $block_type, 'core/' ) === 0 ) {
$block_type = substr( $block_type, 5 );
}
// Get content preview.
$content = wp_strip_all_tags( $block['innerHTML'] );
$content = trim( $content );
$content_preview = $content;
// Truncate to 100 characters.
if ( strlen( $content ) > 100 ) {
$content_preview = substr( $content, 0, 100 ) . '...';
}
return [
'block_type' => $block_type,
'block_name' => $block['blockName'],
'content_preview' => $content_preview,
'full_content' => $content,
'block_count' => count( $found_blocks ),
'attrs' => $block['attrs'],
];
}
/**
* Recursively find blocks with a specific noteId.
*
* @param array $blocks Array of parsed blocks.
* @param int $note_id The note ID to search for.
* @return array Array of blocks that reference this note.
*/
private function find_blocks_by_note_id( $blocks, $note_id ) {
$found = [];
foreach ( $blocks as $block ) {
// Check if this block has the noteId in its metadata.
if ( isset( $block['attrs']['metadata']['noteId'] ) &&
$block['attrs']['metadata']['noteId'] === $note_id ) {
$found[] = $block;
}
// Recursively search inner blocks.
if ( ! empty( $block['innerBlocks'] ) ) {
$found = array_merge(
$found,
$this->find_blocks_by_note_id( $block['innerBlocks'], $note_id )
);
}
}
return $found;
}
}