omment_id ) { return new WP_Error( 'comment_save_error', __( 'Error: The comment could not be saved. Please try again later.' ), 500 ); } return get_comment( $comment_id ); } /** * Registers the personal data exporter for comments. * * @since 4.9.6 * * @param array $exporters An array of personal data exporters. * @return array An array of personal data exporters. */ function wp_register_comment_personal_data_exporter( $exporters ) { $exporters['wordpress-comments'] = array( 'exporter_friendly_name' => __( 'WordPress Comments' ), 'callback' => 'wp_comments_personal_data_exporter', ); return $exporters; } /** * Finds and exports personal data associated with an email address from the comments table. * * @since 4.9.6 * * @param string $email_address The comment author email address. * @param int $page Comment page. * @return array An array of personal data. */ function wp_comments_personal_data_exporter( $email_address, $page = 1 ) { // Limit us to 500 comments at a time to avoid timing out. $number = 500; $page = (int) $page; $data_to_export = array(); $comments = get_comments( array( 'author_email' => $email_address, 'number' => $number, 'paged' => $page, 'orderby' => 'comment_ID', 'order' => 'ASC', 'update_comment_meta_cache' => false, ) ); $comment_prop_to_export = array( 'comment_author' => __( 'Comment Author' ), 'comment_author_email' => __( 'Comment Author Email' ), 'comment_author_url' => __( 'Comment Author URL' ), 'comment_author_IP' => __( 'Comment Author IP' ), 'comment_agent' => __( 'Comment Author User Agent' ), 'comment_date' => __( 'Comment Date' ), 'comment_content' => __( 'Comment Content' ), 'comment_link' => __( 'Comment URL' ), ); foreach ( (array) $comments as $comment ) { $comment_data_to_export = array(); foreach ( $comment_prop_to_export as $key => $name ) { $value = ''; switch ( $key ) { case 'comment_author': case 'comment_author_email': case 'comment_author_url': case 'comment_author_IP': case 'comment_agent': case 'comment_date': $value = $comment->{$key}; break; case 'comment_content': $value = get_comment_text( $comment->comment_ID ); break; case 'comment_link': $value = get_comment_link( $comment->comment_ID ); $value = sprintf( '%s', esc_url( $value ), esc_html( $value ) ); break; } if ( ! empty( $value ) ) { $comment_data_to_export[] = array( 'name' => $name, 'value' => $value, ); } } $data_to_export[] = array( 'group_id' => 'comments', 'group_label' => __( 'Comments' ), 'group_description' => __( 'User’s comment data.' ), 'item_id' => "comment-{$comment->comment_ID}", 'data' => $comment_data_to_export, ); } $done = count( $comments ) < $number; return array( 'data' => $data_to_export, 'done' => $done, ); } /** * Registers the personal data eraser for comments. * * @since 4.9.6 * * @param array $erasers An array of personal data erasers. * @return array An array of personal data erasers. */ function wp_register_comment_personal_data_eraser( $erasers ) { $erasers['wordpress-comments'] = array( 'eraser_friendly_name' => __( 'WordPress Comments' ), 'callback' => 'wp_comments_personal_data_eraser', ); return $erasers; } /** * Erases personal data associated with an email address from the comments table. * * @since 4.9.6 * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $email_address The comment author email address. * @param int $page Comment page. * @return array */ function wp_comments_personal_data_eraser( $email_address, $page = 1 ) { global $wpdb; if ( empty( $email_address ) ) { return array( 'items_removed' => false, 'items_retained' => false, 'messages' => array(), 'done' => true, ); } // Limit us to 500 comments at a time to avoid timing out. $number = 500; $page = (int) $page; $items_removed = false; $items_retained = false; $comments = get_comments( array( 'author_email' => $email_address, 'number' => $number, 'paged' => $page, 'orderby' => 'comment_ID', 'order' => 'ASC', 'include_unapproved' => true, ) ); /* translators: Name of a comment's author after being anonymized. */ $anon_author = __( 'Anonymous' ); $messages = array(); foreach ( (array) $comments as $comment ) { $anonymized_comment = array(); $anonymized_comment['comment_agent'] = ''; $anonymized_comment['comment_author'] = $anon_author; $anonymized_comment['comment_author_email'] = ''; $anonymized_comment['comment_author_IP'] = wp_privacy_anonymize_data( 'ip', $comment->comment_author_IP ); $anonymized_comment['comment_author_url'] = ''; $anonymized_comment['user_id'] = 0; $comment_id = (int) $comment->comment_ID; /** * Filters whether to anonymize the comment. * * @since 4.9.6 * * @param bool|string $anon_message Whether to apply the comment anonymization (bool) or a custom * message (string). Default true. * @param WP_Comment $comment WP_Comment object. * @param array $anonymized_comment Anonymized comment data. */ $anon_message = apply_filters( 'wp_anonymize_comment', true, $comment, $anonymized_comment ); if ( true !== $anon_message ) { if ( $anon_message && is_string( $anon_message ) ) { $messages[] = esc_html( $anon_message ); } else { /* translators: %d: Comment ID. */ $messages[] = sprintf( __( 'Comment %d contains personal data but could not be anonymized.' ), $comment_id ); } $items_retained = true; continue; } $args = array( 'comment_ID' => $comment_id, ); $updated = $wpdb->update( $wpdb->comments, $anonymized_comment, $args ); if ( $updated ) { $items_removed = true; clean_comment_cache( $comment_id ); } else { $items_retained = true; } } $done = count( $comments ) < $number; return array( 'items_removed' => $items_removed, 'items_retained' => $items_retained, 'messages' => $messages, 'done' => $done, ); } /** * Sets the last changed time for the 'comment' cache group. * * @since 5.0.0 */ function wp_cache_set_comments_last_changed() { wp_cache_set_last_changed( 'comment' ); } /** * Updates the comment type for a batch of comments. * * @since 5.5.0 * * @global wpdb $wpdb WordPress database abstraction object. */ function _wp_batch_update_comment_type() { global $wpdb; $lock_name = 'update_comment_type.lock'; // Try to lock. $lock_result = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", $lock_name, time() ) ); if ( ! $lock_result ) { $lock_result = get_option( $lock_name ); // Bail if we were unable to create a lock, or if the existing lock is still valid. if ( ! $lock_result || ( $lock_result > ( time() - HOUR_IN_SECONDS ) ) ) { wp_schedule_single_event( time() + ( 5 * MINUTE_IN_SECONDS ), 'wp_update_comment_type_batch' ); return; } } // Update the lock, as by this point we've definitely got a lock, just need to fire the actions. update_option( $lock_name, time() ); // Check if there's still an empty comment type. $empty_comment_type = $wpdb->get_var( "SELECT comment_ID FROM $wpdb->comments WHERE comment_type = '' LIMIT 1" ); // No empty comment type, we're done here. if ( ! $empty_comment_type ) { update_option( 'finished_updating_comment_type', true ); delete_option( $lock_name ); return; } // Empty comment type found? We'll need to run this script again. wp_schedule_single_event( time() + ( 2 * MINUTE_IN_SECONDS ), 'wp_update_comment_type_batch' ); /** * Filters the comment batch size for updating the comment type. * * @since 5.5.0 * * @param int $comment_batch_size The comment batch size. Default 100. */ $comment_batch_size = (int) apply_filters( 'wp_update_comment_type_batch_size', 100 ); // Get the IDs of the comments to update. $comment_ids = $wpdb->get_col( $wpdb->prepare( "SELECT comment_ID FROM {$wpdb->comments} WHERE comment_type = '' ORDER BY comment_ID DESC LIMIT %d", $comment_batch_size ) ); if ( $comment_ids ) { $comment_id_list = implode( ',', $comment_ids ); // Update the `comment_type` field value to be `comment` for the next batch of comments. $wpdb->query( "UPDATE {$wpdb->comments} SET comment_type = 'comment' WHERE comment_type = '' AND comment_ID IN ({$comment_id_list})" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared ); // Make sure to clean the comment cache. clean_comment_cache( $comment_ids ); } delete_option( $lock_name ); } /** * In order to avoid the _wp_batch_update_comment_type() job being accidentally removed, * check that it's still scheduled while we haven't finished updating comment types. * * @ignore * @since 5.5.0 */ function _wp_check_for_scheduled_update_comment_type() { if ( ! get_option( 'finished_updating_comment_type' ) && ! wp_next_scheduled( 'wp_update_comment_type_batch' ) ) { wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'wp_update_comment_type_batch' ); } } sed in the action claim query. * * @since x.x.x * * @param string $order_by_sql */ $order = apply_filters( 'action_scheduler_claim_actions_order_by', 'ORDER BY attempts ASC, scheduled_date_gmt ASC, action_id ASC' ); $params[] = $limit; $sql = $wpdb->prepare( "{$update} {$where} {$order} LIMIT %d", $params ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders $rows_affected = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching if ( false === $rows_affected ) { throw new \RuntimeException( __( 'Unable to claim actions. Database error.', 'action-scheduler' ) ); } return (int) $rows_affected; } /** * Get the number of active claims. * * @return int */ public function get_claim_count() { global $wpdb; $sql = "SELECT COUNT(DISTINCT claim_id) FROM {$wpdb->actionscheduler_actions} WHERE claim_id != 0 AND status IN ( %s, %s)"; $sql = $wpdb->prepare( $sql, array( self::STATUS_PENDING, self::STATUS_RUNNING ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared return (int) $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } /** * Return an action's claim ID, as stored in the claim_id column. * * @param string $action_id Action ID. * @return mixed */ public function get_claim_id( $action_id ) { /** @var \wpdb $wpdb */ global $wpdb; $sql = "SELECT claim_id FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d"; $sql = $wpdb->prepare( $sql, $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared return (int) $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } /** * Retrieve the action IDs of action in a claim. * * @param int $claim_id Claim ID. * @return int[] */ public function find_actions_by_claim_id( $claim_id ) { /** @var \wpdb $wpdb */ global $wpdb; $action_ids = array(); $before_date = isset( $this->claim_before_date ) ? $this->claim_before_date : as_get_datetime_object(); $cut_off = $before_date->format( 'Y-m-d H:i:s' ); $sql = $wpdb->prepare( "SELECT action_id, scheduled_date_gmt FROM {$wpdb->actionscheduler_actions} WHERE claim_id = %d", $claim_id ); // Verify that the scheduled date for each action is within the expected bounds (in some unusual // cases, we cannot depend on MySQL to honor all of the WHERE conditions we specify). foreach ( $wpdb->get_results( $sql ) as $claimed_action ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared if ( $claimed_action->scheduled_date_gmt <= $cut_off ) { $action_ids[] = absint( $claimed_action->action_id ); } } return $action_ids; } /** * Release actions from a claim and delete the claim. * * @param ActionScheduler_ActionClaim $claim Claim object. */ public function release_claim( ActionScheduler_ActionClaim $claim ) { /** @var \wpdb $wpdb */ global $wpdb; $wpdb->update( $wpdb->actionscheduler_actions, array( 'claim_id' => 0 ), array( 'claim_id' => $claim->get_id() ), array( '%d' ), array( '%d' ) ); $wpdb->delete( $wpdb->actionscheduler_claims, array( 'claim_id' => $claim->get_id() ), array( '%d' ) ); } /** * Remove the claim from an action. * * @param int $action_id Action ID. * * @return void */ public function unclaim_action( $action_id ) { /** @var \wpdb $wpdb */ global $wpdb; $wpdb->update( $wpdb->actionscheduler_actions, array( 'claim_id' => 0 ), array( 'action_id' => $action_id ), array( '%s' ), array( '%d' ) ); } /** * Mark an action as failed. * * @param int $action_id Action ID. * @throws \InvalidArgumentException Throw an exception if action was not updated. */ public function mark_failure( $action_id ) { /** @var \wpdb $wpdb */ global $wpdb; $updated = $wpdb->update( $wpdb->actionscheduler_actions, array( 'status' => self::STATUS_FAILED ), array( 'action_id' => $action_id ), array( '%s' ), array( '%d' ) ); if ( empty( $updated ) ) { throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); //phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment } } /** * Add execution message to action log. * * @param int $action_id Action ID. * * @return void */ public function log_execution( $action_id ) { /** @var \wpdb $wpdb */ global $wpdb; $sql = "UPDATE {$wpdb->actionscheduler_actions} SET attempts = attempts+1, status=%s, last_attempt_gmt = %s, last_attempt_local = %s WHERE action_id = %d"; $sql = $wpdb->prepare( $sql, self::STATUS_RUNNING, current_time( 'mysql', true ), current_time( 'mysql' ), $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } /** * Mark an action as complete. * * @param int $action_id Action ID. * * @return void * @throws \InvalidArgumentException Throw an exception if action was not updated. */ public function mark_complete( $action_id ) { /** @var \wpdb $wpdb */ global $wpdb; $updated = $wpdb->update( $wpdb->actionscheduler_actions, array( 'status' => self::STATUS_COMPLETE, 'last_attempt_gmt' => current_time( 'mysql', true ), 'last_attempt_local' => current_time( 'mysql' ), ), array( 'action_id' => $action_id ), array( '%s' ), array( '%d' ) ); if ( empty( $updated ) ) { throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); //phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment } /** * Fires after a scheduled action has been completed. * * @since 3.4.2 * * @param int $action_id Action ID. */ do_action( 'action_scheduler_completed_action', $action_id ); } /** * Get an action's status. * * @param int $action_id Action ID. * * @return string * @throws \InvalidArgumentException Throw an exception if not status was found for action_id. * @throws \RuntimeException Throw an exception if action status could not be retrieved. */ public function get_status( $action_id ) { /** @var \wpdb $wpdb */ global $wpdb; $sql = "SELECT status FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d"; $sql = $wpdb->prepare( $sql, $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $status = $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared if ( null === $status ) { throw new \InvalidArgumentException( __( 'Invalid action ID. No status found.', 'action-scheduler' ) ); } elseif ( empty( $status ) ) { throw new \RuntimeException( __( 'Unknown status found for action.', 'action-scheduler' ) ); } else { return $status; } } }