From 5c5ae83287ea0d38cd1988460784dfc4ab92f90a Mon Sep 17 00:00:00 2001 From: Corey McKrill <916023+coreymckrill@users.noreply.github.com> Date: Tue, 29 Oct 2019 12:46:13 -0700 Subject: [PATCH] Plugin: Add a system for logging events related to pledges and contributors (#54) Uses action hooks to capture relevant events as log entries on a per-pledge basis. This provides a running history of a pledge and can be used as an audit log if questions arise about changes to a pledge or there are weird bugs. Fixes #39 --- plugins/wporg-5ftf/includes/contributor.php | 58 +++- plugins/wporg-5ftf/includes/pledge-form.php | 4 +- plugins/wporg-5ftf/includes/pledge-log.php | 319 ++++++++++++++++++++ plugins/wporg-5ftf/includes/pledge-meta.php | 5 +- plugins/wporg-5ftf/index.php | 1 + plugins/wporg-5ftf/views/log.php | 55 ++++ 6 files changed, 424 insertions(+), 18 deletions(-) create mode 100644 plugins/wporg-5ftf/includes/pledge-log.php create mode 100644 plugins/wporg-5ftf/views/log.php diff --git a/plugins/wporg-5ftf/includes/contributor.php b/plugins/wporg-5ftf/includes/contributor.php index e4ba7c6..1e05955 100644 --- a/plugins/wporg-5ftf/includes/contributor.php +++ b/plugins/wporg-5ftf/includes/contributor.php @@ -123,22 +123,40 @@ function populate_list_table_columns( $column, $post_id ) { } /** - * Create a contributor post as a child of a pledge post. + * Add one or more contributors to a pledge. * - * @param string $wporg_username - * @param int $pledge_id + * Note that this does not validate whether a contributor's wporg username exists in the system. * - * @return int|WP_Error Post ID on success. Otherwise WP_Error. + * @param int $pledge_id The post ID of the pledge. + * @param array $contributors Array of contributor wporg usernames. + * + * @return void */ -function create_new_contributor( $wporg_username, $pledge_id ) { - $args = array( - 'post_type' => CPT_ID, - 'post_title' => sanitize_user( $wporg_username ), - 'post_parent' => $pledge_id, - 'post_status' => 'pending', - ); +function add_pledge_contributors( $pledge_id, $contributors ) { + $results = array(); - return wp_insert_post( $args, true ); + foreach ( $contributors as $wporg_username ) { + $args = array( + 'post_type' => CPT_ID, + 'post_title' => sanitize_user( $wporg_username ), + 'post_parent' => $pledge_id, + 'post_status' => 'pending', + ); + + $result = wp_insert_post( $args, true ); + + $results[ $wporg_username ] = ( is_wp_error( $result ) ) ? $result->get_error_code() : $result; + } + + /** + * Action: Fires when one or more contributors are added to a pledge. + * + * @param int $pledge_id The post ID of the pledge. + * @param array $contributors Array of contributor wporg usernames. + * @param array $results Associative array, key is wporg username, value is post ID on success, + * or an error code on failure. + */ + do_action( FiveForTheFuture\PREFIX . '_add_pledge_contributors', $pledge_id, $contributors, $results ); } /** @@ -152,7 +170,19 @@ function create_new_contributor( $wporg_username, $pledge_id ) { * @return false|WP_Post|null */ function remove_contributor( $contributor_post_id ) { - return wp_trash_post( $contributor_post_id ); + $pledge_id = get_post( $contributor_post_id )->post_parent; + $result = wp_trash_post( $contributor_post_id ); + + /** + * Action: Fires when a contributor is removed from a pledge. + * + * @param int $pledge_id + * @param int $contributor_post_id + * @param WP_Post|false|null $result + */ + do_action( FiveForTheFuture\PREFIX . '_remove_contributor', $pledge_id, $contributor_post_id, $result ); + + return $result; } /** @@ -271,7 +301,7 @@ function process_my_pledges_form() { 'post_status' => $status, ) ); } elseif ( 'trash' === $status ) { - wp_delete_post( $contributor_post_id ); + remove_contributor( $contributor_post_id ); } return $message; diff --git a/plugins/wporg-5ftf/includes/pledge-form.php b/plugins/wporg-5ftf/includes/pledge-form.php index 3173efe..bd47193 100755 --- a/plugins/wporg-5ftf/includes/pledge-form.php +++ b/plugins/wporg-5ftf/includes/pledge-form.php @@ -90,9 +90,7 @@ function process_form_new() { return $new_pledge_id; } - foreach ( $contributors as $wporg_username ) { - Contributor\create_new_contributor( $wporg_username, $new_pledge_id ); - } + Contributor\add_pledge_contributors( $new_pledge_id, $contributors ); // Attach logo to the pledge. wp_update_post( array( diff --git a/plugins/wporg-5ftf/includes/pledge-log.php b/plugins/wporg-5ftf/includes/pledge-log.php new file mode 100644 index 0000000..0090d79 --- /dev/null +++ b/plugins/wporg-5ftf/includes/pledge-log.php @@ -0,0 +1,319 @@ +ID ); + + require FiveForTheFuture\get_views_path() . 'log.php'; +} + +/** + * Defaults for a log entry. + * + * @return array { + * @type int $timestamp Time of the event. + * @type string $type The type of event. A snake_case or kebab-case string. + * @type string $message Description of the event. + * @type array $data Details and data related to the event. + * @type int $user_id The ID of the logged in user who triggered the event, if applicable. + * } + */ +function get_log_entry_template() { + return array( + 'timestamp' => time(), + 'type' => '', + 'message' => '', + 'data' => array(), + 'user_id' => get_current_user_id(), + ); +} + +/** + * Get a time-sorted array of log entries for a particular pledge. + * + * @param int $pledge_id + * + * @return array + */ +function get_pledge_log( $pledge_id ) { + $log = get_post_meta( $pledge_id, LOG_META_KEY, false ); + + if ( ! $log ) { + return array(); + } + + usort( $log, function( $a, $b ) { + if ( $a['timestamp'] === $b['timestamp'] ) { + return 0; + } + + return ( $a['timestamp'] < $b['timestamp'] ) ? -1 : 1; + } ); + + return $log; +} + +/** + * Add a new log entry for a particular pledge. + * + * @param int $pledge_id + * @param string $type + * @param string $message + * @param array $data + * @param int $user_id + * + * @return void + */ +function add_log_entry( $pledge_id, $type, $message, array $data = array(), $user_id = 0 ) { + $entry = get_log_entry_template(); + + $entry['type'] = $type; + $entry['message'] = $message; + $entry['data'] = $data; + + if ( $user_id ) { + // The template defaults to the current user, so this function parameter shouldn't override unless it's different. + $entry['user_id'] = $user_id; + } + + add_post_meta( $pledge_id, LOG_META_KEY, $entry, false ); +} + +/** + * Record logs for events when saving a post. + * + * Hooked to "save_post_{$post->post_type}". + * + * @param int $post_ID Post ID. + * @param WP_Post $post Unused. Post object. + * @param bool $update Whether this is an existing post being updated or not. + * + * @return void + */ +function capture_save_post( $post_ID, $post, $update ) { + if ( false === $update ) { + add_log_entry( + $post_ID, + 'pledge_created', + sprintf( + 'Pledge created. Status set to %s.', + esc_html( get_post_status( $post_ID ) ) + ), + PledgeForm\get_form_submission() + ); + } +} + +/** + * Record logs for events when postmeta values change. + * + * @param int $meta_id Unused. ID of updated metadata entry. + * @param int $object_id Post ID. + * @param string $meta_key Meta key. + * @param mixed $meta_value Meta value. + * + * @return void + */ +function capture_updated_postmeta( $meta_id, $object_id, $meta_key, $meta_value ) { + $post_type = get_post_type( $object_id ); + + if ( Pledge\CPT_ID !== $post_type ) { + return; + } + + $valid_keys = array_keys( PledgeMeta\get_pledge_meta_config( 'user_input' ) ); + $trimmed_meta_key = str_replace( PledgeMeta\META_PREFIX, '', $meta_key ); + + if ( in_array( $trimmed_meta_key, $valid_keys, true ) ) { + add_log_entry( + $object_id, + 'pledge_data_changed', + sprintf( + 'Changed %1$s to %2$s.', + esc_html( $trimmed_meta_key ), + esc_html( $meta_value ) + ), + array( + $meta_key => $meta_value, + ) + ); + } +} + +/** + * Record logs for events when new postmeta values are added (not changed). + * + * @param int $meta_id Unused. ID of updated metadata entry. + * @param int $object_id Post ID. + * @param string $meta_key Meta key. + * @param mixed $meta_value Meta value. + * + * @return void + */ +function capture_added_post_meta( $meta_id, $object_id, $meta_key, $meta_value ) { + $post_type = get_post_type( $object_id ); + + if ( Pledge\CPT_ID !== $post_type ) { + return; + } + + switch ( $meta_key ) { + case PledgeMeta\META_PREFIX . 'pledge-email-confirmed': + if ( true === $meta_value ) { + add_log_entry( + $object_id, + 'pledge_email_confirmed', + 'Pledge email address confirmed.', + array( + 'email' => get_post_meta( $object_id, PledgeMeta\META_PREFIX . 'org-pledge-email', true ), + ) + ); + } + break; + } +} + +/** + * Record logs for events when a pledge post's status changes. + * + * @param string $new_status + * @param string $old_status + * @param WP_Post $post + * + * @return void + */ +function capture_transition_post_status( $new_status, $old_status, WP_Post $post ) { + $cpts = array( Pledge\CPT_ID, Contributor\CPT_ID ); + $post_type = get_post_type( $post ); + + if ( ! in_array( $post_type, $cpts, true ) ) { + return; + } + + if ( $new_status === $old_status ) { + return; + } + + switch ( $post_type ) { + case Pledge\CPT_ID: + add_log_entry( + $post->ID, + 'pledge_status_changed', + sprintf( + 'Pledge status changed from %1$s to %2$s.', + esc_html( $old_status ), + esc_html( $new_status ) + ), + array() + ); + break; + + case Contributor\CPT_ID: + $pledge = get_post( $post->post_parent ); + + add_log_entry( + $pledge->ID, + 'contributor_status_changed', + sprintf( + 'Contributor %1$s status changed from %2$s to %3$s.', + esc_html( $post->post_title ), + esc_html( $old_status ), + esc_html( $new_status ) + ), + array() + ); + break; + } +} + +/** + * Record a log for the event of contributors being added to a pledge. + * + * @param int $pledge_id The post ID of the pledge. + * @param array $contributors Array of contributor wporg usernames. + * @param array $results Associative array, key is wporg username, value is post ID on success, + * or an error code on failure. + * + * @return void + */ +function capture_add_pledge_contributors( $pledge_id, $contributors, $results ) { + add_log_entry( + $pledge_id, + 'contributors_added', + sprintf( + 'Contributors added: %s', + implode( ', ', $contributors ) + ), + $results + ); +} + +/** + * Record a log for the event when a contributor is removed from a pledge. + * + * @param int $pledge_id The post ID of the pledge. + * @param int $contributor_post_id The post ID of the pledge. + * @param WP_Post|false|null $result The result of the attempt to trash the post. + * + * @return void + */ +function capture_remove_contributor( $pledge_id, $contributor_post_id, $result ) { + // If the result isn't a post object, then it was already trashed, or didn't exist. + if ( $result instanceof WP_Post ) { + $contributor_post = get_post( $contributor_post_id ); + + add_log_entry( + $pledge_id, + 'contributor_removed', + sprintf( + 'Contributor removed: %s', + esc_html( $contributor_post->post_title ) + ), + array( + 'previous_status' => $contributor_post->_wp_trash_meta_status, + ) + ); + } +} diff --git a/plugins/wporg-5ftf/includes/pledge-meta.php b/plugins/wporg-5ftf/includes/pledge-meta.php index a9c38cf..a9d2e90 100755 --- a/plugins/wporg-5ftf/includes/pledge-meta.php +++ b/plugins/wporg-5ftf/includes/pledge-meta.php @@ -28,9 +28,11 @@ add_action( 'added_post_meta', __NAMESPACE__ . '\update_generated_meta', 10, 4 /** * Define pledge meta fields and their properties. * + * @param string $context Optional. The part of the config to return. 'user_input', 'generated', or 'all'. + * * @return array */ -function get_pledge_meta_config( $context = '' ) { +function get_pledge_meta_config( $context = 'all' ) { $user_input = array( 'org-description' => array( 'single' => true, @@ -283,6 +285,7 @@ function update_generated_meta( $meta_id, $object_id, $meta_key, $_meta_value ) case META_PREFIX . 'org-name': if ( 'updated_postmeta' === current_action() ) { wp_update_post( array( + 'ID' => $object_id, 'post_title' => $_meta_value, ) ); } diff --git a/plugins/wporg-5ftf/index.php b/plugins/wporg-5ftf/index.php index 54e1d28..be8c836 100755 --- a/plugins/wporg-5ftf/index.php +++ b/plugins/wporg-5ftf/index.php @@ -30,6 +30,7 @@ function load() { require_once get_includes_path() . 'pledge-form.php'; require_once get_includes_path() . 'directory.php'; require_once get_includes_path() . 'xprofile.php'; + require_once get_includes_path() . 'pledge-log.php'; } /** diff --git a/plugins/wporg-5ftf/views/log.php b/plugins/wporg-5ftf/views/log.php new file mode 100644 index 0000000..7742418 --- /dev/null +++ b/plugins/wporg-5ftf/views/log.php @@ -0,0 +1,55 @@ + + +
+ + + + + + + + + + + + + + + + + + + +
DateEntryUser
+ + + +
+ + + +
+
+ + + +
+ + user_login ); ?> + +
+ + + +

+ There are no log entries. +

+ + +