BBP_Akismet

Loads Akismet extension

Description

Source

File: bp-forums/extend/akismet.php

class BBP_Akismet {

	/**
	 * The main bbPress Akismet loader
	 *
	 * @since bbPress (r3277)
	 *
	 * @uses add_filter()
	 */
	public function __construct() {
		$this->setup_actions();
	}

	/**
	 * Setup the admin hooks
	 *
	 * @since bbPress (r3277)
	 * @access private
	 *
	 * @uses add_filter() To add various filters
	 * @uses add_action() To add various actions
	 */
	private function setup_actions() {

		// Prevent debug notices
		$checks = array();

		// bbPress functions to check for spam
		$checks['check']  = array(
			'bbp_new_topic_pre_insert'  => 1,  // New topic check
			'bbp_new_reply_pre_insert'  => 1,  // New reply check
			'bbp_edit_topic_pre_insert' => 1,  // Edit topic check
			'bbp_edit_reply_pre_insert' => 1   // Edit reply check
		);

		// bbPress functions for spam and ham submissions
		$checks['submit'] = array(
			'bbp_spammed_topic'   => 10, // Spammed topic
			'bbp_unspammed_topic' => 10, // Unspammed reply
			'bbp_spammed_reply'   => 10, // Spammed reply
			'bbp_unspammed_reply' => 10, // Unspammed reply
		);

		// Add the checks
		foreach ( $checks as $type => $functions )
			foreach ( $functions as $function => $priority )
				add_filter( $function, array( $this, $type . '_post'  ), $priority );

		// Update post meta
		add_action( 'wp_insert_post', array( $this, 'update_post_meta' ), 10, 2 );

		// Admin
		if ( is_admin() ) {
			add_action( 'add_meta_boxes', array( $this, 'add_metaboxes' ) );
		}
	}

	/**
	 * Converts topic/reply data into Akismet comment checking format
	 *
	 * @since bbPress (r3277)
	 *
	 * @param string $post_data
	 *
	 * @uses get_userdata() To get the user data
	 * @uses bbp_filter_anonymous_user_data() To get anonymous user data
	 * @uses bbp_get_topic_permalink() To get the permalink of the topic
	 * @uses bbp_get_reply_url() To get the permalink of the reply
	 * @uses bbp_current_author_ip() To get the IP address of the current user
	 * @uses BBP_Akismet::maybe_spam() To check if post is spam
	 * @uses BBP_Akismet::get_user_roles() To get the role(s) of the current user
	 * @uses do_action() To call the 'bbp_akismet_spam_caught' hook
	 * @uses add_filter() To call the 'bbp_new_reply_pre_set_terms' hook
	 *
	 * @return array Array of post data
	 */
	public function check_post( $post_data ) {

		// Define local variables
		$user_data = array();
		$post_permalink = '';

		// Post is not published
		if ( bbp_get_public_status_id() !== $post_data['post_status'] )
			return $post_data;

		// Cast the post_author to 0 if it's empty
		if ( empty( $post_data['post_author'] ) )
			$post_data['post_author'] = 0;

		/** Author ************************************************************/

		// Get user data
		$userdata       = get_userdata( $post_data['post_author'] );
		$anonymous_data = bbp_filter_anonymous_post_data();

		// Author is anonymous
		if ( !empty( $anonymous_data ) ) {
			$user_data['name']    = $anonymous_data['bbp_anonymous_name'];
			$user_data['email']   = $anonymous_data['bbp_anonymous_email'];
			$user_data['website'] = $anonymous_data['bbp_anonymous_website'];

		// Author is logged in
		} elseif ( !empty( $userdata ) ) {
			$user_data['name']    = $userdata->display_name;
			$user_data['email']   = $userdata->user_email;
			$user_data['website'] = $userdata->user_url;

		// Missing author data, so set some empty strings
		} else {
			$user_data['name']    = '';
			$user_data['email']   = '';
			$user_data['website'] = '';
		}

		/** Post **************************************************************/

		// Use post parent for permalink
		if ( !empty( $post_data['post_parent'] ) )
			$post_permalink = get_permalink( $post_data['post_parent'] );

		// Put post_data back into usable array
		$_post = array(
			'comment_author'       => $user_data['name'],
			'comment_author_email' => $user_data['email'],
			'comment_author_url'   => $user_data['website'],
			'comment_content'      => $post_data['post_content'],
			'comment_post_ID'      => $post_data['post_parent'],
			'comment_type'         => $post_data['post_type'],
			'permalink'            => $post_permalink,
			'referrer'             => $_SERVER['HTTP_REFERER'],
			'user_agent'           => $_SERVER['HTTP_USER_AGENT'],
			'user_ID'              => $post_data['post_author'],
			'user_ip'              => bbp_current_author_ip(),
			'user_role'            => $this->get_user_roles( $post_data['post_author'] ),
		);

		// Check the post_data
		$_post = $this->maybe_spam( $_post );

		// Get the result
		$post_data['bbp_akismet_result'] = $_post['bbp_akismet_result'];
		unset( $_post['bbp_akismet_result'] );

		// Store the data as submitted
		$post_data['bbp_post_as_submitted'] = $_post;

		// Allow post_data to be manipulated
		do_action_ref_array( 'bbp_akismet_check_post', $post_data );

		// Spam
		if ( 'true' === $post_data['bbp_akismet_result'] ) {

			// Let plugins do their thing
			do_action( 'bbp_akismet_spam_caught' );

			// This is spam
			$post_data['post_status'] = bbp_get_spam_status_id();

			// We don't want your spam tags here
			add_filter( 'bbp_new_reply_pre_set_terms', array( $this, 'filter_post_terms' ), 1, 3 );

			// @todo Spam counter?
		}

		// @todo Topic/reply moderation? No true/false response - 'pending' or 'draft'
		// @todo Auto-delete old spam?

		// Log the last post
		$this->last_post = $post_data;

		// Pass the data back to the filter
		return $post_data;
	}

	/**
	 * Submit a post for spamming or hamming
	 *
	 * @since bbPress (r3277)
	 *
	 * @param int $post_id
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 * @global string $akismet_api_host
	 * @global string $akismet_api_port
	 * @global object $current_user
	 * @global object $current_site
	 *
	 * @uses current_filter() To get the reply_id
	 * @uses get_post() To get the post object
	 * @uses get_the_author_meta() To get the author meta
	 * @uses get_post_meta() To get the post meta
	 * @uses bbp_get_user_profile_url() To get a user's profile url
	 * @uses get_permalink() To get the permalink of the post_parent
	 * @uses BBP_Akismet::get_user_roles() To get the role(s) of the post_author
	 * @uses bbp_current_author_ip() To get the IP address of the current user
	 * @uses BBP_Akismet::maybe_spam() To submit the post as ham or spam
	 * @uses update_post_meta() To update the post meta with some Akismet data
	 * @uses do_action() To call the 'bbp_akismet_submit_spam_post' and 'bbp_akismet_submit_ham_post' hooks
	 *
	 * @return array Array of existing topic terms
	 */
	public function submit_post( $post_id = 0 ) {
		global $current_user, $current_site;

		// Innocent until proven guilty
		$request_type   = 'ham';
		$current_filter = current_filter();

		// Check this filter and adjust the $request_type accordingly
		switch ( $current_filter ) {

			// Mysterious, and straight from the can
			case 'bbp_spammed_topic' :
			case 'bbp_spammed_reply' :
				$request_type = 'spam';
				break;

			// Honey-glazed, a straight off the bone
			case 'bbp_unspammed_topic' :
			case 'bbp_unspammed_reply' :
				$request_type = 'ham';
				break;

			// Possibly poison...
			default :
				return;
		}

		// Setup some variables
		$post_id = (int) $post_id;

		// Make sure we have a post
		$_post = get_post( $post_id );

		// Bail if get_post() fails
		if ( empty( $_post ) )
			return;

		// Bail if we're spamming, but the post_status isn't spam
		if ( ( 'spam' === $request_type ) && ( bbp_get_spam_status_id() !== $_post->post_status ) )
			return;

		// Set some default post_data
		$post_data = array(
			'comment_approved'     => $_post->post_status,
			'comment_author'       => $_post->post_author ? get_the_author_meta( 'display_name', $_post->post_author ) : get_post_meta( $post_id, '_bbp_anonymous_name',    true ),
			'comment_author_email' => $_post->post_author ? get_the_author_meta( 'email',        $_post->post_author ) : get_post_meta( $post_id, '_bbp_anonymous_email',   true ),
			'comment_author_url'   => $_post->post_author ? bbp_get_user_profile_url(            $_post->post_author ) : get_post_meta( $post_id, '_bbp_anonymous_website', true ),
			'comment_content'      => $_post->post_content,
			'comment_date'         => $_post->post_date,
			'comment_ID'           => $post_id,
			'comment_post_ID'      => $_post->post_parent,
			'comment_type'         => $_post->post_type,
			'permalink'            => get_permalink( $post_id ),
			'user_ID'              => $_post->post_author,
			'user_ip'              => get_post_meta( $post_id, '_bbp_author_ip', true ),
			'user_role'            => $this->get_user_roles( $_post->post_author ),
		);

		// Use the original version stored in post_meta if available
		$as_submitted = get_post_meta( $post_id, '_bbp_akismet_as_submitted', true );
		if ( $as_submitted && is_array( $as_submitted ) && isset( $as_submitted['comment_content'] ) )
			$post_data = array_merge( $post_data, $as_submitted );

		// Add the reporter IP address
		$post_data['reporter_ip']  = bbp_current_author_ip();

		// Add some reporter info
		if ( is_object( $current_user ) )
		    $post_data['reporter'] = $current_user->user_login;

		// Add the current site domain
		if ( is_object( $current_site ) )
			$post_data['site_domain'] = $current_site->domain;

		// Place your slide beneath the microscope
		$post_data = $this->maybe_spam( $post_data, 'submit', $request_type );

		// Manual user action
		if ( isset( $post_data['reporter'] ) ) {

			// What kind of action
			switch ( $request_type ) {

				// Spammy
				case 'spam' :
					$this->update_post_history( $post_id, sprintf( esc_html__( '%1$s reported this %2$s as spam', 'buddyboss' ),     $post_data['reporter'], $post_data['comment_type'] ), 'report-spam' );
					update_post_meta( $post_id, '_bbp_akismet_user_result', 'true'                 );
					update_post_meta( $post_id, '_bbp_akismet_user',        $post_data['reporter'] );
					break;

				// Hammy
				case 'ham'  :
					$this->update_post_history( $post_id, sprintf( esc_html__( '%1$s reported this %2$s as not spam', 'buddyboss' ), $post_data['reporter'], $post_data['comment_type'] ), 'report-ham'  );
					update_post_meta( $post_id, '_bbp_akismet_user_result', 'false'                 );
					update_post_meta( $post_id, '_bbp_akismet_user',         $post_data['reporter'] );

					// @todo Topic term revision history
					break;

				// Possible other actions
				default :
					break;
			}
		}

		do_action( 'bbp_akismet_submit_' . $request_type . '_post', $post_id, $post_data['bbp_akismet_result'] );
	}

	/**
	 * Ping Akismet service and check for spam/ham response
	 *
	 * @since bbPress (r3277)
	 *
	 * @param array $post_data
	 * @param string $check Accepts check|submit
	 * @param string $spam Accepts spam|ham
	 *
	 * @global string $akismet_api_host
	 * @global string $akismet_api_port
	 *
	 * @uses akismet_test_mode() To determine if Akismet is in test mode
	 * @uses akismet_http_post() To send data to the mothership for processing
	 *
	 * @return array Array of post data
	 */
	private function maybe_spam( $post_data, $check = 'check', $spam = 'spam' ) {
		global $akismet_api_host, $akismet_api_port;

		// Define variables
		$query_string = $path = $response = '';

		// Populate post data
		$post_data['blog']         = get_option( 'home' );
		$post_data['blog_charset'] = get_option( 'blog_charset' );
		$post_data['blog_lang']    = get_locale();
		$post_data['referrer']     = $_SERVER['HTTP_REFERER'];
		$post_data['user_agent']   = $_SERVER['HTTP_USER_AGENT'];

		// Akismet Test Mode
		if ( akismet_test_mode() )
			$post_data['is_test'] = 'true';

		// Loop through _POST args and rekey strings
		foreach ( $_POST as $key => $value )
			if ( is_string( $value ) )
				$post_data['POST_' . $key] = $value;

		// Keys to ignore
		$ignore = array( 'HTTP_COOKIE', 'HTTP_COOKIE2', 'PHP_AUTH_PW' );

		// Loop through _SERVER args and remove whitelisted keys
		foreach ( $_SERVER as $key => $value ) {

			// Key should not be ignored
			if ( !in_array( $key, $ignore ) && is_string( $value ) ) {
				$post_data[$key] = $value;

			// Key should be ignored
			} else {
				$post_data[$key] = '';
			}
		}

		// Ready...
		foreach ( $post_data as $key => $data )
			$query_string .= $key . '=' . urlencode( stripslashes( $data ) ) . '&';

		// Aim...
		if ( 'check' === $check ) {
			$path = '/1.1/comment-check';
		} elseif ( 'submit' === $check ) {
			$path = '/1.1/submit-' . $spam;
		}

		// Fire!
		$response = $this->http_post( $query_string, $akismet_api_host, $path, $akismet_api_port );

		// Check the high-speed cam
		if ( !empty( $response[1] ) ) {
			$post_data['bbp_akismet_result'] = $response[1];
		} else {
			$post_data['bbp_akismet_result'] = esc_html__( 'No response', 'buddyboss' );
		}

		// This is ham
		return $post_data;
	}

	/**
	 * Update post meta after a spam check
	 *
	 * @since bbPress (r3308)
	 *
	 * @param int $post_id
	 * @param object $_post
	 *
	 * @global object $this->last_post
	 *
	 * @uses get_post() To get the post object
	 * @uses get_userdata() To get the user data
	 * @uses bbp_filter_anonymous_user_data() To get anonymous user data
	 * @uses update_post_meta() To update post meta with Akismet data
	 * @uses BBP_Akismet::update_post_history() To update post Akismet history
	 */
	public function update_post_meta( $post_id = 0, $_post = false ) {

		// Define local variable(s)
		$as_submitted = false;

		// Setup some variables
		$post_id = (int) $post_id;

		// Ensure we have a post object
		if ( empty( $_post ) )
			$_post = get_post( $post_id );

		// Set up Akismet last post data
		if ( !empty( $this->last_post ) )
			$as_submitted = $this->last_post['bbp_post_as_submitted'];

		// wp_insert_post() might be called in other contexts. Ensure this is
		// the same topic/reply as was checked by BBP_Akismet::check_post()
		if ( is_object( $_post ) && !empty( $this->last_post ) && is_array( $as_submitted ) ) {

			// Get user data
			$userdata       = get_userdata( $_post->post_author );
			$anonymous_data = bbp_filter_anonymous_post_data();

			// More checks
			if (	intval( $as_submitted['comment_post_ID'] )    === intval( $_post->post_parent )
					&&      $as_submitted['comment_author']       === ( $anonymous_data ? $anonymous_data['bbp_anonymous_name']  : $userdata->display_name )
					&&      $as_submitted['comment_author_email'] === ( $anonymous_data ? $anonymous_data['bbp_anonymous_email'] : $userdata->user_email   )
				) {

				// Normal result: true
				if ( $this->last_post['bbp_akismet_result'] === 'true' ) {

					// Leave a trail so other's know what we did
					update_post_meta( $post_id, '_bbp_akismet_result', 'true' );
					$this->update_post_history( $post_id, esc_html__( 'Akismet caught this post as spam', 'buddyboss' ), 'check-spam' );

					// If post_status isn't the spam status, as expected, leave a note
					if ( bbp_get_spam_status_id() !== $_post->post_status ) {
						$this->update_post_history( $post_id, sprintf( esc_html__( 'Post status was changed to %s', 'buddyboss' ), $_post->post_status ), 'status-changed-' . $_post->post_status );
					}

				// Normal result: false
				} elseif ( $this->last_post['bbp_akismet_result'] === 'false' ) {

					// Leave a trail so other's know what we did
					update_post_meta( $post_id, '_bbp_akismet_result', 'false' );
					$this->update_post_history( $post_id, esc_html__( 'Akismet cleared this post as not spam', 'buddyboss' ), 'check-ham' );

					// If post_status is the spam status, which isn't expected, leave a note
					if ( bbp_get_spam_status_id() === $_post->post_status ) {

						// @todo Use wp_blacklist_check()

						$this->update_post_history( $post_id, sprintf( esc_html__( 'Post status was changed to %s', 'buddyboss' ), $_post->post_status ), 'status-changed-' . $_post->post_status );
					}

				// Abnormal result: error
				} else {
					// Leave a trail so other's know what we did
					update_post_meta( $post_id, '_bbp_akismet_error', time() );
					$this->update_post_history( $post_id, sprintf( esc_html__( 'Akismet was unable to check this post (response: %s), will automatically retry again later.', 'buddyboss' ), $this->last_post['bbp_akismet_result'] ), 'check-error' );
				}

				// Record the complete original data as submitted for checking
				if ( isset( $this->last_post['bbp_post_as_submitted'] ) ) {
					update_post_meta( $post_id, '_bbp_akismet_as_submitted', $this->last_post['bbp_post_as_submitted'] );
				}
			}
		}
	}

	/**
	 * Update a post's Akismet history
	 *
	 * @since bbPress (r3308)
	 *
	 * @param int $post_id
	 * @param string $message
	 * @param string $event
	 *
	 * @uses wp_get_current_user() To get the current_user object
	 * @uses add_post_meta() Add Akismet post history
	 */
	private function update_post_history( $post_id = 0, $message = null, $event = null ) {

		// Define local variable(s)
		$user = '';

		// Get the current user
		$current_user = wp_get_current_user();

		// Get the user's login name if possible
		if ( is_object( $current_user ) && isset( $current_user->user_login ) )
			$user = $current_user->user_login;

		// Setup the event to be saved
		$event = array(
			'time'    => akismet_microtime(),
			'message' => $message,
			'event'   => $event,
			'user'    => $user,
		);

		// Save the event data
		add_post_meta( $post_id, '_bbp_akismet_history', $event );
	}

	/**
	 * Get a post's Akismet history
	 *
	 * @since bbPress (r3308)
	 *
	 * @param int $post_id
	 *
	 * @uses wp_get_current_user() To get the current_user object
	 * @uses get_post_meta() Get a post's Akismet history
	 *
	 * @return array Array of a post's Akismet history
	 */
	public function get_post_history( $post_id = 0 ) {

		// Retrieve any previous history
		$history = get_post_meta( $post_id, '_bbp_akismet_history' );

		// Sort it by the time recorded
		usort( $history, 'akismet_cmp_time' );

		return $history;
	}

	/**
	 * Handle any terms submitted with a post flagged as spam
	 *
	 * @since bbPress (r3308)
	 *
	 * @param string $terms Comma-separated list of terms
	 * @param int $topic_id
	 * @param int $reply_id
	 *
	 * @uses bbp_get_reply_id() To get the reply_id
	 * @uses bbp_get_topic_id() To get the topic_id
	 * @uses wp_get_object_terms() To a post's current terms
	 * @uses update_post_meta() To add spam terms to post meta
	 *
	 * @return array Array of existing topic terms
	 */
	public function filter_post_terms( $terms = '', $topic_id = 0, $reply_id = 0 ) {

		// Validate the reply_id and topic_id
		$reply_id = bbp_get_reply_id( $reply_id );
		$topic_id = bbp_get_topic_id( $topic_id );

		// Get any pre-existing terms
		$existing_terms = wp_get_object_terms( $topic_id, bbp_get_topic_tag_tax_id(), array( 'fields' => 'names' ) );

		// Save the terms for later in case the reply gets hammed
		if ( !empty( $terms ) )
			update_post_meta( $reply_id, '_bbp_akismet_spam_terms', $terms );

		// Keep the topic tags the same for now
		return $existing_terms;
	}

	/**
	 * Submit data to Akismet service with unique bbPress User Agent
	 *
	 * This code is directly taken from the akismet_http_post() function and
	 * documented to bbPress 2.0 standard.
	 *
	 * @since bbPress (r3466)
	 *
	 * @param string $request The request we are sending
	 * @param string $host The host to send our request to
	 * @param string $path The path from the host
	 * @param string $port The port to use
	 * @param string $ip Optional Override $host with an IP address
	 * @uses bbp_get_version() To get the current bbPress version
	 * @return mixed WP_Error on error, array on success, empty on failure
	 */
	private function http_post( $request, $host, $path, $port = 80, $ip = '' ) {

		// Preload required variables
		$bbp_version    = bbp_get_version();
		$content_length = strlen( $request );
		$http_host      = $host;
		$blog_charset   = get_option( 'blog_charset' );
		$response       = '';
		$errno          = null;
		$errstr         = null;

		// Untque User Agent
		$akismet_ua     = "bbPress/{$bbp_version} | ";
		$akismet_ua    .= 'Akismet/' . constant( 'AKISMET_VERSION' );

		// Use specific IP (if provided)
		if ( !empty( $ip ) && long2ip( ip2long( $ip ) ) )
			$http_host = $ip;

		// WP HTTP class is available
		if ( function_exists( 'wp_remote_post' ) ) {

			// Setup the arguments
			$http_args = array(
				'body'             => $request,
				'headers'          => array(
					'Content-Type' => 'application/x-www-form-urlencoded; charset=' . $blog_charset,
					'Host'         => $host,
					'User-Agent'   => $akismet_ua
				),
				'httpversion'      => '1.0',
				'timeout'          => 15
			);

			// Where we are sending our request
			$akismet_url = 'http://' . $http_host . $path;

			// Send the request
			$response    = wp_remote_post( $akismet_url, $http_args );

			// Bail if the response is an error
			if ( is_wp_error( $response ) )
				return '';

			// No errors so return response
			return array( $response['headers'], $response['body'] );

		// WP HTTP class is not available (Why not?)
		} else {

			// Header info to use with our socket
			$http_request  = "POST {$path} HTTP/1.0\r\n";
			$http_request .= "Host: {$host}\r\n";
			$http_request .= "Content-Type: application/x-www-form-urlencoded; charset={$blog_charset}\r\n";
			$http_request .= "Content-Length: {$content_length}\r\n";
			$http_request .= "User-Agent: {$akismet_ua}\r\n";
			$http_request .= "\r\n";
			$http_request .= $request;

			// Open a socket connection
			if ( false !== ( $fs = @fsockopen( $http_host, $port, $errno, $errstr, 10 ) ) ) {

				// Write our request to the pointer
				fwrite( $fs, $http_request );

				// Loop through pointer and compile a response
				while ( !feof( $fs ) ) {
					// One TCP-IP packet at a time
					$response .= fgets( $fs, 1160 );
				}

				// Close our socket
				fclose( $fs );

				// Explode the response into usable data
				$response = explode( "\r\n\r\n", $response, 2 );
			}

			// Return the response ('' if error/empty)
			return $response;
		}
	}

	/**
	 * Return a user's roles on this site (including super_admin)
	 *
	 * @since bbPress (r4812)
	 *
	 * @param type $user_id
	 * @return boolean
	 */
	private function get_user_roles( $user_id = 0 ) {

		// Default return value
		$roles = array();

		// Bail if cannot query the user
		if ( ! class_exists( 'WP_User' ) || empty( $user_id ) ) {
			return false;
		}

		// User ID
		$user = new WP_User( $user_id );
		if ( isset( $user->roles ) ) {
			$roles = (array) $user->roles;
		}

		// Super admin
		if ( is_multisite() && is_super_admin( $user_id ) ) {
			$roles[] = 'super_admin';
		}

		return implode( ',', $roles );
	}

	/** Admin *****************************************************************/

	/**
	 * Add Aksimet History metaboxes to topics and replies
	 *
	 * @since bbPress (r5049)
	 */
	public function add_metaboxes() {

		// Topics
		add_meta_box(
			'bbp_akismet_topic_history',
			__( 'Akismet History', 'buddyboss' ),
			array( $this, 'history_metabox' ),
			bbp_get_topic_post_type(),
			'normal',
			'core'
		);

		// Replies
		add_meta_box(
			'bbp_akismet_reply_history',
			__( 'Akismet History', 'buddyboss' ),
			array( $this, 'history_metabox' ),
			bbp_get_reply_post_type(),
			'normal',
			'core'
		);
	}

	/**
	 * Output for Akismet History metabox
	 *
	 * @since bbPress (r5049)
	 *
	 * @uses get_post_history() To get the Akismet history for the post
	 * @uses get_the_ID() To get the post ID
	 * @uses bbp_time_since() To get the human readable time
	 */
	public function history_metabox() {

		// Post ID
		$history = $this->get_post_history( get_the_ID() ); ?>

		<div class="akismet-history" style="margin: 13px 0;">

			<?php if ( !empty( $history ) ) : ?>

				<table>
					<tbody>

						<?php foreach ( $history as $row ) : ?>

							<tr>
								<td style="color: #999; text-align: right; white-space: nowrap;">
									<span title="<?php echo esc_attr( date( 'D d M Y @ h:i:m a', $row['time'] ) . ' GMT' ); ?>">
										<?php bbp_time_since( $row['time'], false, true ); ?>
									</span>
								</td>
								<td style="padding-left: 5px;">
									<?php echo esc_html( $row['message'] ); ?>
								</td>
							</tr>

						<?php endforeach; ?>
					</tbody>
				</table>

			<?php else : ?>

				<p><?php esc_html_e( 'No recorded history. Akismet has not checked this post.', 'buddyboss' ); ?></p>

			<?php endif; ?>

		</div>

		<?php
	}
}

Changelog

Changelog
Version Description
bbPress (r3277) Introduced.

Methods

Questions?

We're always happy to help with code or other questions you might have! Search our developer docs, contact support, or connect with our sales team.