<?php
/**
 * Upgrade API: WP_Automatic_Updater class
 *
 * @package WordPress
 * @subpackage Upgrader
 * @since 4.6.0
 */

/**
 * Core class used for handling automatic background updates.
 *
 * @since 3.7.0
 * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader.php.
 */
class WP_Automatic_Updater {

	/**
	 * Tracks update results during processing.
	 *
	 * @var array
	 */
	protected $update_results = array();

	/**
	 * Whether the entire automatic updater is disabled.
	 *
	 * @since 3.7.0
	 */
	public function is_disabled() {
		// Background updates are disabled if you don't want file changes.
		if ( ! wp_is_file_mod_allowed( 'automatic_updater' ) ) {
			return true;
		}

		if ( wp_installing() ) {
			return true;
		}

		// More fine grained control can be done through the WP_AUTO_UPDATE_CORE constant and filters.
		$disabled = defined( 'AUTOMATIC_UPDATER_DISABLED' ) && AUTOMATIC_UPDATER_DISABLED;

		/**
		 * Filters whether to entirely disable background updates.
		 *
		 * There are more fine-grained filters and controls for selective disabling.
		 * This filter parallels the AUTOMATIC_UPDATER_DISABLED constant in name.
		 *
		 * This also disables update notification emails. That may change in the future.
		 *
		 * @since 3.7.0
		 *
		 * @param bool $disabled Whether the updater should be disabled.
		 */
		return apply_filters( 'automatic_updater_disabled', $disabled );
	}

	/**
	 * Check for version control checkouts.
	 *
	 * Checks for Subversion, Git, Mercurial, and Bazaar. It recursively looks up the
	 * filesystem to the top of the drive, erring on the side of detecting a VCS
	 * checkout somewhere.
	 *
	 * ABSPATH is always checked in addition to whatever $context is (which may be the
	 * wp-content directory, for example). The underlying assumption is that if you are
	 * using version control *anywhere*, then you should be making decisions for
	 * how things get updated.
	 *
	 * @since 3.7.0
	 *
	 * @param string $context The filesystem path to check, in addition to ABSPATH.
	 */
	public function is_vcs_checkout( $context ) {
		$context_dirs = array( untrailingslashit( $context ) );
		if ( ABSPATH !== $context ) {
			$context_dirs[] = untrailingslashit( ABSPATH );
		}

		$vcs_dirs   = array( '.svn', '.git', '.hg', '.bzr' );
		$check_dirs = array();

		foreach ( $context_dirs as $context_dir ) {
			// Walk up from $context_dir to the root.
			do {
				$check_dirs[] = $context_dir;

				// Once we've hit '/' or 'C:\', we need to stop. dirname will keep returning the input here.
				if ( dirname( $context_dir ) === $context_dir ) {
					break;
				}

				// Continue one level at a time.
			} while ( $context_dir = dirname( $context_dir ) );
		}

		$check_dirs = array_unique( $check_dirs );

		// Search all directories we've found for evidence of version control.
		foreach ( $vcs_dirs as $vcs_dir ) {
			foreach ( $check_dirs as $check_dir ) {
				$checkout = @is_dir( rtrim( $check_dir, '\\/' ) . "/$vcs_dir" );
				if ( $checkout ) {
					break 2;
				}
			}
		}

		/**
		 * Filters whether the automatic updater should consider a filesystem
		 * location to be potentially managed by a version control system.
		 *
		 * @since 3.7.0
		 *
		 * @param bool $checkout  Whether a VCS checkout was discovered at $context
		 *                        or ABSPATH, or anywhere higher.
		 * @param string $context The filesystem context (a path) against which
		 *                        filesystem status should be checked.
		 */
		return apply_filters( 'automatic_updates_is_vcs_checkout', $checkout, $context );
	}

	/**
	 * Tests to see if we can and should update a specific item.
	 *
	 * @since 3.7.0
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 *
	 * @param string $type    The type of update being checked: 'core', 'theme',
	 *                        'plugin', 'translation'.
	 * @param object $item    The update offer.
	 * @param string $context The filesystem context (a path) against which filesystem
	 *                        access and status should be checked.
	 */
	public function should_update( $type, $item, $context ) {
		// Used to see if WP_Filesystem is set up to allow unattended updates.
		$skin = new Automatic_Upgrader_Skin;

		if ( $this->is_disabled() ) {
			return false;
		}

		// Only relax the filesystem checks when the update doesn't include new files.
		$allow_relaxed_file_ownership = false;
		if ( 'core' === $type && isset( $item->new_files ) && ! $item->new_files ) {
			$allow_relaxed_file_ownership = true;
		}

		// If we can't do an auto core update, we may still be able to email the user.
		if ( ! $skin->request_filesystem_credentials( false, $context, $allow_relaxed_file_ownership ) || $this->is_vcs_checkout( $context ) ) {
			if ( 'core' === $type ) {
				$this->send_core_update_notification_email( $item );
			}
			return false;
		}

		// Next up, is this an item we can update?
		if ( 'core' === $type ) {
			$update = Core_Upgrader::should_update_to_version( $item->current );
		} elseif ( 'plugin' === $type || 'theme' === $type ) {
			$update = ! empty( $item->autoupdate );

			if ( ! $update && wp_is_auto_update_enabled_for_type( $type ) ) {
				// Check if the site admin has enabled auto-updates by default for the specific item.
				$auto_updates = (array) get_site_option( "auto_update_{$type}s", array() );
				$update       = in_array( $item->{$type}, $auto_updates, true );
			}
		} else {
			$update = ! empty( $item->autoupdate );
		}

		// If the `disable_autoupdate` flag is set, override any user-choice, but allow filters.
		if ( ! empty( $item->disable_autoupdate ) ) {
			$update = $item->disable_autoupdate;
		}

		/**
		 * Filters whether to automatically update core, a plugin, a theme, or a language.
		 *
		 * The dynamic portion of the hook name, `$type`, refers to the type of update
		 * being checked. Potential hook names include:
		 *
		 *  - `auto_update_core`
		 *  - `auto_update_plugin`
		 *  - `auto_update_theme`
		 *  - `auto_update_translation`
		 *
		 * Generally speaking, plugins, themes, and major core versions are not updated
		 * by default, while translations and minor and development versions for core
		 * are updated by default.
		 *
		 * See the {@see 'allow_dev_auto_core_updates'}, {@see 'allow_minor_auto_core_updates'},
		 * and {@see 'allow_major_auto_core_updates'} filters for a more straightforward way to
		 * adjust core updates.
		 *
		 * @since 3.7.0
		 * @since 5.5.0 The `$update` parameter accepts the value of null.
		 *
		 * @param bool|null $update Whether to update. The value of null is internally used
		 *                          to detect whether nothing has hooked into this filter.
		 * @param object    $item   The update offer.
		 */
		$update = apply_filters( "auto_update_{$type}", $update, $item );

		if ( ! $update ) {
			if ( 'core' === $type ) {
				$this->send_core_update_notification_email( $item );
			}
			return false;
		}

		// If it's a core update, are we actually compatible with its requirements?
		if ( 'core' === $type ) {
			global $wpdb;

			$php_compat = version_compare( phpversion(), $item->php_version, '>=' );
			if ( file_exists( WP_CONTENT_DIR . '/db.php' ) && empty( $wpdb->is_mysql ) ) {
				$mysql_compat = true;
			} else {
				$mysql_compat = version_compare( $wpdb->db_version(), $item->mysql_version, '>=' );
			}

			if ( ! $php_compat || ! $mysql_compat ) {
				return false;
			}
		}

		// If updating a plugin or theme, ensure the minimum PHP version requirements are satisfied.
		if ( in_array( $type, array( 'plugin', 'theme' ), true ) ) {
			if ( ! empty( $item->requires_php ) && version_compare( phpversion(), $item->requires_php, '<' ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Notifies an administrator of a core update.
	 *
	 * @since 3.7.0
	 *
	 * @param object $item The update offer.
	 */
	protected function send_core_update_notification_email( $item ) {
		$notified = get_site_option( 'auto_core_update_notified' );

		// Don't notify if we've already notified the same email address of the same version.
		if ( $notified && get_site_option( 'admin_email' ) === $notified['email'] && $notified['version'] == $item->current ) {
			return false;
		}

		// See if we need to notify users of a core update.
		$notify = ! empty( $item->notify_email );

		/**
		 * Filters whether to notify the site administrator of a new core update.
		 *
		 * By default, administrators are notified when the update offer received
		 * from WordPress.org sets a particular flag. This allows some discretion
		 * in if and when to notify.
		 *
		 * This filter is only evaluated once per release. If the same email address
		 * was already notified of the same new version, WordPress won't repeatedly
		 * email the administrator.
		 *
		 * This filter is also used on about.php to check if a plugin has disabled
		 * these notifications.
		 *
		 * @since 3.7.0
		 *
		 * @param bool   $notify Whether the site administrator is notified.
		 * @param object $item   The update offer.
		 */
		if ( ! apply_filters( 'send_core_update_notification_email', $notify, $item ) ) {
			return false;
		}

		$this->send_email( 'manual', $item );
		return true;
	}

	/**
	 * Update an item, if appropriate.
	 *
	 * @since 3.7.0
	 *
	 * @param string $type The type of update being checked: 'core', 'theme', 'plugin', 'translation'.
	 * @param object $item The update offer.
	 * @return null|WP_Error
	 */
	public function update( $type, $item ) {
		$skin = new Automatic_Upgrader_Skin;

		switch ( $type ) {
			case 'core':
				// The Core upgrader doesn't use the Upgrader's skin during the actual main part of the upgrade, instead, firing a filter.
				add_filter( 'update_feedback', array( $skin, 'feedback' ) );
				$upgrader = new Core_Upgrader( $skin );
				$context  = ABSPATH;
				break;
			case 'plugin':
				$upgrader = new Plugin_Upgrader( $skin );
				$context  = WP_PLUGIN_DIR; // We don't support custom Plugin directories, or updates for WPMU_PLUGIN_DIR.
				break;
			case 'theme':
				$upgrader = new Theme_Upgrader( $skin );
				$context  = get_theme_root( $item->theme );
				break;
			case 'translation':
				$upgrader = new Language_Pack_Upgrader( $skin );
				$context  = WP_CONTENT_DIR; // WP_LANG_DIR;
				break;
		}

		// Determine whether we can and should perform this update.
		if ( ! $this->should_update( $type, $item, $context ) ) {
			return false;
		}

		/**
		 * Fires immediately prior to an auto-update.
		 *
		 * @since 4.4.0
		 *
		 * @param string $type    The type of update being checked: 'core', 'theme', 'plugin', or 'translation'.
		 * @param object $item    The update offer.
		 * @param string $context The filesystem context (a path) against which filesystem access and status
		 *                        should be checked.
		 */
		do_action( 'pre_auto_update', $type, $item, $context );

		$upgrader_item = $item;
		switch ( $type ) {
			case 'core':
				/* translators: %s: WordPress version. */
				$skin->feedback( __( 'Updating to WordPress %s' ), $item->version );
				/* translators: %s: WordPress version. */
				$item_name = sprintf( __( 'WordPress %s' ), $item->version );
				break;
			case 'theme':
				$upgrader_item = $item->theme;
				$theme         = wp_get_theme( $upgrader_item );
				$item_name     = $theme->Get( 'Name' );
				/* translators: %s: Theme name. */
				$skin->feedback( __( 'Updating theme: %s' ), $item_name );
				break;
			case 'plugin':
				$upgrader_item = $item->plugin;
				$plugin_data   = get_plugin_data( $context . '/' . $upgrader_item );
				$item_name     = $plugin_data['Name'];
				/* translators: %s: Plugin name. */
				$skin->feedback( __( 'Updating plugin: %s' ), $item_name );
				break;
			case 'translation':
				$language_item_name = $upgrader->get_name_for_update( $item );
				/* translators: %s: Project name (plugin, theme, or WordPress). */
				$item_name = sprintf( __( 'Translations for %s' ), $language_item_name );
				/* translators: 1: Project name (plugin, theme, or WordPress), 2: Language. */
				$skin->feedback( sprintf( __( 'Updating translations for %1$s (%2$s)…' ), $language_item_name, $item->language ) );
				break;
		}

		$allow_relaxed_file_ownership = false;
		if ( 'core' === $type && isset( $item->new_files ) && ! $item->new_files ) {
			$allow_relaxed_file_ownership = true;
		}

		// Boom, this site's about to get a whole new splash of paint!
		$upgrade_result = $upgrader->upgrade(
			$upgrader_item,
			array(
				'clear_update_cache'           => false,
				// Always use partial builds if possible for core updates.
				'pre_check_md5'                => false,
				// Only available for core updates.
				'attempt_rollback'             => true,
				// Allow relaxed file ownership in some scenarios.
				'allow_relaxed_file_ownership' => $allow_relaxed_file_ownership,
			)
		);

		// If the filesystem is unavailable, false is returned.
		if ( false === $upgrade_result ) {
			$upgrade_result = new WP_Error( 'fs_unavailable', __( 'Could not access filesystem.' ) );
		}

		if ( 'core' === $type ) {
			if ( is_wp_error( $upgrade_result )
				&& ( 'up_to_date' === $upgrade_result->get_error_code()
					|| 'locked' === $upgrade_result->get_error_code() )
			) {
				// These aren't actual errors, treat it as a skipped-update instead
				// to avoid triggering the post-core update failure routines.
				return false;
			}

			// Core doesn't output this, so let's append it so we don't get confused.
			if ( is_wp_error( $upgrade_result ) ) {
				$skin->error( __( 'Installation failed.' ), $upgrade_result );
			} else {
				$skin->feedback( __( 'WordPress updated successfully.' ) );
			}
		}

		$this->update_results[ $type ][] = (object) array(
			'item'     => $item,
			'result'   => $upgrade_result,
			'name'     => $item_name,
			'messages' => $skin->get_upgrade_messages(),
		);

		return $upgrade_result;
	}

	/**
	 * Kicks off the background update process, looping through all pending updates.
	 *
	 * @since 3.7.0
	 */
	public function run() {
		if ( $this->is_disabled() ) {
			return;
		}

		if ( ! is_main_network() || ! is_main_site() ) {
			return;
		}

		if ( ! WP_Upgrader::create_lock( 'auto_updater' ) ) {
			return;
		}

		// Don't automatically run these things, as we'll handle it ourselves.
		remove_action( 'upgrader_process_complete', array( 'Language_Pack_Upgrader', 'async_upgrade' ), 20 );
		remove_action( 'upgrader_process_complete', 'wp_version_check' );
		remove_action( 'upgrader_process_complete', 'wp_update_plugins' );
		remove_action( 'upgrader_process_complete', 'wp_update_themes' );

		// Next, plugins.
		wp_update_plugins(); // Check for plugin updates.
		$plugin_updates = get_site_transient( 'update_plugins' );
		if ( $plugin_updates && ! empty( $plugin_updates->response ) ) {
			foreach ( $plugin_updates->response as $plugin ) {
				$this->update( 'plugin', $plugin );
			}
			// Force refresh of plugin update information.
			wp_clean_plugins_cache();
		}

		// Next, those themes we all love.
		wp_update_themes();  // Check for theme updates.
		$theme_updates = get_site_transient( 'update_themes' );
		if ( $theme_updates && ! empty( $theme_updates->response ) ) {
			foreach ( $theme_updates->response as $theme ) {
				$this->update( 'theme', (object) $theme );
			}
			// Force refresh of theme update information.
			wp_clean_themes_cache();
		}

		// Next, process any core update.
		wp_version_check(); // Check for core updates.
		$core_update = find_core_auto_update();

		if ( $core_update ) {
			$this->update( 'core', $core_update );
		}

		// Clean up, and check for any pending translations.
		// (Core_Upgrader checks for core updates.)
		$theme_stats = array();
		if ( isset( $this->update_results['theme'] ) ) {
			foreach ( $this->update_results['theme'] as $upgrade ) {
				$theme_stats[ $upgrade->item->theme ] = ( true === $upgrade->result );
			}
		}
		wp_update_themes( $theme_stats ); // Check for theme updates.

		$plugin_stats = array();
		if ( isset( $this->update_results['plugin'] ) ) {
			foreach ( $this->update_results['plugin'] as $upgrade ) {
				$plugin_stats[ $upgrade->item->plugin ] = ( true === $upgrade->result );
			}
		}
		wp_update_plugins( $plugin_stats ); // Check for plugin updates.

		// Finally, process any new translations.
		$language_updates = wp_get_translation_updates();
		if ( $language_updates ) {
			foreach ( $language_updates as $update ) {
				$this->update( 'translation', $update );
			}

			// Clear existing caches.
			wp_clean_update_cache();

			wp_version_check();  // Check for core updates.
			wp_update_themes();  // Check for theme updates.
			wp_update_plugins(); // Check for plugin updates.
		}

		// Send debugging email to admin for all development installations.
		if ( ! empty( $this->update_results ) ) {
			$development_version = false !== strpos( get_bloginfo( 'version' ), '-' );

			/**
			 * Filters whether to send a debugging email for each automatic background update.
			 *
			 * @since 3.7.0
			 *
			 * @param bool $development_version By default, emails are sent if the
			 *                                  install is a development version.
			 *                                  Return false to avoid the email.
			 */
			if ( apply_filters( 'automatic_updates_send_debug_email', $development_version ) ) {
				$this->send_debug_email();
			}

			if ( ! empty( $this->update_results['core'] ) ) {
				$this->after_core_update( $this->update_results['core'][0] );
			} elseif ( ! empty( $this->update_results['plugin'] ) || ! empty( $this->update_results['theme'] ) ) {
				$this->after_plugin_theme_update( $this->update_results );
			}

			/**
			 * Fires after all automatic updates have run.
			 *
			 * @since 3.8.0
			 *
			 * @param array $update_results The results of all attempted updates.
			 */
			do_action( 'automatic_updates_complete', $this->update_results );
		}

		WP_Upgrader::release_lock( 'auto_updater' );
	}

	/**
	 * If we tried to perform a core update, check if we should send an email,
	 * and if we need to avoid processing future updates.
	 *
	 * @since 3.7.0
	 *
	 * @param object $update_result The result of the core update. Includes the update offer and result.
	 */
	protected function after_core_update( $update_result ) {
		$wp_version = get_bloginfo( 'version' );

		$core_update = $update_result->item;
		$result      = $update_result->result;

		if ( ! is_wp_error( $result ) ) {
			$this->send_email( 'success', $core_update );
			return;
		}

		$error_code = $result->get_error_code();

		// Any of these WP_Error codes are critical failures, as in they occurred after we started to copy core files.
		// We should not try to perform a background update again until there is a successful one-click update performed by the user.
		$critical = false;
		if ( 'disk_full' === $error_code || false !== strpos( $error_code, '__copy_dir' ) ) {
			$critical = true;
		} elseif ( 'rollback_was_required' === $error_code && is_wp_error( $result->get_error_data()->rollback ) ) {
			// A rollback is only critical if it failed too.
			$critical        = true;
			$rollback_result = $result->get_error_data()->rollback;
		} elseif ( false !== strpos( $error_code, 'do_rollback' ) ) {
			$critical = true;
		}

		if ( $critical ) {
			$critical_data = array(
				'attempted'  => $core_update->current,
				'current'    => $wp_version,
				'error_code' => $error_code,
				'error_data' => $result->get_error_data(),
				'timestamp'  => time(),
				'critical'   => true,
			);
			if ( isset( $rollback_result ) ) {
				$critical_data['rollback_code'] = $rollback_result->get_error_code();
				$critical_data['rollback_data'] = $rollback_result->get_error_data();
			}
			update_site_option( 'auto_core_update_failed', $critical_data );
			$this->send_email( 'critical', $core_update, $result );
			return;
		}

		/*
		 * Any other WP_Error code (like download_failed or files_not_writable) occurs before
		 * we tried to copy over core files. Thus, the failures are early and graceful.
		 *
		 * We should avoid trying to perform a background update again for the same version.
		 * But we can try again if another version is released.
		 *
		 * For certain 'transient' failures, like download_failed, we should allow retries.
		 * In fact, let's schedule a special update for an hour from now. (It's possible
		 * the issue could actually be on WordPress.org's side.) If that one fails, then email.
		 */
		$send               = true;
		$transient_failures = array( 'incompatible_archive', 'download_failed', 'insane_distro', 'locked' );
		if ( in_array( $error_code, $transient_failures, true ) && ! get_site_option( 'auto_core_update_failed' ) ) {
			wp_schedule_single_event( time() + HOUR_IN_SECONDS, 'wp_maybe_auto_update' );
			$send = false;
		}

		$n = get_site_option( 'auto_core_update_notified' );
		// Don't notify if we've already notified the same email address of the same version of the same notification type.
		if ( $n && 'fail' === $n['type'] && get_site_option( 'admin_email' ) === $n['email'] && $n['version'] == $core_update->current ) {
			$send = false;
		}

		update_site_option(
			'auto_core_update_failed',
			array(
				'attempted'  => $core_update->current,
				'current'    => $wp_version,
				'error_code' => $error_code,
				'error_data' => $result->get_error_data(),
				'timestamp'  => time(),
				'retry'      => in_array( $error_code, $transient_failures, true ),
			)
		);

		if ( $send ) {
			$this->send_email( 'fail', $core_update, $result );
		}
	}

	/**
	 * Sends an email upon the completion or failure of a background core update.
	 *
	 * @since 3.7.0
	 *
	 * @param string $type        The type of email to send. Can be one of 'success', 'fail', 'manual', 'critical'.
	 * @param object $core_update The update offer that was attempted.
	 * @param mixed  $result      Optional. The result for the core update. Can be WP_Error.
	 */
	protected function send_email( $type, $core_update, $result = null ) {
		update_site_option(
			'auto_core_update_notified',
			array(
				'type'      => $type,
				'email'     => get_site_option( 'admin_email' ),
				'version'   => $core_update->current,
				'timestamp' => time(),
			)
		);

		$next_user_core_update = get_preferred_from_update_core();

		// If the update transient is empty, use the update we just performed.
		if ( ! $next_user_core_update ) {
			$next_user_core_update = $core_update;
		}

		$newer_version_available = ( 'upgrade' === $next_user_core_update->response && version_compare( $next_user_core_update->version, $core_update->version, '>' ) );

		/**
		 * Filters whether to send an email following an automatic background core update.
		 *
		 * @since 3.7.0
		 *
		 * @param bool   $send        Whether to send the email. Default true.
		 * @param string $type        The type of email to send. Can be one of
		 *                            'success', 'fail', 'critical'.
		 * @param object $core_update The update offer that was attempted.
		 * @param mixed  $result      The result for the core update. Can be WP_Error.
		 */
		if ( 'manual' !== $type && ! apply_filters( 'auto_core_update_send_email', true, $type, $core_update, $result ) ) {
			return;
		}

		switch ( $type ) {
			case 'success': // We updated.
				/* translators: Site updated notification email subject. 1: Site title, 2: WordPress version. */
				$subject = __( '[%1$s] Your site has updated to WordPress %2$s' );
				break;

			case 'fail':   // We tried to update but couldn't.
			case 'manual': // We can't update (and made no attempt).
				/* translators: Update available notification email subject. 1: Site title, 2: WordPress version. */
				$subject = __( '[%1$s] WordPress %2$s is available. Please update!' );
				break;

			case 'critical': // We tried to update, started to copy files, then things went wrong.
				/* translators: Site down notification email subject. 1: Site title. */
				$subject = __( '[%1$s] URGENT: Your site may be down due to a failed update' );
				break;

			default:
				return;
		}

		// If the auto-update is not to the latest version, say that the current version of WP is available instead.
		$version = 'success' === $type ? $core_update->current : $next_user_core_update->current;
		$subject = sprintf( $subject, wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ), $version );

		$body = '';

		switch ( $type ) {
			case 'success':
				$body .= sprintf(
					/* translators: 1: Home URL, 2: WordPress version. */
					__( 'Howdy! Your site at %1$s has been updated automatically to WordPress %2$s.' ),
					home_url(),
					$core_update->current
				);
				$body .= "\n\n";
				if ( ! $newer_version_available ) {
					$body .= __( 'No further action is needed on your part.' ) . ' ';
				}

				// Can only reference the About screen if their update was successful.
				list( $about_version ) = explode( '-', $core_update->current, 2 );
				/* translators: %s: WordPress version. */
				$body .= sprintf( __( 'For more on version %s, see the About WordPress screen:' ), $about_version );
				$body .= "\n" . admin_url( 'about.php' );

				if ( $newer_version_available ) {
					/* translators: %s: WordPress latest version. */
					$body .= "\n\n" . sprintf( __( 'WordPress %s is also now available.' ), $next_user_core_update->current ) . ' ';
					$body .= __( 'Updating is easy and only takes a few moments:' );
					$body .= "\n" . network_admin_url( 'update-core.php' );
				}

				break;

			case 'fail':
			case 'manual':
				$body .= sprintf(
					/* translators: 1: Home URL, 2: WordPress version. */
					__( 'Please update your site at %1$s to WordPress %2$s.' ),
					home_url(),
					$next_user_core_update->current
				);

				$body .= "\n\n";

				// Don't show this message if there is a newer version available.
				// Potential for confusion, and also not useful for them to know at this point.
				if ( 'fail' === $type && ! $newer_version_available ) {
					$body .= __( 'We tried but were unable to update your site automatically.' ) . ' ';
				}

				$body .= __( 'Updating is easy and only takes a few moments:' );
				$body .= "\n" . network_admin_url( 'update-core.php' );
				break;

			case 'critical':
				if ( $newer_version_available ) {
					$body .= sprintf(
						/* translators: 1: Home URL, 2: WordPress version. */
						__( 'Your site at %1$s experienced a critical failure while trying to update WordPress to version %2$s.' ),
						home_url(),
						$core_update->current
					);
				} else {
					$body .= sprintf(
						/* translators: 1: Home URL, 2: WordPress latest version. */
						__( 'Your site at %1$s experienced a critical failure while trying to update to the latest version of WordPress, %2$s.' ),
						home_url(),
						$core_update->current
					);
				}

				$body .= "\n\n" . __( "This means your site may be offline or broken. Don't panic; this can be fixed." );

				$body .= "\n\n" . __( "Please check out your site now. It's possible that everything is working. If it says you need to update, you should do so:" );
				$body .= "\n" . network_admin_url( 'update-core.php' );
				break;
		}

		$critical_support = 'critical' === $type && ! empty( $core_update->support_email );
		if ( $critical_support ) {
			// Support offer if available.
			$body .= "\n\n" . sprintf(
				/* translators: %s: Support email address. */
				__( 'The WordPress team is willing to help you. Forward this email to %s and the team will work with you to make sure your site is working.' ),
				$core_update->support_email
			);
		} else {
			// Add a note about the support forums.
			$body .= "\n\n" . __( 'If you experience any issues or need support, the volunteers in the WordPress.org support forums may be able to help.' );
			$body .= "\n" . __( 'https://wordpress.org/support/forums/' );
		}

		// Updates are important!
		if ( 'success' !== $type || $newer_version_available ) {
			$body .= "\n\n" . __( 'Keeping your site updated is important for security. It also makes the internet a safer place for you and your readers.' );
		}

		if ( $critical_support ) {
			$body .= ' ' . __( "If you reach out to us, we'll also ensure you'll never have this problem again." );
		}

		// If things are successful and we're now on the latest, mention plugins and themes if any are out of date.
		if ( 'success' === $type && ! $newer_version_available && ( get_plugin_updates() || get_theme_updates() ) ) {
			$body .= "\n\n" . __( 'You also have some plugins or themes with updates available. Update them now:' );
			$body .= "\n" . network_admin_url();
		}

		$body .= "\n\n" . __( 'The WordPress Team' ) . "\n";

		if ( 'critical' === $type && is_wp_error( $result ) ) {
			$body .= "\n***\n\n";
			/* translators: %s: WordPress version. */
			$body .= sprintf( __( 'Your site was running version %s.' ), get_bloginfo( 'version' ) );
			$body .= ' ' . __( 'We have some data that describes the error your site encountered.' );
			$body .= ' ' . __( 'Your hosting company, support forum volunteers, or a friendly developer may be able to use this information to help you:' );

			// If we had a rollback and we're still critical, then the rollback failed too.
			// Loop through all errors (the main WP_Error, the update result, the rollback result) for code, data, etc.
			if ( 'rollback_was_required' === $result->get_error_code() ) {
				$errors = array( $result, $result->get_error_data()->update, $result->get_error_data()->rollback );
			} else {
				$errors = array( $result );
			}

			foreach ( $errors as $error ) {
				if ( ! is_wp_error( $error ) ) {
					continue;
				}

				$error_code = $error->get_error_code();
				/* translators: %s: Error code. */
				$body .= "\n\n" . sprintf( __( 'Error code: %s' ), $error_code );

				if ( 'rollback_was_required' === $error_code ) {
					continue;
				}

				if ( $error->get_error_message() ) {
					$body .= "\n" . $error->get_error_message();
				}

				$error_data = $error->get_error_data();
				if ( $error_data ) {
					$body .= "\n" . implode( ', ', (array) $error_data );
				}
			}

			$body .= "\n";
		}

		$to      = get_site_option( 'admin_email' );
		$headers = '';

		$email = compact( 'to', 'subject', 'body', 'headers' );

		/**
		 * Filters the email sent following an automatic background core update.
		 *
		 * @since 3.7.0
		 *
		 * @param array $email {
		 *     Array of email arguments that will be passed to wp_mail().
		 *
		 *     @type string $to      The email recipient. An array of emails
		 *                            can be returned, as handled by wp_mail().
		 *     @type string $subject The email's subject.
		 *     @type string $body    The email message body.
		 *     @type string $headers Any email headers, defaults to no headers.
		 * }
		 * @param string $type        The type of email being sent. Can be one of
		 *                            'success', 'fail', 'manual', 'critical'.
		 * @param object $core_update The update offer that was attempted.
		 * @param mixed  $result      The result for the core update. Can be WP_Error.
		 */
		$email = apply_filters( 'auto_core_update_email', $email, $type, $core_update, $result );

		wp_mail( $email['to'], wp_specialchars_decode( $email['subject'] ), $email['body'], $email['headers'] );
	}

	/**
	 * If we tried to perform plugin or theme updates, check if we should send an email.
	 *
	 * @since 5.5.0
	 *
	 * @param array $update_results The results of update tasks.
	 */
	protected function after_plugin_theme_update( $update_results ) {
		$successful_updates = array();
		$failed_updates     = array();

		if ( ! empty( $update_results['plugin'] ) ) {
			/**
			 * Filters whether to send an email following an automatic background plugin update.
			 *
			 * @since 5.5.0
			 * @since 5.5.1 Added the `$update_results` parameter.
			 *
			 * @param bool  $enabled        True if plugin update notifications are enabled, false otherwise.
			 * @param array $update_results The results of plugins update tasks.
			 */
			$notifications_enabled = apply_filters( 'auto_plugin_update_send_email', true, $update_results['plugin'] );

			if ( $notifications_enabled ) {
				foreach ( $update_results['plugin'] as $update_result ) {
					if ( true === $update_result->result ) {
						$successful_updates['plugin'][] = $update_result;
					} else {
						$failed_updates['plugin'][] = $update_result;
					}
				}
			}
		}

		if ( ! empty( $update_results['theme'] ) ) {
			/**
			 * Filters whether to send an email following an automatic background theme update.
			 *
			 * @since 5.5.0
			 * @since 5.5.1 Added the `$update_results` parameter.
			 *
			 * @param bool  $enabled        True if theme update notifications are enabled, false otherwise.
			 * @param array $update_results The results of theme update tasks.
			 */
			$notifications_enabled = apply_filters( 'auto_theme_update_send_email', true, $update_results['theme'] );

			if ( $notifications_enabled ) {
				foreach ( $update_results['theme'] as $update_result ) {
					if ( true === $update_result->result ) {
						$successful_updates['theme'][] = $update_result;
					} else {
						$failed_updates['theme'][] = $update_result;
					}
				}
			}
		}

		if ( empty( $successful_updates ) && empty( $failed_updates ) ) {
			return;
		}

		if ( empty( $failed_updates ) ) {
			$this->send_plugin_theme_email( 'success', $successful_updates, $failed_updates );
		} elseif ( empty( $successful_updates ) ) {
			$this->send_plugin_theme_email( 'fail', $successful_updates, $failed_updates );
		} else {
			$this->send_plugin_theme_email( 'mixed', $successful_updates, $failed_updates );
		}
	}

	/**
	 * Sends an email upon the completion or failure of a plugin or theme background update.
	 *
	 * @since 5.5.0
	 *
	 * @param string $type               The type of email to send. Can be one of 'success', 'fail', 'mixed'.
	 * @param array  $successful_updates A list of updates that succeeded.
	 * @param array  $failed_updates     A list of updates that failed.
	 */
	protected function send_plugin_theme_email( $type, $successful_updates, $failed_updates ) {
		// No updates were attempted.
		if ( empty( $successful_updates ) && empty( $failed_updates ) ) {
			return;
		}

		$unique_failures     = false;
		$past_failure_emails = get_option( 'auto_plugin_theme_update_emails', array() );

		/*
		 * When only failures have occurred, an email should only be sent if there are unique failures.
		 * A failure is considered unique if an email has not been sent for an update attempt failure
		 * to a plugin or theme with the same new_version.
		 */
		if ( 'fail' === $type ) {
			foreach ( $failed_updates as $update_type => $failures ) {
				foreach ( $failures as $failed_update ) {
					if ( ! isset( $past_failure_emails[ $failed_update->item->{$update_type} ] ) ) {
						$unique_failures = true;
						continue;
					}

					// Check that the failure represents a new failure based on the new_version.
					if ( version_compare( $past_failure_emails[ $failed_update->item->{$update_type} ], $failed_update->item->new_version, '<' ) ) {
						$unique_failures = true;
					}
				}
			}

			if ( ! $unique_failures ) {
				return;
			}
		}

		$body               = array();
		$successful_plugins = ( ! empty( $successful_updates['plugin'] ) );
		$successful_themes  = ( ! empty( $successful_updates['theme'] ) );
		$failed_plugins     = ( ! empty( $failed_updates['plugin'] ) );
		$failed_themes      = ( ! empty( $failed_updates['theme'] ) );

		switch ( $type ) {
			case 'success':
				if ( $successful_plugins && $successful_themes ) {
					/* translators: %s: Site title. */
					$subject = __( '[%s] Some plugins and themes have automatically updated' );
					$body[]  = sprintf(
						/* translators: %s: Home URL. */
						__( 'Howdy! Some plugins and themes have automatically updated to their latest versions on your site at %s. No further action is needed on your part.' ),
						home_url()
					);
				} elseif ( $successful_plugins ) {
					/* translators: %s: Site title. */
					$subject = __( '[%s] Some plugins were automatically updated' );
					$body[]  = sprintf(
						/* translators: %s: Home URL. */
						__( 'Howdy! Some plugins have automatically updated to their latest versions on your site at %s. No further action is needed on your part.' ),
						home_url()
					);
				} else {
					/* translators: %s: Site title. */
					$subject = __( '[%s] Some themes were automatically updated' );
					$body[]  = sprintf(
						/* translators: %s: Home URL. */
						__( 'Howdy! Some themes have automatically updated to their latest versions on your site at %s. No further action is needed on your part.' ),
						home_url()
					);
				}

				break;
			case 'fail':
			case 'mixed':
				if ( $failed_plugins && $failed_themes ) {
					/* translators: %s: Site title. */
					$subject = __( '[%s] Some plugins and themes have failed to update' );
					$body[]  = sprintf(
						/* translators: %s: Home URL. */
						__( 'Howdy! Plugins and themes failed to update on your site at %s.' ),
						home_url()
					);
				} elseif ( $failed_plugins ) {
					/* translators: %s: Site title. */
					$subject = __( '[%s] Some plugins have failed to update' );
					$body[]  = sprintf(
						/* translators: %s: Home URL. */
						__( 'Howdy! Plugins failed to update on your site at %s.' ),
						home_url()
					);
				} else {
					/* translators: %s: Site title. */
					$subject = __( '[%s] Some themes have failed to update' );
					$body[]  = sprintf(
						/* translators: %s: Home URL. */
						__( 'Howdy! Themes failed to update on your site at %s.' ),
						home_url()
					);
				}

				break;
		}

		if ( in_array( $type, array( 'fail', 'mixed' ), true ) ) {
			$body[] = "\n";
			$body[] = __( 'Please check your site now. Itâ€™s possible that everything is working. If there are updates available, you should update.' );
			$body[] = "\n";

			// List failed plugin updates.
			if ( ! empty( $failed_updates['plugin'] ) ) {
				$body[] = __( 'These plugins failed to update:' );

				foreach ( $failed_updates['plugin'] as $item ) {
					$body[] = sprintf(
						/* translators: 1: Plugin name, 2: Version number. */
						__( '- %1$s version %2$s' ),
						$item->name,
						$item->item->new_version
					);

					$past_failure_emails[ $item->item->plugin ] = $item->item->new_version;
				}

				$body[] = "\n";
			}

			// List failed theme updates.
			if ( ! empty( $failed_updates['theme'] ) ) {
				$body[] = __( 'These themes failed to update:' );

				foreach ( $failed_updates['theme'] as $item ) {
					$body[] = sprintf(
						/* translators: 1: Theme name, 2: Version number. */
						__( '- %1$s version %2$s' ),
						$item->name,
						$item->item->new_version
					);

					$past_failure_emails[ $item->item->theme ] = $item->item->new_version;
				}

				$body[] = "\n";
			}
		}

		// List successful updates.
		if ( in_array( $type, array( 'success', 'mixed' ), true ) ) {
			$body[] = "\n";

			// List successful plugin updates.
			if ( ! empty( $successful_updates['plugin'] ) ) {
				$body[] = __( 'These plugins are now up to date:' );

				foreach ( $successful_updates['plugin'] as $item ) {
					$body[] = sprintf(
						/* translators: 1: Plugin name, 2: Version number. */
						__( '- %1$s version %2$s' ),
						$item->name,
						$item->item->new_version
					);

					unset( $past_failure_emails[ $item->item->plugin ] );
				}

				$body[] = "\n";
			}

			// List successful theme updates.
			if ( ! empty( $successful_updates['theme'] ) ) {
				$body[] = __( 'These themes are now up to date:' );

				foreach ( $successful_updates['theme'] as $item ) {
					$body[] = sprintf(
						/* translators: 1: Theme name, 2: Version number. */
						__( '- %1$s version %2$s' ),
						$item->name,
						$item->item->new_version
					);

					unset( $past_failure_emails[ $item->item->theme ] );
				}

				$body[] = "\n";
			}
		}

		if ( $failed_plugins ) {
			$body[] = sprintf(
				/* translators: %s: Plugins screen URL. */
				__( 'To manage plugins on your site, visit the Plugins page: %s' ),
				admin_url( 'plugins.php' )
			);
			$body[] = "\n";
		}

		if ( $failed_themes ) {
			$body[] = sprintf(
				/* translators: %s: Themes screen URL. */
				__( 'To manage themes on your site, visit the Themes page: %s' ),
				admin_url( 'themes.php' )
			);
			$body[] = "\n";
		}

		// Add a note about the support forums.
		$body[] = __( 'If you experience any issues or need support, the volunteers in the WordPress.org support forums may be able to help.' );
		$body[] = __( 'https://wordpress.org/support/forums/' );
		$body[] = "\n" . __( 'The WordPress Team' );

		$body    = implode( "\n", $body );
		$to      = get_site_option( 'admin_email' );
		$subject = sprintf( $subject, wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) );
		$headers = '';

		$email = compact( 'to', 'subject', 'body', 'headers' );

		/**
		 * Filters the email sent following an automatic background update for plugins and themes.
		 *
		 * @since 5.5.0
		 *
		 * @param array  $email {
		 *     Array of email arguments that will be passed to wp_mail().
		 *
		 *     @type string $to      The email recipient. An array of emails
		 *                           can be returned, as handled by wp_mail().
		 *     @type string $subject The email's subject.
		 *     @type string $body    The email message body.
		 *     @type string $headers Any email headers, defaults to no headers.
		 * }
		 * @param string $type               The type of email being sent. Can be one of 'success', 'fail', 'mixed'.
		 * @param array  $successful_updates A list of updates that succeeded.
		 * @param array  $failed_updates     A list of updates that failed.
		 */
		$email = apply_filters( 'auto_plugin_theme_update_email', $email, $type, $successful_updates, $failed_updates );

		$result = wp_mail( $email['to'], wp_specialchars_decode( $email['subject'] ), $email['body'], $email['headers'] );

		if ( $result ) {
			update_option( 'auto_plugin_theme_update_emails', $past_failure_emails );
		}
	}

	/**
	 * Prepares and sends an email of a full log of background update results, useful for debugging and geekery.
	 *
	 * @since 3.7.0
	 */
	protected function send_debug_email() {
		$update_count = 0;
		foreach ( $this->update_results as $type => $updates ) {
			$update_count += count( $updates );
		}

		$body     = array();
		$failures = 0;

		/* translators: %s: Network home URL. */
		$body[] = sprintf( __( 'WordPress site: %s' ), network_home_url( '/' ) );

		// Core.
		if ( isset( $this->update_results['core'] ) ) {
			$result = $this->update_results['core'][0];
			if ( $result->result && ! is_wp_error( $result->result ) ) {
				/* translators: %s: WordPress version. */
				$body[] = sprintf( __( 'SUCCESS: WordPress was successfully updated to %s' ), $result->name );
			} else {
				/* translators: %s: WordPress version. */
				$body[] = sprintf( __( 'FAILED: WordPress failed to update to %s' ), $result->name );
				$failures++;
			}
			$body[] = '';
		}

		// Plugins, Themes, Translations.
		foreach ( array( 'plugin', 'theme', 'translation' ) as $type ) {
			if ( ! isset( $this->update_results[ $type ] ) ) {
				continue;
			}
			$success_items = wp_list_filter( $this->update_results[ $type ], array( 'result' => true ) );
			if ( $success_items ) {
				$messages = array(
					'plugin'      => __( 'The following plugins were successfully updated:' ),
					'theme'       => __( 'The following themes were successfully updated:' ),
					'translation' => __( 'The following translations were successfully updated:' ),
				);

				$body[] = $messages[ $type ];
				foreach ( wp_list_pluck( $success_items, 'name' ) as $name ) {
					/* translators: %s: Name of plugin / theme / translation. */
					$body[] = ' * ' . sprintf( __( 'SUCCESS: %s' ), $name );
				}
			}
			if ( $success_items != $this->update_results[ $type ] ) {
				// Failed updates.
				$messages = array(
					'plugin'      => __( 'The following plugins failed to update:' ),
					'theme'       => __( 'The following themes failed to update:' ),
					'translation' => __( 'The following translations failed to update:' ),
				);

				$body[] = $messages[ $type ];
				foreach ( $this->update_results[ $type ] as $item ) {
					if ( ! $item->result || is_wp_error( $item->result ) ) {
						/* translators: %s: Name of plugin / theme / translation. */
						$body[] = ' * ' . sprintf( __( 'FAILED: %s' ), $item->name );
						$failures++;
					}
				}
			}
			$body[] = '';
		}

		$site_title = wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES );
		if ( $failures ) {
			$body[] = trim(
				__(
					"BETA TESTING?
=============

This debugging email is sent when you are using a development version of WordPress.

If you think these failures might be due to a bug in WordPress, could you report it?
 * Open a thread in the support forums: https://wordpress.org/support/forum/alphabeta
 * Or, if you're comfortable writing a bug report: https://core.trac.wordpress.org/

Thanks! -- The WordPress Team"
				)
			);
			$body[] = '';

			/* translators: Background update failed notification email subject. %s: Site title. */
			$subject = sprintf( __( '[%s] Background Update Failed' ), $site_title );
		} else {
			/* translators: Background update finished notification email subject. %s: Site title. */
			$subject = sprintf( __( '[%s] Background Update Finished' ), $site_title );
		}

		$body[] = trim(
			__(
				'UPDATE LOG
=========='
			)
		);
		$body[] = '';

		foreach ( array( 'core', 'plugin', 'theme', 'translation' ) as $type ) {
			if ( ! isset( $this->update_results[ $type ] ) ) {
				continue;
			}
			foreach ( $this->update_results[ $type ] as $update ) {
				$body[] = $update->name;
				$body[] = str_repeat( '-', strlen( $update->name ) );
				foreach ( $update->messages as $message ) {
					$body[] = '  ' . html_entity_decode( str_replace( '…', '...', $message ) );
				}
				if ( is_wp_error( $update->result ) ) {
					$results = array( 'update' => $update->result );
					// If we rolled back, we want to know an error that occurred then too.
					if ( 'rollback_was_required' === $update->result->get_error_code() ) {
						$results = (array) $update->result->get_error_data();
					}
					foreach ( $results as $result_type => $result ) {
						if ( ! is_wp_error( $result ) ) {
							continue;
						}

						if ( 'rollback' === $result_type ) {
							/* translators: 1: Error code, 2: Error message. */
							$body[] = '  ' . sprintf( __( 'Rollback Error: [%1$s] %2$s' ), $result->get_error_code(), $result->get_error_message() );
						} else {
							/* translators: 1: Error code, 2: Error message. */
							$body[] = '  ' . sprintf( __( 'Error: [%1$s] %2$s' ), $result->get_error_code(), $result->get_error_message() );
						}

						if ( $result->get_error_data() ) {
							$body[] = '         ' . implode( ', ', (array) $result->get_error_data() );
						}
					}
				}
				$body[] = '';
			}
		}

		$email = array(
			'to'      => get_site_option( 'admin_email' ),
			'subject' => $subject,
			'body'    => implode( "\n", $body ),
			'headers' => '',
		);

		/**
		 * Filters the debug email that can be sent following an automatic
		 * background core update.
		 *
		 * @since 3.8.0
		 *
		 * @param array $email {
		 *     Array of email arguments that will be passed to wp_mail().
		 *
		 *     @type string $to      The email recipient. An array of emails
		 *                           can be returned, as handled by wp_mail().
		 *     @type string $subject Email subject.
		 *     @type string $body    Email message body.
		 *     @type string $headers Any email headers. Default empty.
		 * }
		 * @param int   $failures The number of failures encountered while upgrading.
		 * @param mixed $results  The results of all attempted updates.
		 */
		$email = apply_filters( 'automatic_updates_debug_email', $email, $failures, $this->update_results );

		wp_mail( $email['to'], wp_specialchars_decode( $email['subject'] ), $email['body'], $email['headers'] );
	}
}


<?php
/**
 * List Table API: WP_Comments_List_Table class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 3.1.0
 */

/**
 * Core class used to implement displaying comments in a list table.
 *
 * @since 3.1.0
 * @access private
 *
 * @see WP_List_Table
 */
class WP_Comments_List_Table extends WP_List_Table {

	public $checkbox = true;

	public $pending_count = array();

	public $extra_items;

	private $user_can;

	/**
	 * Constructor.
	 *
	 * @since 3.1.0
	 *
	 * @see WP_List_Table::__construct() for more information on default arguments.
	 *
	 * @global int $post_id
	 *
	 * @param array $args An associative array of arguments.
	 */
	public function __construct( $args = array() ) {
		global $post_id;

		$post_id = isset( $_REQUEST['p'] ) ? absint( $_REQUEST['p'] ) : 0;

		if ( get_option( 'show_avatars' ) ) {
			add_filter( 'comment_author', array( $this, 'floated_admin_avatar' ), 10, 2 );
		}

		parent::__construct(
			array(
				'plural'   => 'comments',
				'singular' => 'comment',
				'ajax'     => true,
				'screen'   => isset( $args['screen'] ) ? $args['screen'] : null,
			)
		);
	}

	/**
	 * Adds avatars to comment author names.
	 *
	 * @since 3.1.0
	 *
	 * @param string $name       Comment author name.
	 * @param int    $comment_ID Comment ID.
	 * @return string Avatar with the user name.
	 */
	public function floated_admin_avatar( $name, $comment_ID ) {
		$comment = get_comment( $comment_ID );
		$avatar  = get_avatar( $comment, 32, 'mystery' );
		return "$avatar $name";
	}

	/**
	 * @return bool
	 */
	public function ajax_user_can() {
		return current_user_can( 'edit_posts' );
	}

	/**
	 * @global string $mode           List table view mode.
	 * @global int    $post_id
	 * @global string $comment_status
	 * @global string $comment_type
	 * @global string $search
	 */
	public function prepare_items() {
		global $mode, $post_id, $comment_status, $comment_type, $search;

		if ( ! empty( $_REQUEST['mode'] ) ) {
			$mode = 'excerpt' === $_REQUEST['mode'] ? 'excerpt' : 'list';
			set_user_setting( 'posts_list_mode', $mode );
		} else {
			$mode = get_user_setting( 'posts_list_mode', 'list' );
		}

		$comment_status = isset( $_REQUEST['comment_status'] ) ? $_REQUEST['comment_status'] : 'all';
		if ( ! in_array( $comment_status, array( 'all', 'mine', 'moderated', 'approved', 'spam', 'trash' ), true ) ) {
			$comment_status = 'all';
		}

		$comment_type = ! empty( $_REQUEST['comment_type'] ) ? $_REQUEST['comment_type'] : '';

		$search = ( isset( $_REQUEST['s'] ) ) ? $_REQUEST['s'] : '';

		$post_type = ( isset( $_REQUEST['post_type'] ) ) ? sanitize_key( $_REQUEST['post_type'] ) : '';

		$user_id = ( isset( $_REQUEST['user_id'] ) ) ? $_REQUEST['user_id'] : '';

		$orderby = ( isset( $_REQUEST['orderby'] ) ) ? $_REQUEST['orderby'] : '';
		$order   = ( isset( $_REQUEST['order'] ) ) ? $_REQUEST['order'] : '';

		$comments_per_page = $this->get_per_page( $comment_status );

		$doing_ajax = wp_doing_ajax();

		if ( isset( $_REQUEST['number'] ) ) {
			$number = (int) $_REQUEST['number'];
		} else {
			$number = $comments_per_page + min( 8, $comments_per_page ); // Grab a few extra.
		}

		$page = $this->get_pagenum();

		if ( isset( $_REQUEST['start'] ) ) {
			$start = $_REQUEST['start'];
		} else {
			$start = ( $page - 1 ) * $comments_per_page;
		}

		if ( $doing_ajax && isset( $_REQUEST['offset'] ) ) {
			$start += $_REQUEST['offset'];
		}

		$status_map = array(
			'mine'      => '',
			'moderated' => 'hold',
			'approved'  => 'approve',
			'all'       => '',
		);

		$args = array(
			'status'    => isset( $status_map[ $comment_status ] ) ? $status_map[ $comment_status ] : $comment_status,
			'search'    => $search,
			'user_id'   => $user_id,
			'offset'    => $start,
			'number'    => $number,
			'post_id'   => $post_id,
			'type'      => $comment_type,
			'orderby'   => $orderby,
			'order'     => $order,
			'post_type' => $post_type,
		);

		/**
		 * Filters the arguments for the comment query in the comments list table.
		 *
		 * @since 5.1.0
		 *
		 * @param array $args An array of get_comments() arguments.
		 */
		$args = apply_filters( 'comments_list_table_query_args', $args );

		$_comments = get_comments( $args );
		if ( is_array( $_comments ) ) {
			update_comment_cache( $_comments );

			$this->items       = array_slice( $_comments, 0, $comments_per_page );
			$this->extra_items = array_slice( $_comments, $comments_per_page );

			$_comment_post_ids = array_unique( wp_list_pluck( $_comments, 'comment_post_ID' ) );

			$this->pending_count = get_pending_comments_num( $_comment_post_ids );
		}

		$total_comments = get_comments(
			array_merge(
				$args,
				array(
					'count'  => true,
					'offset' => 0,
					'number' => 0,
				)
			)
		);

		$this->set_pagination_args(
			array(
				'total_items' => $total_comments,
				'per_page'    => $comments_per_page,
			)
		);
	}

	/**
	 * @param string $comment_status
	 * @return int
	 */
	public function get_per_page( $comment_status = 'all' ) {
		$comments_per_page = $this->get_items_per_page( 'edit_comments_per_page' );
		/**
		 * Filters the number of comments listed per page in the comments list table.
		 *
		 * @since 2.6.0
		 *
		 * @param int    $comments_per_page The number of comments to list per page.
		 * @param string $comment_status    The comment status name. Default 'All'.
		 */
		return apply_filters( 'comments_per_page', $comments_per_page, $comment_status );
	}

	/**
	 * @global string $comment_status
	 */
	public function no_items() {
		global $comment_status;

		if ( 'moderated' === $comment_status ) {
			_e( 'No comments awaiting moderation.' );
		} elseif ( 'trash' === $comment_status ) {
			_e( 'No comments found in Trash.' );
		} else {
			_e( 'No comments found.' );
		}
	}

	/**
	 * @global int $post_id
	 * @global string $comment_status
	 * @global string $comment_type
	 */
	protected function get_views() {
		global $post_id, $comment_status, $comment_type;

		$status_links = array();
		$num_comments = ( $post_id ) ? wp_count_comments( $post_id ) : wp_count_comments();

		$stati = array(
			/* translators: %s: Number of comments. */
			'all'       => _nx_noop(
				'All (%s)',
				'All (%s)',
				'comments'
			), // Singular not used.

			/* translators: %s: Number of comments. */
			'mine'      => _nx_noop(
				'Mine (%s)',
				'Mine (%s)',
				'comments'
			),

			/* translators: %s: Number of comments. */
			'moderated' => _nx_noop(
				'Pending (%s)',
				'Pending (%s)',
				'comments'
			),

			/* translators: %s: Number of comments. */
			'approved'  => _nx_noop(
				'Approved (%s)',
				'Approved (%s)',
				'comments'
			),

			/* translators: %s: Number of comments. */
			'spam'      => _nx_noop(
				'Spam (%s)',
				'Spam (%s)',
				'comments'
			),

			/* translators: %s: Number of comments. */
			'trash'     => _nx_noop(
				'Trash (%s)',
				'Trash (%s)',
				'comments'
			),
		);

		if ( ! EMPTY_TRASH_DAYS ) {
			unset( $stati['trash'] );
		}

		$link = admin_url( 'edit-comments.php' );
		if ( ! empty( $comment_type ) && 'all' !== $comment_type ) {
			$link = add_query_arg( 'comment_type', $comment_type, $link );
		}

		foreach ( $stati as $status => $label ) {
			$current_link_attributes = '';

			if ( $status === $comment_status ) {
				$current_link_attributes = ' class="current" aria-current="page"';
			}

			if ( 'mine' === $status ) {
				$current_user_id    = get_current_user_id();
				$num_comments->mine = get_comments(
					array(
						'post_id' => $post_id ? $post_id : 0,
						'user_id' => $current_user_id,
						'count'   => true,
					)
				);
				$link               = add_query_arg( 'user_id', $current_user_id, $link );
			} else {
				$link = remove_query_arg( 'user_id', $link );
			}

			if ( ! isset( $num_comments->$status ) ) {
				$num_comments->$status = 10;
			}
			$link = add_query_arg( 'comment_status', $status, $link );
			if ( $post_id ) {
				$link = add_query_arg( 'p', absint( $post_id ), $link );
			}
			/*
			// I toyed with this, but decided against it. Leaving it in here in case anyone thinks it is a good idea. ~ Mark
			if ( !empty( $_REQUEST['s'] ) )
				$link = add_query_arg( 's', esc_attr( wp_unslash( $_REQUEST['s'] ) ), $link );
			*/
			$status_links[ $status ] = "" . sprintf(
				translate_nooped_plural( $label, $num_comments->$status ),
				sprintf(
					'%s',
					( 'moderated' === $status ) ? 'pending' : $status,
					number_format_i18n( $num_comments->$status )
				)
			) . '';
		}

		/**
		 * Filters the comment status links.
		 *
		 * @since 2.5.0
		 * @since 5.1.0 The 'Mine' link was added.
		 *
		 * @param string[] $status_links An associative array of fully-formed comment status links. Includes 'All', 'Mine',
		 *                              'Pending', 'Approved', 'Spam', and 'Trash'.
		 */
		return apply_filters( 'comment_status_links', $status_links );
	}

	/**
	 * @global string $comment_status
	 *
	 * @return array
	 */
	protected function get_bulk_actions() {
		global $comment_status;

		$actions = array();
		if ( in_array( $comment_status, array( 'all', 'approved' ), true ) ) {
			$actions['unapprove'] = __( 'Unapprove' );
		}
		if ( in_array( $comment_status, array( 'all', 'moderated' ), true ) ) {
			$actions['approve'] = __( 'Approve' );
		}
		if ( in_array( $comment_status, array( 'all', 'moderated', 'approved', 'trash' ), true ) ) {
			$actions['spam'] = _x( 'Mark as spam', 'comment' );
		}

		if ( 'trash' === $comment_status ) {
			$actions['untrash'] = __( 'Restore' );
		} elseif ( 'spam' === $comment_status ) {
			$actions['unspam'] = _x( 'Not spam', 'comment' );
		}

		if ( in_array( $comment_status, array( 'trash', 'spam' ), true ) || ! EMPTY_TRASH_DAYS ) {
			$actions['delete'] = __( 'Delete permanently' );
		} else {
			$actions['trash'] = __( 'Move to Trash' );
		}

		return $actions;
	}

	/**
	 * @global string $comment_status
	 * @global string $comment_type
	 *
	 * @param string $which
	 */
	protected function extra_tablenav( $which ) {
		global $comment_status, $comment_type;
		static $has_items;

		if ( ! isset( $has_items ) ) {
			$has_items = $this->has_items();
		}
		echo '
';
		if ( 'top' === $which ) {
			ob_start();

			$this->comment_status_dropdown( $comment_type );
			/**
			 * Fires just before the Filter submit button for comment types.
			 *
			 * @since 3.5.0
			 */
			do_action( 'restrict_manage_comments' );

			$output = ob_get_clean();

			if ( ! empty( $output ) && $this->has_items() ) {
				echo $output;
				submit_button( esc_html__( 'Filter' ), '', 'filter_action', false, array( 'id' => 'post-query-submit' ) );
			}
		}

		if ( ( 'spam' === $comment_status || 'trash' === $comment_status ) && current_user_can( 'moderate_comments' ) && $has_items ) {
			wp_nonce_field( 'bulk-destroy', '_destroy_nonce' );
			$title = ( 'spam' === $comment_status ) ? esc_attr__( 'Empty Spam' ) : esc_attr__( 'Empty Trash' );
			submit_button( $title, 'apply', 'delete_all', false );
		}
		/**
		 * Fires after the Filter submit button for comment types.
		 *
		 * @since 2.5.0
		 *
		 * @param string $comment_status The comment status name. Default 'All'.
		 */
		do_action( 'manage_comments_nav', $comment_status );
		echo '
';
	}

	/**
	 * @return string|false
	 */
	public function current_action() {
		if ( isset( $_REQUEST['delete_all'] ) || isset( $_REQUEST['delete_all2'] ) ) {
			return 'delete_all';
		}

		return parent::current_action();
	}

	/**
	 * @global int $post_id
	 *
	 * @return array
	 */
	public function get_columns() {
		global $post_id;

		$columns = array();

		if ( $this->checkbox ) {
			$columns['cb'] = '<input type="checkbox" />';
		}

		$columns['author']  = __( 'Author' );
		$columns['comment'] = _x( 'Comment', 'column name' );

		if ( ! $post_id ) {
			/* translators: Column name or table row header. */
			$columns['response'] = __( 'In response to' );
		}

		$columns['date'] = _x( 'Submitted on', 'column name' );

		return $columns;
	}

	/**
	 * Displays a comment status drop-down for filtering on the Comments list table.
	 *
	 * @since 5.5.0
	 *
	 * @param string $comment_type The current comment type slug.
	 */
	protected function comment_status_dropdown( $comment_type ) {
		/**
		 * Filters the comment types dropdown menu.
		 *
		 * @since 2.7.0
		 *
		 * @param array $comment_types An array of comment types. Accepts 'Comments', 'Pings'.
		 */
		$comment_types = apply_filters(
			'admin_comment_types_dropdown',
			array(
				'comment' => esc_html__( 'Comments' ),
				'pings'   => esc_html__( 'Pings' ),
			)
		);

		if ( $comment_types && is_array( $comment_types ) ) {
			printf( '%s', esc_html__( 'Filter by comment type' ) );

			echo '<select id="filter-by-comment-type" name="comment_type">';

			printf( "\t<option value=''>%s</option>", esc_html__( 'All comment types' ) );

			foreach ( $comment_types as $type => $label ) {
				if ( get_comments(
					array(
						'number' => 1,
						'type'   => $type,
					)
				) ) {
					printf(
						"\t<option value='%s'%s>%s</option>\n",
						esc_attr( $type ),
						selected( $comment_type, $type, false ),
						esc_html( $label )
					);
				}
			}
				echo '</select>';
		}
	}

	/**
	 * @return array
	 */
	protected function get_sortable_columns() {
		return array(
			'author'   => 'comment_author',
			'response' => 'comment_post_ID',
			'date'     => 'comment_date',
		);
	}

	/**
	 * Get the name of the default primary column.
	 *
	 * @since 4.3.0
	 *
	 * @return string Name of the default primary column, in this case, 'comment'.
	 */
	protected function get_default_primary_column_name() {
		return 'comment';
	}

	/**
	 * Displays the comments table.
	 *
	 * Overrides the parent display() method to render extra comments.
	 *
	 * @since 3.1.0
	 */
	public function display() {
		wp_nonce_field( 'fetch-list-' . get_class( $this ), '_ajax_fetch_list_nonce' );
		static $has_items;

		if ( ! isset( $has_items ) ) {
			$has_items = $this->has_items();
			if ( $has_items ) {
				$this->display_tablenav( 'top' );
			}
		}

		$this->screen->render_screen_reader_content( 'heading_list' );

		?>
<table class="wp-list-table <?php echo implode( ' ', $this->get_table_classes() ); ?>">

		<?php $this->print_column_headers(); ?>

		<?php $this->display_rows_or_placeholder(); ?>

		<?php
			/*
			 * Back up the items to restore after printing the extra items markup.
			 * The extra items may be empty, which will prevent the table nav from displaying later.
			 */
			$items       = $this->items;
			$this->items = $this->extra_items;
			$this->display_rows_or_placeholder();
			$this->items = $items;
		?>

		<?php $this->print_column_headers( false ); ?>

		<?php

		$this->display_tablenav( 'bottom' );
	}

	/**
	 * @global WP_Post    $post    Global post object.
	 * @global WP_Comment $comment Global comment object.
	 *
	 * @param WP_Comment $item
	 */
	public function single_row( $item ) {
		global $post, $comment;

		$comment = $item;

		$the_comment_class = wp_get_comment_status( $comment );
		if ( ! $the_comment_class ) {
			$the_comment_class = '';
		}
		$the_comment_class = join( ' ', get_comment_class( $the_comment_class, $comment, $comment->comment_post_ID ) );

		if ( $comment->comment_post_ID > 0 ) {
			$post = get_post( $comment->comment_post_ID );
		}
		$this->user_can = current_user_can( 'edit_comment', $comment->comment_ID );

		echo "
comment_ID' class='$the_comment_class'>";
		$this->single_row_columns( $comment );
		echo "
\n";

		unset( $GLOBALS['post'], $GLOBALS['comment'] );
	}

	/**
	 * Generate and display row actions links.
	 *
	 * @since 4.3.0
	 *
	 * @global string $comment_status Status for the current listed comments.
	 *
	 * @param WP_Comment $comment     The comment object.
	 * @param string     $column_name Current column name.
	 * @param string     $primary     Primary column name.
	 * @return string Row actions output for comments. An empty string
	 *                if the current column is not the primary column,
	 *                or if the current user cannot edit the comment.
	 */
	protected function handle_row_actions( $comment, $column_name, $primary ) {
		global $comment_status;

		if ( $primary !== $column_name ) {
			return '';
		}

		if ( ! $this->user_can ) {
			return '';
		}

		$the_comment_status = wp_get_comment_status( $comment );

		$out = '';

		$del_nonce     = esc_html( '_wpnonce=' . wp_create_nonce( "delete-comment_$comment->comment_ID" ) );
		$approve_nonce = esc_html( '_wpnonce=' . wp_create_nonce( "approve-comment_$comment->comment_ID" ) );

		$url = "comment.php?c=$comment->comment_ID";

		$approve_url   = esc_url( $url . "&action=approvecomment&$approve_nonce" );
		$unapprove_url = esc_url( $url . "&action=unapprovecomment&$approve_nonce" );
		$spam_url      = esc_url( $url . "&action=spamcomment&$del_nonce" );
		$unspam_url    = esc_url( $url . "&action=unspamcomment&$del_nonce" );
		$trash_url     = esc_url( $url . "&action=trashcomment&$del_nonce" );
		$untrash_url   = esc_url( $url . "&action=untrashcomment&$del_nonce" );
		$delete_url    = esc_url( $url . "&action=deletecomment&$del_nonce" );

		// Preorder it: Approve | Reply | Quick Edit | Edit | Spam | Trash.
		$actions = array(
			'approve'   => '',
			'unapprove' => '',
			'reply'     => '',
			'quickedit' => '',
			'edit'      => '',
			'spam'      => '',
			'unspam'    => '',
			'trash'     => '',
			'untrash'   => '',
			'delete'    => '',
		);

		// Not looking at all comments.
		if ( $comment_status && 'all' !== $comment_status ) {
			if ( 'approved' === $the_comment_status ) {
				$actions['unapprove'] = sprintf(
					'%s',
					$unapprove_url,
					"delete:the-comment-list:comment-{$comment->comment_ID}:e7e7d3:action=dim-comment&new=unapproved",
					esc_attr__( 'Unapprove this comment' ),
					__( 'Unapprove' )
				);
			} elseif ( 'unapproved' === $the_comment_status ) {
				$actions['approve'] = sprintf(
					'%s',
					$approve_url,
					"delete:the-comment-list:comment-{$comment->comment_ID}:e7e7d3:action=dim-comment&new=approved",
					esc_attr__( 'Approve this comment' ),
					__( 'Approve' )
				);
			}
		} else {
			$actions['approve'] = sprintf(
				'%s',
				$approve_url,
				"dim:the-comment-list:comment-{$comment->comment_ID}:unapproved:e7e7d3:e7e7d3:new=approved",
				esc_attr__( 'Approve this comment' ),
				__( 'Approve' )
			);

			$actions['unapprove'] = sprintf(
				'%s',
				$unapprove_url,
				"dim:the-comment-list:comment-{$comment->comment_ID}:unapproved:e7e7d3:e7e7d3:new=unapproved",
				esc_attr__( 'Unapprove this comment' ),
				__( 'Unapprove' )
			);
		}

		if ( 'spam' !== $the_comment_status ) {
			$actions['spam'] = sprintf(
				'%s',
				$spam_url,
				"delete:the-comment-list:comment-{$comment->comment_ID}::spam=1",
				esc_attr__( 'Mark this comment as spam' ),
				/* translators: "Mark as spam" link. */
				_x( 'Spam', 'verb' )
			);
		} elseif ( 'spam' === $the_comment_status ) {
			$actions['unspam'] = sprintf(
				'%s',
				$unspam_url,
				"delete:the-comment-list:comment-{$comment->comment_ID}:66cc66:unspam=1",
				esc_attr__( 'Restore this comment from the spam' ),
				_x( 'Not Spam', 'comment' )
			);
		}

		if ( 'trash' === $the_comment_status ) {
			$actions['untrash'] = sprintf(
				'%s',
				$untrash_url,
				"delete:the-comment-list:comment-{$comment->comment_ID}:66cc66:untrash=1",
				esc_attr__( 'Restore this comment from the Trash' ),
				__( 'Restore' )
			);
		}

		if ( 'spam' === $the_comment_status || 'trash' === $the_comment_status || ! EMPTY_TRASH_DAYS ) {
			$actions['delete'] = sprintf(
				'%s',
				$delete_url,
				"delete:the-comment-list:comment-{$comment->comment_ID}::delete=1",
				esc_attr__( 'Delete this comment permanently' ),
				__( 'Delete Permanently' )
			);
		} else {
			$actions['trash'] = sprintf(
				'%s',
				$trash_url,
				"delete:the-comment-list:comment-{$comment->comment_ID}::trash=1",
				esc_attr__( 'Move this comment to the Trash' ),
				_x( 'Trash', 'verb' )
			);
		}

		if ( 'spam' !== $the_comment_status && 'trash' !== $the_comment_status ) {
			$actions['edit'] = sprintf(
				'%s',
				"comment.php?action=editcomment&c={$comment->comment_ID}",
				esc_attr__( 'Edit this comment' ),
				__( 'Edit' )
			);

			$format = '<button type="button" data-comment-id="%d" data-post-id="%d" data-action="%s" class="%s button-link" aria-expanded="false" aria-label="%s">%s</button>';

			$actions['quickedit'] = sprintf(
				$format,
				$comment->comment_ID,
				$comment->comment_post_ID,
				'edit',
				'vim-q comment-inline',
				esc_attr__( 'Quick edit this comment inline' ),
				__( 'Quick Edit' )
			);

			$actions['reply'] = sprintf(
				$format,
				$comment->comment_ID,
				$comment->comment_post_ID,
				'replyto',
				'vim-r comment-inline',
				esc_attr__( 'Reply to this comment' ),
				__( 'Reply' )
			);
		}

		/** This filter is documented in wp-admin/includes/dashboard.php */
		$actions = apply_filters( 'comment_row_actions', array_filter( $actions ), $comment );

		$always_visible = false;

		$mode = get_user_setting( 'posts_list_mode', 'list' );

		if ( 'excerpt' === $mode ) {
			$always_visible = true;
		}

		$out .= '
';

		$i = 0;

		foreach ( $actions as $action => $link ) {
			++$i;

			if ( ( ( 'approve' === $action || 'unapprove' === $action ) && 2 === $i )
				|| 1 === $i
			) {
				$sep = '';
			} else {
				$sep = ' | ';
			}

			// Reply and quickedit need a hide-if-no-js span when not added with Ajax.
			if ( ( 'reply' === $action || 'quickedit' === $action ) && ! wp_doing_ajax() ) {
				$action .= ' hide-if-no-js';
			} elseif ( ( 'untrash' === $action && 'trash' === $the_comment_status )
				|| ( 'unspam' === $action && 'spam' === $the_comment_status )
			) {
				if ( '1' == get_comment_meta( $comment->comment_ID, '_wp_trash_meta_status', true ) ) {
					$action .= ' approve';
				} else {
					$action .= ' unapprove';
				}
			}

			$out .= "$sep$link";
		}

		$out .= '
';

		$out .= '<button type="button" class="toggle-row">' . __( 'Show more details' ) . '</button>';

		return $out;
	}

	/**
	 * @param WP_Comment $comment The comment object.
	 */
	public function column_cb( $comment ) {
		if ( $this->user_can ) {
			?>
		<label class="screen-reader-text" for="cb-select-<?php echo $comment->comment_ID; ?>"><?php _e( 'Select comment' ); ?>
		<input id="cb-select-<?php echo $comment->comment_ID; ?>" type="checkbox" name="delete_comments[]" value="<?php echo $comment->comment_ID; ?>" />
			<?php
		}
	}

	/**
	 * @param WP_Comment $comment The comment object.
	 */
	public function column_comment( $comment ) {
		echo '
';
			$this->column_author( $comment );
		echo '
';

		if ( $comment->comment_parent ) {
			$parent = get_comment( $comment->comment_parent );
			if ( $parent ) {
				$parent_link = esc_url( get_comment_link( $parent ) );
				$name        = get_comment_author( $parent );
				printf(
					/* translators: %s: Comment link. */
					__( 'In reply to %s.' ),
					'' . $name . ''
				);
			}
		}

		comment_text( $comment );

		if ( $this->user_can ) {
			/** This filter is documented in wp-admin/includes/comment.php */
			$comment_content = apply_filters( 'comment_edit_pre', $comment->comment_content );
			?>
		<div id="inline-<?php echo $comment->comment_ID; ?>" class="hidden">
			<textarea class="comment" rows="1" cols="1"><?php echo esc_textarea( $comment_content ); ?></textarea>

<?php echo esc_attr( $comment->comment_author_email ); ?>

<?php echo esc_attr( $comment->comment_author ); ?>

<?php echo esc_attr( $comment->comment_author_url ); ?>

<?php echo $comment->comment_approved; ?>

			<?php
		}
	}

	/**
	 * @global string $comment_status
	 *
	 * @param WP_Comment $comment The comment object.
	 */
	public function column_author( $comment ) {
		global $comment_status;

		$author_url = get_comment_author_url( $comment );

		$author_url_display = untrailingslashit( preg_replace( '|^http(s)?://(www\.)?|i', '', $author_url ) );
		if ( strlen( $author_url_display ) > 50 ) {
			$author_url_display = wp_html_excerpt( $author_url_display, 49, '…' );
		}

		echo '';
		comment_author( $comment );
		echo '
';
		if ( ! empty( $author_url_display ) ) {
			printf( '%s
', esc_url( $author_url ), esc_html( $author_url_display ) );
		}

		if ( $this->user_can ) {
			if ( ! empty( $comment->comment_author_email ) ) {
				/** This filter is documented in wp-includes/comment-template.php */
				$email = apply_filters( 'comment_email', $comment->comment_author_email, $comment );

				if ( ! empty( $email ) && '@' !== $email ) {
					printf( '%2$s
', esc_url( 'mailto:' . $email ), esc_html( $email ) );
				}
			}

			$author_ip = get_comment_author_IP( $comment );
			if ( $author_ip ) {
				$author_ip_url = add_query_arg(
					array(
						's'    => $author_ip,
						'mode' => 'detail',
					),
					admin_url( 'edit-comments.php' )
				);
				if ( 'spam' === $comment_status ) {
					$author_ip_url = add_query_arg( 'comment_status', 'spam', $author_ip_url );
				}
				printf( '%2$s', esc_url( $author_ip_url ), esc_html( $author_ip ) );
			}
		}
	}

	/**
	 * @param WP_Comment $comment The comment object.
	 */
	public function column_date( $comment ) {
		$submitted = sprintf(
			/* translators: 1: Comment date, 2: Comment time. */
			__( '%1$s at %2$s' ),
			/* translators: Comment date format. See https://www.php.net/date */
			get_comment_date( __( 'Y/m/d' ), $comment ),
			/* translators: Comment time format. See https://www.php.net/date */
			get_comment_date( __( 'g:i a' ), $comment )
		);

		echo '
';
		if ( 'approved' === wp_get_comment_status( $comment ) && ! empty( $comment->comment_post_ID ) ) {
			printf(
				'%s',
				esc_url( get_comment_link( $comment ) ),
				$submitted
			);
		} else {
			echo $submitted;
		}
		echo '
';
	}

	/**
	 * @param WP_Comment $comment The comment object.
	 */
	public function column_response( $comment ) {
		$post = get_post();

		if ( ! $post ) {
			return;
		}

		if ( isset( $this->pending_count[ $post->ID ] ) ) {
			$pending_comments = $this->pending_count[ $post->ID ];
		} else {
			$_pending_count_temp              = get_pending_comments_num( array( $post->ID ) );
			$pending_comments                 = $_pending_count_temp[ $post->ID ];
			$this->pending_count[ $post->ID ] = $pending_comments;
		}

		if ( current_user_can( 'edit_post', $post->ID ) ) {
			$post_link  = "ID ) . "' class='comments-edit-item-link'>";
			$post_link .= esc_html( get_the_title( $post->ID ) ) . '';
		} else {
			$post_link = esc_html( get_the_title( $post->ID ) );
		}

		echo '
';
		if ( 'attachment' === $post->post_type ) {
			$thumb = wp_get_attachment_image( $post->ID, array( 80, 60 ), true );
			if ( $thumb ) {
				echo $thumb;
			}
		}
		echo $post_link;
		$post_type_object = get_post_type_object( $post->post_type );
		echo "ID ) . "' class='comments-view-item-link'>" . $post_type_object->labels->view_item . '';
		echo 'ID, '">';
		$this->comments_bubble( $post->ID, $pending_comments );
		echo ' ';
		echo '
';
	}

	/**
	 * @param WP_Comment $comment     The comment object.
	 * @param string     $column_name The custom column's name.
	 */
	public function column_default( $comment, $column_name ) {
		/**
		 * Fires when the default column output is displayed for a single row.
		 *
		 * @since 2.8.0
		 *
		 * @param string $column_name The custom column's name.
		 * @param int    $comment_ID  The custom column's unique ID number.
		 */
		do_action( 'manage_comments_custom_column', $column_name, $comment->comment_ID );
	}
}


<?php
/**
 * Administration: Community Events class.
 *
 * @package WordPress
 * @subpackage Administration
 * @since 4.8.0
 */

/**
 * Class WP_Community_Events.
 *
 * A client for api.wordpress.org/events.
 *
 * @since 4.8.0
 */
class WP_Community_Events {
	/**
	 * ID for a WordPress user account.
	 *
	 * @since 4.8.0
	 *
	 * @var int
	 */
	protected $user_id = 0;

	/**
	 * Stores location data for the user.
	 *
	 * @since 4.8.0
	 *
	 * @var bool|array
	 */
	protected $user_location = false;

	/**
	 * Constructor for WP_Community_Events.
	 *
	 * @since 4.8.0
	 *
	 * @param int        $user_id       WP user ID.
	 * @param bool|array $user_location Stored location data for the user.
	 *                                  false to pass no location;
	 *                                  array to pass a location {
	 *     @type string $description The name of the location
	 *     @type string $latitude    The latitude in decimal degrees notation, without the degree
	 *                               symbol. e.g.: 47.615200.
	 *     @type string $longitude   The longitude in decimal degrees notation, without the degree
	 *                               symbol. e.g.: -122.341100.
	 *     @type string $country     The ISO 3166-1 alpha-2 country code. e.g.: BR
	 * }
	 */
	public function __construct( $user_id, $user_location = false ) {
		$this->user_id       = absint( $user_id );
		$this->user_location = $user_location;
	}

	/**
	 * Gets data about events near a particular location.
	 *
	 * Cached events will be immediately returned if the `user_location` property
	 * is set for the current user, and cached events exist for that location.
	 *
	 * Otherwise, this method sends a request to the w.org Events API with location
	 * data. The API will send back a recognized location based on the data, along
	 * with nearby events.
	 *
	 * The browser's request for events is proxied with this method, rather
	 * than having the browser make the request directly to api.wordpress.org,
	 * because it allows results to be cached server-side and shared with other
	 * users and sites in the network. This makes the process more efficient,
	 * since increasing the number of visits that get cached data means users
	 * don't have to wait as often; if the user's browser made the request
	 * directly, it would also need to make a second request to WP in order to
	 * pass the data for caching. Having WP make the request also introduces
	 * the opportunity to anonymize the IP before sending it to w.org, which
	 * mitigates possible privacy concerns.
	 *
	 * @since 4.8.0
	 * @since 5.5.2 Response no longer contains formatted date field. They're added
	 *              in `wp.communityEvents.populateDynamicEventFields()` now.
	 *
	 * @param string $location_search Optional. City name to help determine the location.
	 *                                e.g., "Seattle". Default empty string.
	 * @param string $timezone        Optional. Timezone to help determine the location.
	 *                                Default empty string.
	 * @return array|WP_Error A WP_Error on failure; an array with location and events on
	 *                        success.
	 */
	public function get_events( $location_search = '', $timezone = '' ) {
		$cached_events = $this->get_cached_events();

		if ( ! $location_search && $cached_events ) {
			return $cached_events;
		}

		// Include an unmodified $wp_version.
		require ABSPATH . WPINC . '/version.php';

		$api_url                    = 'http://api.wordpress.org/events/1.0/';
		$request_args               = $this->get_request_args( $location_search, $timezone );
		$request_args['user-agent'] = 'WordPress/' . $wp_version . '; ' . home_url( '/' );

		if ( wp_http_supports( array( 'ssl' ) ) ) {
			$api_url = set_url_scheme( $api_url, 'https' );
		}

		$response       = wp_remote_get( $api_url, $request_args );
		$response_code  = wp_remote_retrieve_response_code( $response );
		$response_body  = json_decode( wp_remote_retrieve_body( $response ), true );
		$response_error = null;

		if ( is_wp_error( $response ) ) {
			$response_error = $response;
		} elseif ( 200 !== $response_code ) {
			$response_error = new WP_Error(
				'api-error',
				/* translators: %d: Numeric HTTP status code, e.g. 400, 403, 500, 504, etc. */
				sprintf( __( 'Invalid API response code (%d).' ), $response_code )
			);
		} elseif ( ! isset( $response_body['location'], $response_body['events'] ) ) {
			$response_error = new WP_Error(
				'api-invalid-response',
				isset( $response_body['error'] ) ? $response_body['error'] : __( 'Unknown API error.' )
			);
		}

		if ( is_wp_error( $response_error ) ) {
			return $response_error;
		} else {
			$expiration = false;

			if ( isset( $response_body['ttl'] ) ) {
				$expiration = $response_body['ttl'];
				unset( $response_body['ttl'] );
			}

			/*
			 * The IP in the response is usually the same as the one that was sent
			 * in the request, but in some cases it is different. In those cases,
			 * it's important to reset it back to the IP from the request.
			 *
			 * For example, if the IP sent in the request is private (e.g., 192.168.1.100),
			 * then the API will ignore that and use the corresponding public IP instead,
			 * and the public IP will get returned. If the public IP were saved, though,
			 * then get_cached_events() would always return `false`, because the transient
			 * would be generated based on the public IP when saving the cache, but generated
			 * based on the private IP when retrieving the cache.
			 */
			if ( ! empty( $response_body['location']['ip'] ) ) {
				$response_body['location']['ip'] = $request_args['body']['ip'];
			}

			/*
			 * The API doesn't return a description for latitude/longitude requests,
			 * but the description is already saved in the user location, so that
			 * one can be used instead.
			 */
			if ( $this->coordinates_match( $request_args['body'], $response_body['location'] ) && empty( $response_body['location']['description'] ) ) {
				$response_body['location']['description'] = $this->user_location['description'];
			}

			/*
			 * Store the raw response, because events will expire before the cache does.
			 * The response will need to be processed every page load.
			 */
			$this->cache_events( $response_body, $expiration );

			$response_body['events'] = $this->trim_events( $response_body['events'] );

			return $response_body;
		}
	}

	/**
	 * Builds an array of args to use in an HTTP request to the w.org Events API.
	 *
	 * @since 4.8.0
	 *
	 * @param string $search   Optional. City search string. Default empty string.
	 * @param string $timezone Optional. Timezone string. Default empty string.
	 * @return array The request args.
	 */
	protected function get_request_args( $search = '', $timezone = '' ) {
		$args = array(
			'number' => 5, // Get more than three in case some get trimmed out.
			'ip'     => self::get_unsafe_client_ip(),
		);

		/*
		 * Include the minimal set of necessary arguments, in order to increase the
		 * chances of a cache-hit on the API side.
		 */
		if ( empty( $search ) && isset( $this->user_location['latitude'], $this->user_location['longitude'] ) ) {
			$args['latitude']  = $this->user_location['latitude'];
			$args['longitude'] = $this->user_location['longitude'];
		} else {
			$args['locale'] = get_user_locale( $this->user_id );

			if ( $timezone ) {
				$args['timezone'] = $timezone;
			}

			if ( $search ) {
				$args['location'] = $search;
			}
		}

		// Wrap the args in an array compatible with the second parameter of `wp_remote_get()`.
		return array(
			'body' => $args,
		);
	}

	/**
	 * Determines the user's actual IP address and attempts to partially
	 * anonymize an IP address by converting it to a network ID.
	 *
	 * Geolocating the network ID usually returns a similar location as the
	 * actual IP, but provides some privacy for the user.
	 *
	 * $_SERVER['REMOTE_ADDR'] cannot be used in all cases, such as when the user
	 * is making their request through a proxy, or when the web server is behind
	 * a proxy. In those cases, $_SERVER['REMOTE_ADDR'] is set to the proxy address rather
	 * than the user's actual address.
	 *
	 * Modified from https://stackoverflow.com/a/2031935/450127, MIT license.
	 * Modified from https://github.com/geertw/php-ip-anonymizer, MIT license.
	 *
	 * SECURITY WARNING: This function is _NOT_ intended to be used in
	 * circumstances where the authenticity of the IP address matters. This does
	 * _NOT_ guarantee that the returned address is valid or accurate, and it can
	 * be easily spoofed.
	 *
	 * @since 4.8.0
	 *
	 * @return string|false The anonymized address on success; the given address
	 *                      or false on failure.
	 */
	public static function get_unsafe_client_ip() {
		$client_ip = false;

		// In order of preference, with the best ones for this purpose first.
		$address_headers = array(
			'HTTP_CLIENT_IP',
			'HTTP_X_FORWARDED_FOR',
			'HTTP_X_FORWARDED',
			'HTTP_X_CLUSTER_CLIENT_IP',
			'HTTP_FORWARDED_FOR',
			'HTTP_FORWARDED',
			'REMOTE_ADDR',
		);

		foreach ( $address_headers as $header ) {
			if ( array_key_exists( $header, $_SERVER ) ) {
				/*
				 * HTTP_X_FORWARDED_FOR can contain a chain of comma-separated
				 * addresses. The first one is the original client. It can't be
				 * trusted for authenticity, but we don't need to for this purpose.
				 */
				$address_chain = explode( ',', $_SERVER[ $header ] );
				$client_ip     = trim( $address_chain[0] );

				break;
			}
		}

		if ( ! $client_ip ) {
			return false;
		}

		$anon_ip = wp_privacy_anonymize_ip( $client_ip, true );

		if ( '0.0.0.0' === $anon_ip || '::' === $anon_ip ) {
			return false;
		}

		return $anon_ip;
	}

	/**
	 * Test if two pairs of latitude/longitude coordinates match each other.
	 *
	 * @since 4.8.0
	 *
	 * @param array $a The first pair, with indexes 'latitude' and 'longitude'.
	 * @param array $b The second pair, with indexes 'latitude' and 'longitude'.
	 * @return bool True if they match, false if they don't.
	 */
	protected function coordinates_match( $a, $b ) {
		if ( ! isset( $a['latitude'], $a['longitude'], $b['latitude'], $b['longitude'] ) ) {
			return false;
		}

		return $a['latitude'] === $b['latitude'] && $a['longitude'] === $b['longitude'];
	}

	/**
	 * Generates a transient key based on user location.
	 *
	 * This could be reduced to a one-liner in the calling functions, but it's
	 * intentionally a separate function because it's called from multiple
	 * functions, and having it abstracted keeps the logic consistent and DRY,
	 * which is less prone to errors.
	 *
	 * @since 4.8.0
	 *
	 * @param array $location Should contain 'latitude' and 'longitude' indexes.
	 * @return bool|string false on failure, or a string on success.
	 */
	protected function get_events_transient_key( $location ) {
		$key = false;

		if ( isset( $location['ip'] ) ) {
			$key = 'community-events-' . md5( $location['ip'] );
		} elseif ( isset( $location['latitude'], $location['longitude'] ) ) {
			$key = 'community-events-' . md5( $location['latitude'] . $location['longitude'] );
		}

		return $key;
	}

	/**
	 * Caches an array of events data from the Events API.
	 *
	 * @since 4.8.0
	 *
	 * @param array    $events     Response body from the API request.
	 * @param int|bool $expiration Optional. Amount of time to cache the events. Defaults to false.
	 * @return bool true if events were cached; false if not.
	 */
	protected function cache_events( $events, $expiration = false ) {
		$set              = false;
		$transient_key    = $this->get_events_transient_key( $events['location'] );
		$cache_expiration = $expiration ? absint( $expiration ) : HOUR_IN_SECONDS * 12;

		if ( $transient_key ) {
			$set = set_site_transient( $transient_key, $events, $cache_expiration );
		}

		return $set;
	}

	/**
	 * Gets cached events.
	 *
	 * @since 4.8.0
	 * @since 5.5.2 Response no longer contains formatted date field. They're added
	 *              in `wp.communityEvents.populateDynamicEventFields()` now.
	 *
	 * @return array|false An array containing `location` and `events` items
	 *                     on success, false on failure.
	 */
	public function get_cached_events() {
		$cached_response = get_site_transient( $this->get_events_transient_key( $this->user_location ) );

		if ( isset( $cached_response['events'] ) ) {
			$cached_response['events'] = $this->trim_events( $cached_response['events'] );
		}

		return $cached_response;
	}

	/**
	 * Adds formatted date and time items for each event in an API response.
	 *
	 * This has to be called after the data is pulled from the cache, because
	 * the cached events are shared by all users. If it was called before storing
	 * the cache, then all users would see the events in the localized data/time
	 * of the user who triggered the cache refresh, rather than their own.
	 *
	 * @since 4.8.0
	 * @deprecated 5.6.0 No longer used in core.
	 *
	 * @param array $response_body The response which contains the events.
	 * @return array The response with dates and times formatted.
	 */
	protected function format_event_data_time( $response_body ) {
		_deprecated_function(
			__METHOD__,
			'5.5.2',
			'This is no longer used by core, and only kept for backward compatibility.'
		);

		if ( isset( $response_body['events'] ) ) {
			foreach ( $response_body['events'] as $key => $event ) {
				$timestamp = strtotime( $event['date'] );

				/*
				 * The `date_format` option is not used because it's important
				 * in this context to keep the day of the week in the formatted date,
				 * so that users can tell at a glance if the event is on a day they
				 * are available, without having to open the link.
				 */
				/* translators: Date format for upcoming events on the dashboard. Include the day of the week. See https://www.php.net/date */
				$formatted_date = date_i18n( __( 'l, M j, Y' ), $timestamp );
				$formatted_time = date_i18n( get_option( 'time_format' ), $timestamp );

				if ( isset( $event['end_date'] ) ) {
					$end_timestamp      = strtotime( $event['end_date'] );
					$formatted_end_date = date_i18n( __( 'l, M j, Y' ), $end_timestamp );

					if ( 'meetup' !== $event['type'] && $formatted_end_date !== $formatted_date ) {
						/* translators: Upcoming events month format. See https://www.php.net/date */
						$start_month = date_i18n( _x( 'F', 'upcoming events month format' ), $timestamp );
						$end_month   = date_i18n( _x( 'F', 'upcoming events month format' ), $end_timestamp );

						if ( $start_month === $end_month ) {
							$formatted_date = sprintf(
								/* translators: Date string for upcoming events. 1: Month, 2: Starting day, 3: Ending day, 4: Year. */
								__( '%1$s %2$dâ€“%3$d, %4$d' ),
								$start_month,
								/* translators: Upcoming events day format. See https://www.php.net/date */
								date_i18n( _x( 'j', 'upcoming events day format' ), $timestamp ),
								date_i18n( _x( 'j', 'upcoming events day format' ), $end_timestamp ),
								/* translators: Upcoming events year format. See https://www.php.net/date */
								date_i18n( _x( 'Y', 'upcoming events year format' ), $timestamp )
							);
						} else {
							$formatted_date = sprintf(
								/* translators: Date string for upcoming events. 1: Starting month, 2: Starting day, 3: Ending month, 4: Ending day, 5: Year. */
								__( '%1$s %2$d â€“ %3$s %4$d, %5$d' ),
								$start_month,
								date_i18n( _x( 'j', 'upcoming events day format' ), $timestamp ),
								$end_month,
								date_i18n( _x( 'j', 'upcoming events day format' ), $end_timestamp ),
								date_i18n( _x( 'Y', 'upcoming events year format' ), $timestamp )
							);
						}

						$formatted_date = wp_maybe_decline_date( $formatted_date, 'F j, Y' );
					}
				}

				$response_body['events'][ $key ]['formatted_date'] = $formatted_date;
				$response_body['events'][ $key ]['formatted_time'] = $formatted_time;
			}
		}

		return $response_body;
	}

	/**
	 * Prepares the event list for presentation.
	 *
	 * Discards expired events, and makes WordCamps "sticky." Attendees need more
	 * advanced notice about WordCamps than they do for meetups, so camps should
	 * appear in the list sooner. If a WordCamp is coming up, the API will "stick"
	 * it in the response, even if it wouldn't otherwise appear. When that happens,
	 * the event will be at the end of the list, and will need to be moved into a
	 * higher position, so that it doesn't get trimmed off.
	 *
	 * @since 4.8.0
	 * @since 4.9.7 Stick a WordCamp to the final list.
	 * @since 5.5.2 Accepts and returns only the events, rather than an entire HTTP response.
	 *
	 * @param array $events The events that will be prepared.
	 * @return array The response body with events trimmed.
	 */
	protected function trim_events( array $events ) {
		$future_events = array();

		foreach ( $events as $event ) {
			/*
			 * The API's `date` and `end_date` fields are in the _event's_ local timezone, but UTC is needed so
			 * it can be converted to the _user's_ local time.
			 */
			$end_time = (int) $event['end_unix_timestamp'];

			if ( time() < $end_time ) {
				array_push( $future_events, $event );
			}
		}

		$future_wordcamps = array_filter(
			$future_events,
			function( $wordcamp ) {
				return 'wordcamp' === $wordcamp['type'];
			}
		);

		$future_wordcamps    = array_values( $future_wordcamps ); // Remove gaps in indices.
		$trimmed_events      = array_slice( $future_events, 0, 3 );
		$trimmed_event_types = wp_list_pluck( $trimmed_events, 'type' );

		// Make sure the soonest upcoming WordCamp is pinned in the list.
		if ( $future_wordcamps && ! in_array( 'wordcamp', $trimmed_event_types, true ) ) {
			array_pop( $trimmed_events );
			array_push( $trimmed_events, $future_wordcamps[0] );
		}

		return $trimmed_events;
	}

	/**
	 * Logs responses to Events API requests.
	 *
	 * @since 4.8.0
	 * @deprecated 4.9.0 Use a plugin instead. See #41217 for an example.
	 *
	 * @param string $message A description of what occurred.
	 * @param array  $details Details that provide more context for the
	 *                        log entry.
	 */
	protected function maybe_log_events_response( $message, $details ) {
		_deprecated_function( __METHOD__, '4.9.0' );

		if ( ! WP_DEBUG_LOG ) {
			return;
		}

		error_log(
			sprintf(
				'%s: %s. Details: %s',
				__METHOD__,
				trim( $message, '.' ),
				wp_json_encode( $details )
			)
		);
	}
}