diff --git a/classes/class-db.php b/classes/class-db.php
index 9209676b..a35b89ab 100755
--- a/classes/class-db.php
+++ b/classes/class-db.php
@@ -119,7 +119,7 @@ protected function sanitize_record( $record ) {
return array_map(
function ( $value ) {
if ( ! is_array( $value ) ) {
- return wp_strip_all_tags( $value );
+ return wp_kses_post( $value );
}
return $value;
diff --git a/connectors/class-connector-posts.php b/connectors/class-connector-posts.php
index 057b70f9..575c0c07 100644
--- a/connectors/class-connector-posts.php
+++ b/connectors/class-connector-posts.php
@@ -7,6 +7,9 @@
namespace WP_Stream;
+use WP_Post;
+use WP_Taxonomy;
+
/**
* Class - Connector_Posts
*/
@@ -24,10 +27,41 @@ class Connector_Posts extends Connector {
* @var array
*/
public $actions = array(
- 'transition_post_status',
'deleted_post',
+ 'wp_after_insert_post',
+ 'transition_post_status',
+ 'set_object_terms',
);
+ /**
+ * Adds an action to retrieve previous post data before updating a post.
+ */
+ public function register() {
+ parent::register();
+ add_action( 'set_object_terms', array( $this, 'get_previous_post_terms' ), 10, 6 );
+ }
+
+ /**
+ * Add an array with the previous terms to a filter for future use.
+ *
+ * @param int|string] $object_id The post id.
+ * @param array $terms The current terms.
+ * @param array $tt_ids The current term taxonomy ids.
+ * @param string $taxonomy The taxonomy slug.
+ * @param bool $append Whether or not the terms were appended.
+ * @param array $old_tt_ids The previous term taxonomy ids.
+ * @return void
+ */
+ public function get_previous_post_terms( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) {
+
+ add_filter(
+ "wp_stream_previous_{$object_id}_{$taxonomy}_terms",
+ static function () use ( $old_tt_ids ) {
+ return (array) $old_tt_ids;
+ }
+ );
+ }
+
/**
* Return translated connector label
*
@@ -81,7 +115,8 @@ public function get_context_labels() {
public function action_links( $links, $record ) {
$post = get_post( $record->object_id );
- if ( $post && $post->post_status === $record->get_meta( 'new_status', true ) ) {
+ // Let's get action links for all posts.
+ if ( $post ) {
$post_type_name = $this->get_post_type_name( get_post_type( $post->ID ) );
if ( 'trash' === $post->post_status ) {
@@ -149,26 +184,62 @@ public function registered_post_type( $post_type, $args ) {
wp_stream_get_instance()->connectors->term_labels['stream_context'][ $post_type ] = $label;
}
+ /**
+ * Generates a list of terms based on the provided term IDs and taxonomy.
+ *
+ * @param array $updated The array of term IDs to generate the list from.
+ * @param string $taxonomy The taxonomy to which the terms belong.
+ *
+ * @return string The generated list of terms as HTML links.
+ */
+ private function make_term_list( $updated, $taxonomy ) {
+ $list = array_reduce(
+ $updated,
+ function ( $acc, $id ) use ( $taxonomy ) {
+ $term = get_term( $id, $taxonomy );
+
+ if ( empty( $term ) || is_wp_error( $term ) ) {
+ return $acc;
+ }
+
+ return $acc .= sprintf(
+ '%s, ',
+ get_term_link( $term, $taxonomy ),
+ $term->name
+ );
+ },
+ ''
+ );
+
+ return rtrim( $list, ', ' );
+ }
+
/**
* Log all post status changes ( creating / updating / trashing )
*
* @action transition_post_status
*
- * @param mixed $new_status New status.
- * @param mixed $old_status Old status.
- * @param \WP_Post $post Post object.
+ * @param mixed $new_status New status.
+ * @param mixed $old_status Old status.
+ * @param WP_Post $post Post object.
*/
public function callback_transition_post_status( $new_status, $old_status, $post ) {
- if ( in_array( $post->post_type, $this->get_excluded_post_types(), true ) ) {
+ // Don't log the non-included post types.
+ if ( ! ( $post instanceof WP_Post ) || in_array( $post->post_type, $this->get_excluded_post_types(), true ) ) {
return;
}
- // We don't want the meta box update request, just the post update.
+ // We don't want the meta box update request either, just the postupdate.
if ( ! empty( wp_stream_filter_input( INPUT_GET, 'meta-box-loader' ) ) ) {
return;
}
+ /**
+ * Whether or not there should also be a "post updated" log.
+ */
+ $should_log_update = false;
+
$start_statuses = array( 'auto-draft', 'inherit', 'new' );
if ( in_array( $new_status, $start_statuses, true ) ) {
return;
@@ -248,11 +319,11 @@ public function callback_transition_post_status( $new_status, $old_status, $post
$action = 'trashed';
} else {
/* translators: %1$s: a post title, %2$s: a post type singular name (e.g. "Hello World", "Post") */
- $summary = _x(
- '"%1$s" %2$s updated',
- '1: Post title, 2: Post type singular name',
- 'stream'
- );
+ $summary = false;
+ }
+
+ if ( ! $summary ) {
+ return;
}
if ( in_array( $old_status, $start_statuses, true ) && ! in_array( $new_status, $start_statuses, true ) ) {
@@ -263,28 +334,11 @@ public function callback_transition_post_status( $new_status, $old_status, $post
$action = 'updated';
}
- $revision_id = null;
-
- if ( wp_revisions_enabled( $post ) ) {
- $revision = get_children(
- array(
- 'post_type' => 'revision',
- 'post_status' => 'inherit',
- 'post_parent' => $post->ID,
- 'posts_per_page' => 1, // VIP safe.
- 'orderby' => 'post_date',
- 'order' => 'DESC',
- )
- );
-
- if ( $revision ) {
- $revision = array_values( $revision );
- $revision_id = $revision[0]->ID;
- }
- }
+ $revision_id = $this->get_revision_id( $post );
$post_type_name = strtolower( $this->get_post_type_name( $post->post_type ) );
+ add_filter( 'wp_stream_has_post_transition_log', '__return_true' );
$this->log(
$summary,
array(
@@ -302,6 +356,213 @@ public function callback_transition_post_status( $new_status, $old_status, $post
);
}
+ /**
+ * This currently only looks at the posts table.
+ *
+ * @param int|string $post_id The post id.
+ * @param WP_Post $post_after The post object of the final post.
+ * @param bool $update Whether or not this is an updated post.
+ * @param WP_Post $post_before The post object before it was updated.
+ * @return void
+ */
+ public function callback_wp_after_insert_post( $post_id, $post_after, $update, $post_before ) {
+
+ // Don't log newly created posts or the non-included post types.
+ if ( ! $update || in_array( $post_after->post_type, $this->get_excluded_post_types(), true ) ) {
+ return;
+ }
+
+ // We don't want the meta box update request either, just the post update.
+ if ( ! empty( wp_stream_filter_input( INPUT_GET, 'meta-box-loader' ) ) ) {
+ return;
+ }
+
+ $start_statuses = array( 'auto-draft', 'inherit', 'new' );
+ if (
+ in_array( $post_after->post_status, $start_statuses, true ) || in_array( $post_before->post_status, $start_statuses, true ) || ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) ) {
+ return;
+ }
+
+ $action = 'updated';
+ $post_type_name = $this->get_post_type_name( $post_after->post_type );
+
+ $updated_fields = array();
+
+ // Find out what was updated.
+ foreach ( $post_after as $field => $value ) {
+ if ( $value === $post_before->$field ) {
+ continue;
+ }
+
+ switch ( $field ) {
+ case 'post_author':
+ $updated_fields['post_author'] = sprintf(
+ /* Translators: %1$s is the previous post author, %2$s is the current post author */
+ __( '%1$s to %2$s', 'stream' ),
+ $this->get_author_maybe_link( $post_before->post_author ),
+ $this->get_author_maybe_link( $post_after->post_author )
+ );
+ break;
+ // Not including these for now.
+ case 'post_modified':
+ case 'post_modified_gmt':
+ // Handled in transition_post_status hook.
+ case 'post_status':
+ break;
+ case 'post_content':
+ $updated_fields['post_content'] = __( 'updated', 'stream' );
+ break;
+ case 'post_date':
+ case 'post_date_gmt':
+ if ( apply_filters( 'wp_stream_has_post_transition_log', false ) ) {
+ break;
+ }
+ // Break if there's a log, otherwise pass through.
+ default:
+ $updated_fields[ $field ] = sprintf(
+ /* Translators: %1$s is the previous value, %2$s is the current value */
+ __( '"%1$s" to "%2$s"', 'stream' ),
+ esc_html( $post_before->$field ),
+ esc_html( $value )
+ );
+ break;
+ }
+ }
+
+ $updated_terms = array();
+ $included_taxes = $this->get_included_taxonomies( $post_after->ID );
+ $post_before_terms = true;
+
+ // Only do this if the filter is working.
+ if ( false !== $post_before_terms ) {
+
+ foreach ( $included_taxes as $tax ) {
+
+ $previous_terms = apply_filters( "wp_stream_previous_{$post_after->ID}_{$tax}_terms", false );
+
+ // Bail if the filter failed.
+ if ( false === $previous_terms ) {
+ continue;
+ }
+
+ $tax_terms = wp_list_pluck( get_the_terms( $post_after->ID, $tax ), 'term_taxonomy_id' );
+
+ $added = array_diff( $tax_terms, $previous_terms );
+ $removed = array_diff( $previous_terms, $tax_terms );
+
+ if ( ! empty( $added ) ) {
+ $updated_terms[ $tax ]['added'] = $added;
+ }
+
+ if ( ! empty( $removed ) ) {
+ $updated_terms[ $tax ]['removed'] = $removed;
+ }
+ }
+ }
+
+ // If none of the post fields or terms were updated, there should be a log somewhere else.
+ if ( empty( $updated_fields ) && empty( $updated_terms ) ) {
+ return;
+ }
+
+ $details = '';
+
+ if ( ! empty( $updated_terms ) ) {
+ foreach ( $updated_terms as $tax => $term_updates ) {
+ $taxonomy = get_taxonomy( $tax );
+ $tax_name = ( $taxonomy instanceof WP_Taxonomy ) ? $taxonomy->labels->singular_name : $tax;
+ if ( ! empty( $term_updates['added'] ) ) {
+ $added_terms = sprintf(
+ /* Translators: %1$s is the taxonomy slug and %2$s is a linked list of the added terms. */
+ __( ' %1$s terms added: %2$s ', 'stream' ),
+ $tax_name,
+ $this->make_term_list( $term_updates['added'], $tax )
+ );
+ $details .= sprintf( '%s
', $added_terms );
+ }
+
+ if ( ! empty( $term_updates['removed'] ) ) {
+ $removed_terms = sprintf(
+ /* Translators: %1$s is the taxonomy slug and %2$s is a linked list of the removed terms. */
+ __( ' %1$s terms removed: %2$s', 'stream' ),
+ $tax_name,
+ $this->make_term_list( $term_updates['removed'], $tax )
+ );
+
+ $details .= sprintf( '%s
', $removed_terms );
+ }
+ }
+ }
+
+ if ( ! empty( $updated_fields ) ) {
+ $details .= __( 'Post updates: ', 'stream' );
+ // Creating a string for the summary. The array will be stored in the meta.
+ $details .= array_reduce(
+ array_keys( $updated_fields ),
+ function ( $acc, $key ) use ( $updated_fields ) {
+ return $acc .= sprintf( ' %s: %s, ', $key, $updated_fields[ $key ] );
+ },
+ ''
+ );
+
+ $details = rtrim( $details, ', ' );
+ }
+
+ /* translators: %1$s: a post title, %2$s: a post type singular name (e.g. "HelloWorld", "Post") */
+ $summary = _x(
+ '"%1$s" %2$s updated',
+ '1: Post title, 2: Post type singular name, 3: Fields updated list',
+ 'stream'
+ );
+
+ $log_summary = apply_filters( 'wp_stream_post_updated_summary', "{$summary}
{$details}", $post_after, $post_before, $post_before_terms );
+
+ $this->log(
+ $log_summary,
+ array(
+ 'post_title' => $post_after->post_title,
+ 'singular_name' => $post_type_name,
+ 'fields_updated' => wp_json_encode( $updated_fields ),
+ 'terms_updated' => wp_json_encode( $updated_terms ),
+ 'post_date' => $post_after->post_date,
+ 'post_date_gmt' => $post_after->post_date_gmt,
+ 'revision_id' => $this->get_revision_id( $post_after ),
+ ),
+ $post_after->ID,
+ $post_after->post_type,
+ $action
+ );
+ }
+
+ /**
+ * Retrieves the author name with an optional link to the author's profile.
+ *
+ * @param int $author_id The ID of the author.
+ * @return string The author name with an optional link to the author's profile.
+ */
+ private function get_author_maybe_link( $author_id ) {
+ $author = get_userdata( $author_id );
+
+ if ( empty( $author ) || is_wp_error( $author ) ) {
+ /* Translators: %d is the user id. */
+ return sprintf( __( 'Unknown user %d', 'stream' ), $author_id );
+ }
+
+ $author_name = $author->display_name;
+
+ // This is the same cap check as in `get_edit_user_link()` so we'll use it
+ // here to return just the name if the link won't work for the current user.
+ if ( ! current_user_can( 'edit_user', $author_id ) ) {
+ return $author_name;
+ }
+
+ return sprintf(
+ '%s',
+ esc_url( get_edit_user_link( $author_id ) ),
+ esc_html( $author_name )
+ );
+ }
+
/**
* Log post deletion
*
@@ -313,7 +574,7 @@ public function callback_deleted_post( $post_id ) {
$post = get_post( $post_id );
// We check if post is an instance of WP_Post as it doesn't always resolve in unit testing.
- if ( ! ( $post instanceof \WP_Post ) || in_array( $post->post_type, $this->get_excluded_post_types(), true ) ) {
+ if ( ! ( $post instanceof WP_Post ) || in_array( $post->post_type, $this->get_excluded_post_types(), true ) ) {
return;
}
@@ -357,6 +618,32 @@ public function get_excluded_post_types() {
);
}
+ /**
+ * Retrieves the list of taxonomies to include when logging term changes.
+ *
+ * By default, it includes the 'post_tag' and 'category' taxonomies.
+ *
+ * @param int|string $post_id The post id.
+ *
+ * @return array The list of taxonomies to log.
+ */
+ public function get_included_taxonomies( $post_id ) {
+ /**
+ * Filter the taxonomies for which term changes should be logged.
+ *
+ * @param array An array of the taxonomies.
+ * @param int|string The post id.
+ */
+ return apply_filters(
+ 'wp_stream_posts_include_taxonomies',
+ array(
+ 'post_tag',
+ 'category',
+ ),
+ $post_id
+ );
+ }
+
/**
* Gets the singular post type label
*
@@ -418,4 +705,35 @@ public function get_adjacent_post_revision( $revision_id, $previous = true ) {
return $revision_id;
}
+
+
+ /**
+ * Retrieves the ID of the latest revision for a given post.
+ *
+ * @param WP_Post $post The post object.
+ * @return int|null The ID of the latest revision, or null if revisions are not enabled for the post.
+ */
+ public function get_revision_id( WP_Post $post ) {
+ $revision_id = null;
+
+ if ( wp_revisions_enabled( $post ) ) {
+ $revision = get_children(
+ array(
+ 'post_type' => 'revision',
+ 'post_status' => 'inherit',
+ 'post_parent' => $post->ID,
+ 'posts_per_page' => 1, // VIP safe.
+ 'orderby' => 'post_date',
+ 'order' => 'DESC',
+ )
+ );
+
+ if ( $revision ) {
+ $revision = array_values( $revision );
+ $revision_id = $revision[0]->ID;
+ }
+ }
+
+ return $revision_id;
+ }
}
diff --git a/connectors/class-connector-taxonomies.php b/connectors/class-connector-taxonomies.php
index 13e4bdb8..8fea9239 100644
--- a/connectors/class-connector-taxonomies.php
+++ b/connectors/class-connector-taxonomies.php
@@ -44,13 +44,6 @@ class Connector_Taxonomies extends Connector {
*/
public $context_labels;
- /**
- * Register connector in the WP Frontend
- *
- * @var bool
- */
- public $register_frontend = false;
-
/**
* Return translated connector label
*