setup_globals();
$this->setup_actions();
$this->setup_filters();
$this->fully_loaded();
}
/**
* Extension variables.
*
* @since 2.2.0 bbPress (r4394)
*
* @access private
*/
private function setup_globals() {
// The name of the BuddyPress component, used in activity streams
$this->component = 'bbpress';
// Forums
$this->forum_create = 'bbp_forum_create';
// Topics
$this->topic_create = 'bbp_topic_create';
$this->topic_edit = 'bbp_topic_edit';
$this->topic_close = 'bbp_topic_close';
$this->topic_open = 'bbp_topic_open';
// Replies
$this->reply_create = 'bbp_reply_create';
$this->reply_edit = 'bbp_reply_edit';
}
/**
* Setup the actions.
*
* @since 2.2.0 bbPress (r4394)
*
* @access private
*/
private function setup_actions() {
// Register the activity stream actions
add_action( 'bp_register_activity_actions', array( $this, 'register_activity_actions' ) );
// Hook into topic and reply creation
add_action( 'bbp_new_topic', array( $this, 'topic_create' ), 10, 4 );
add_action( 'bbp_new_reply', array( $this, 'reply_create' ), 10, 5 );
// Hook into topic and reply status changes
add_action( 'edit_post', array( $this, 'topic_update' ), 10, 2 );
add_action( 'edit_post', array( $this, 'reply_update' ), 10, 2 );
// Hook into topic and reply deletion
add_action( 'bbp_delete_topic', array( $this, 'topic_delete' ), 10, 1 );
add_action( 'bbp_delete_reply', array( $this, 'reply_delete' ), 10, 1 );
}
/**
* Setup the filters.
*
* @since 2.2.0 bbPress (r4394)
*
* @access private
*/
private function setup_filters() {
// Obey BuddyPress commenting rules
add_filter( 'bp_activity_can_comment', array( $this, 'activity_can_comment' ) );
// Link directly to the topic or reply
add_filter( 'bp_activity_get_permalink', array( $this, 'activity_get_permalink' ), 10, 2 );
// Check current forum visibility when activity is read, including older entries.
add_filter( 'bp_activity_get_where_conditions', array( $this, 'activity_get_where_conditions' ), 10, 2 );
add_filter( 'bp_activity_get', array( $this, 'activity_get' ), 10, 2 );
add_filter( 'bp_activity_user_can_read', array( $this, 'activity_user_can_read' ), 10, 3 );
}
/**
* Allow the variables, actions, and filters to be modified by third party
* plugins and themes.
*
* @since 2.2.0 bbPress (r4394)
*/
private function fully_loaded() {
do_action_ref_array( 'bbp_buddypress_activity_loaded', array( $this ) );
}
/** Methods ***************************************************************/
/**
* Identify BuddyPress's REST single-item query, which checks access afterward.
*
* @param array $args BuddyPress activity query arguments.
* @return bool Whether this is a single-item lookup.
*/
private function is_single_activity_lookup( $args ) {
return ! empty( $args['show_hidden'] )
&& ! empty( $args['display_comments'] )
&& empty( $args['per_page'] )
&& ! empty( $args['in'] )
&& 1 === count( wp_parse_id_list( $args['in'] ) );
}
/**
* Check whether an activity query can contain bbPress topic or reply activity.
*
* @param array $args BuddyPress activity query arguments.
* @return bool Whether bbPress visibility conditions may be needed.
*/
private function query_can_include_forum_activity( $args ) {
if ( empty( $args['filter'] ) || ! is_array( $args['filter'] ) ) {
return true;
}
$filter = $args['filter'];
if ( ! empty( $filter['action'] ) ) {
$actions = wp_parse_list( $filter['action'] );
if ( ! array_intersect( $actions, array( $this->topic_create, $this->reply_create ) ) ) {
return false;
}
}
if ( ! empty( $filter['object'] ) ) {
$objects = wp_parse_list( $filter['object'] );
$group_component = bp_is_active( 'groups' ) ? buddypress()->groups->id : 'groups';
if ( ! array_intersect( $objects, array( $this->component, $group_component ) ) ) {
return false;
}
}
return true;
}
/**
* Get password-protected bbPress posts that the current visitor cannot read.
*
* @return array[] Post IDs keyed by post type.
*/
private function get_password_restricted_post_ids() {
$post_types = bbp_get_post_types();
$post_ids = array_fill_keys( $post_types, array() );
$protected = get_posts(
array(
'has_password' => true,
'no_found_rows' => true,
'numberposts' => -1,
'orderby' => 'none',
'post_status' => 'any',
'post_type' => $post_types,
'suppress_filters' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
)
);
foreach ( $protected as $post ) {
if ( post_password_required( $post ) ) {
$post_ids[ $post->post_type ][] = (int) $post->ID;
}
}
// A password on a forum also protects every descendant forum.
$forum_type = bbp_get_forum_post_type();
$parents = $post_ids[ $forum_type ];
$seen = array_fill_keys( $parents, true );
$index = 0;
while ( isset( $parents[ $index ] ) ) {
$parent_id = $parents[ $index ];
++$index;
foreach ( bbp_forum_query_subforum_ids( $parent_id ) as $forum_id ) {
$forum_id = (int) $forum_id;
if ( ! isset( $seen[ $forum_id ] ) ) {
$seen[ $forum_id ] = true;
$post_ids[ $forum_type ][] = $forum_id;
$parents[] = $forum_id;
}
}
}
return $post_ids;
}
/**
* Check the single-item query shape when used through bp_activity_get().
*
* BuddyPress REST retrieves single items through bp_activity_get_specific(),
* which performs its own can-read check. Other callers using the same query
* shape must not inherit that query's visibility exception.
*
* @param array $result BuddyPress activity query result.
* @param array $args BuddyPress activity query arguments.
* @return array Filtered activity query result.
*/
public function activity_get( $result, $args ) {
if ( ! $this->is_single_activity_lookup( $args ) || empty( $result['activities'] ) ) {
return $result;
}
foreach ( $result['activities'] as $key => $activity ) {
if ( ! bp_activity_user_can_read( $activity, get_current_user_id() ) ) {
unset( $result['activities'][ $key ] );
}
}
$result['activities'] = array_values( $result['activities'] );
$result['total'] = count( $result['activities'] );
return $result;
}
/**
* Exclude activity whose topic or reply is now in a restricted forum.
*
* Activity can outlive a forum visibility or parent change, so the stored
* hide_sitewide value alone cannot protect previously public entries.
*
* @param array $where_conditions BuddyPress activity query conditions.
* @param array $args BuddyPress activity query arguments.
* @return array Filtered conditions.
*/
public function activity_get_where_conditions( $where_conditions, $args ) {
global $wpdb;
// The REST single-item lookup needs the object for BuddyPress's can-read check.
if ( $this->is_single_activity_lookup( $args ) ) {
return $where_conditions;
}
if ( ! $this->query_can_include_forum_activity( $args ) ) {
return $where_conditions;
}
$forum_ids = array();
foreach ( wp_parse_id_list( bbp_get_excluded_forum_ids() ) as $forum_id ) {
if ( bbp_is_forum_restricted_for_user( $forum_id, get_current_user_id() ) ) {
$forum_ids[] = $forum_id;
}
}
$password_ids = $this->get_password_restricted_post_ids();
$forum_ids = wp_parse_id_list( array_merge( $forum_ids, $password_ids[ bbp_get_forum_post_type() ] ) );
$topic_ids = wp_parse_id_list( $password_ids[ bbp_get_topic_post_type() ] );
$reply_ids = wp_parse_id_list( $password_ids[ bbp_get_reply_post_type() ] );
if ( empty( $forum_ids ) && empty( $topic_ids ) && empty( $reply_ids ) ) {
return $where_conditions;
}
$group_component = bp_is_active( 'groups' ) ? buddypress()->groups->id : 'groups';
// Group activity stores the post ID in secondary_item_id instead.
$topic_restrictions = array();
$topic_values = array( $this->topic_create, $this->component, $group_component, $group_component );
if ( ! empty( $forum_ids ) ) {
$topic_restrictions[] = 'bbp_topic.post_parent IN (' . implode( ', ', array_fill( 0, count( $forum_ids ), '%d' ) ) . ')';
$topic_values = array_merge( $topic_values, $forum_ids );
}
if ( ! empty( $topic_ids ) ) {
$topic_restrictions[] = 'bbp_topic.ID IN (' . implode( ', ', array_fill( 0, count( $topic_ids ), '%d' ) ) . ')';
$topic_values = array_merge( $topic_values, $topic_ids );
}
if ( ! empty( $topic_restrictions ) ) {
$topic_sql = "
NOT (
a.type = %s
AND (
a.component = %s
OR a.component = %s
)
AND EXISTS (
SELECT 1
FROM {$wpdb->posts} AS bbp_topic
WHERE bbp_topic.ID = CASE
WHEN a.component = %s THEN a.secondary_item_id
ELSE a.item_id
END
AND (" . implode( ' OR ', $topic_restrictions ) . ')
)
)';
$where_conditions['bbpress_topic_visibility'] = $wpdb->prepare( $topic_sql, $topic_values ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- SQL contains only table names and generated integer placeholders.
}
// Reply activity points to the reply; follow its topic to the forum.
$reply_restrictions = array();
$reply_values = array( $this->reply_create, $this->component, $group_component, $group_component );
if ( ! empty( $forum_ids ) ) {
$reply_restrictions[] = 'bbp_reply_topic.post_parent IN (' . implode( ', ', array_fill( 0, count( $forum_ids ), '%d' ) ) . ')';
$reply_values = array_merge( $reply_values, $forum_ids );
}
if ( ! empty( $topic_ids ) ) {
$reply_restrictions[] = 'bbp_reply_topic.ID IN (' . implode( ', ', array_fill( 0, count( $topic_ids ), '%d' ) ) . ')';
$reply_values = array_merge( $reply_values, $topic_ids );
}
if ( ! empty( $reply_ids ) ) {
$reply_restrictions[] = 'bbp_reply.ID IN (' . implode( ', ', array_fill( 0, count( $reply_ids ), '%d' ) ) . ')';
$reply_values = array_merge( $reply_values, $reply_ids );
}
if ( ! empty( $reply_restrictions ) ) {
$reply_sql = "
NOT (
a.type = %s
AND (
a.component = %s
OR a.component = %s
)
AND EXISTS (
SELECT 1
FROM {$wpdb->posts} AS bbp_reply
INNER JOIN {$wpdb->posts} AS bbp_reply_topic
ON bbp_reply_topic.ID = bbp_reply.post_parent
WHERE bbp_reply.ID = CASE
WHEN a.component = %s THEN a.secondary_item_id
ELSE a.item_id
END
AND (" . implode( ' OR ', $reply_restrictions ) . ')
)
)';
$where_conditions['bbpress_reply_visibility'] = $wpdb->prepare( $reply_sql, $reply_values ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- SQL contains only table names and generated integer placeholders.
}
return $where_conditions;
}
/**
* Check the current forum before allowing direct access to activity.
*
* @param bool $can_read Whether BuddyPress permits access.
* @param int $user_id User requesting the activity.
* @param BP_Activity_Activity $activity Activity being requested.
* @return bool Whether the user may read the activity.
*/
public function activity_user_can_read( $can_read, $user_id, $activity ) {
$group_component = bp_is_active( 'groups' ) ? buddypress()->groups->id : 'groups';
if ( ! $can_read || ! in_array( $activity->component, array( $this->component, $group_component ), true ) ) {
return $can_read;
}
$post_id = ( $group_component === $activity->component )
? $activity->secondary_item_id
: $activity->item_id;
if ( $this->topic_create === $activity->type ) {
$forum_id = bbp_get_topic_forum_id( $post_id );
} elseif ( $this->reply_create === $activity->type ) {
$forum_id = bbp_get_reply_forum_id( $post_id );
} else {
return $can_read;
}
return ! empty( $forum_id )
&& ! bbp_is_forum_restricted_for_user( $forum_id, $user_id )
&& ! bbp_get_password_required_id( $post_id );
}
/**
* Register our activity actions with BuddyPress.
*
* @since 2.2.0 bbPress (r4394)
*/
public function register_activity_actions() {
// Sitewide topic
bp_activity_set_action(
$this->component,
$this->topic_create,
esc_html__( 'New forum topic', 'bbpress' ),
'bbp_format_activity_action_new_topic',
esc_html__( 'Topics', 'bbpress' ),
array( 'activity', 'member', 'member_groups', 'group' )
);
// Sitewide reply
bp_activity_set_action(
$this->component,
$this->reply_create,
esc_html__( 'New forum reply', 'bbpress' ),
'bbp_format_activity_action_new_reply',
esc_html__( 'Replies', 'bbpress' ),
array( 'activity', 'member', 'member_groups', 'group' )
);
}
/**
* Wrapper for recoding bbPress actions to the BuddyPress activity stream.
*
* @since 2.2.0 bbPress (r4394)
*
* @param array $args Array of arguments for bp_activity_add().
*
* @return int Activity ID if successful, false if not.
*/
private function record_activity( $args = array() ) {
// Default activity args
$activity = bbp_parse_args(
$args,
array(
'id' => null,
'user_id' => bbp_get_current_user_id(),
'type' => '',
'action' => '',
'item_id' => '',
'secondary_item_id' => '',
'content' => '',
'primary_link' => '',
'component' => $this->component,
'recorded_time' => bp_core_current_time(),
'hide_sitewide' => false
),
'record_activity'
);
// Add the activity
return bp_activity_add( $activity );
}
/**
* Wrapper for deleting bbPress actions from BuddyPress activity stream.
*
* @since 2.2.0 bbPress (r4394)
*
* @param array $args Array of arguments for bp_activity_add().
*
* @return int Activity ID if successful, false if not.
*/
public function delete_activity( $args = array() ) {
// Default activity args
$activity = bbp_parse_args(
$args,
array(
'item_id' => false,
'component' => $this->component,
'type' => false,
'user_id' => false,
'secondary_item_id' => false
),
'delete_activity'
);
// Delete the activity
bp_activity_delete_by_item_id( $activity );
}
/**
* Check for an existing activity stream entry for a given post_id.
*
* @param int $post_id ID of the topic or reply.
* @return int if an activity id is verified, false if not.
*/
private static function get_activity_id( $post_id = 0 ) {
// Try to get the activity ID of the post
$activity_id = (int) get_post_meta( $post_id, '_bbp_activity_id', true );
// Bail if no activity ID is in post meta
if ( empty( $activity_id ) ) {
return null;
}
// Get the activity stream item, bail if it doesn't exist
$existing = new BP_Activity_Activity( $activity_id );
if ( empty( $existing->component ) ) {
return null;
}
// Return the activity ID since we've verified the connection
return $activity_id;
}
/**
* Maybe disable activity stream comments on select actions.
*
* @since 2.2.0 bbPress (r4394)
*
* @global BP_Activity_Template $activities_template
* @param boolean $can_comment
* @return boolean
*/
public function activity_can_comment( $can_comment = true ) {
global $activities_template;
// Already forced off, so comply
if ( false === $can_comment ) {
return $can_comment;
}
// Check if blog & forum activity stream commenting is off
if ( ! empty( $activities_template->disable_blogforum_replies ) ) {
// Get the current action name
$action_name = bp_get_activity_action_name();
// Setup the array of possibly disabled actions
$disabled_actions = array(
$this->topic_create,
$this->reply_create
);
// Check if this activity stream action is disabled
if ( in_array( $action_name, $disabled_actions, true ) ) {
$can_comment = false;
}
}
return $can_comment;
}
/**
* Maybe link directly to topics and replies in activity stream entries.
*
* @since 2.2.0 bbPress (r4394)
*
* @param string $link
* @param mixed $activity_object
* @return string The link to the activity stream item.
*/
public function activity_get_permalink( $link = '', $activity_object = false ) {
// Setup the array of actions to link directly to
$disabled_actions = array(
$this->topic_create,
$this->reply_create
);
// Check if this activity stream action is directly linked
if ( in_array( $activity_object->type, $disabled_actions, true ) ) {
$link = $activity_object->primary_link;
}
return $link;
}
/** Topics ****************************************************************/
/**
* Record an activity stream entry when a topic is created or updated.
*
* @since 2.2.0 bbPress (r4394)
*
* @param int $topic_id
* @param int $forum_id
* @param array $anonymous_data
* @param int $topic_author_id
* @return Bail early if topic is by anonymous user.
*/
public function topic_create( $topic_id = 0, $forum_id = 0, $anonymous_data = array(), $topic_author_id = 0 ) {
// Bail early if topic is by anonymous user
if ( ! empty( $anonymous_data ) ) {
return;
}
// Bail if site is private
if ( ! bbp_is_site_public() ) {
return;
}
// Validate activity data
$user_id = bbp_get_user_id( $topic_author_id );
$topic_id = bbp_get_topic_id( $topic_id );
$forum_id = bbp_get_forum_id( $forum_id );
// Bail if user is not active
if ( bbp_is_user_inactive( $user_id ) ) {
return;
}
// Bail if topic is not published
if ( ! bbp_is_topic_published( $topic_id ) ) {
return;
}
// User link for topic author
$user_link = bbp_get_user_profile_link( $user_id );
// Topic
$topic_permalink = bbp_get_topic_permalink( $topic_id );
$topic_title = get_post_field( 'post_title', $topic_id, 'raw' );
$topic_content = get_post_field( 'post_content', $topic_id, 'raw' );
$topic_link = '' . $topic_title . '';
// Forum
$forum_permalink = bbp_get_forum_permalink( $forum_id );
$forum_title = get_post_field( 'post_title', $forum_id, 'raw' );
$forum_link = '' . $forum_title . '';
// Activity action & text
$activity_text = sprintf(
/* translators: 1: User linked profile, 2: Topic linked title, 3: Forum linked title */
esc_html__( '%1$s started the topic %2$s in the forum %3$s', 'bbpress' ),
$user_link,
$topic_link,
$forum_link
);
$activity_action = apply_filters( 'bbp_activity_topic_create', $activity_text, $user_id, $topic_id, $forum_id );
$activity_content = apply_filters( 'bbp_activity_topic_create_excerpt', $topic_content );
// Compile and record the activity stream results
$activity_id = $this->record_activity(
array(
'id' => $this->get_activity_id( $topic_id ),
'user_id' => $user_id,
'action' => $activity_action,
'content' => $activity_content,
'primary_link' => $topic_permalink,
'type' => $this->topic_create,
'item_id' => $topic_id,
'secondary_item_id' => $forum_id,
'recorded_time' => get_post_time( 'Y-m-d H:i:s', true, $topic_id ),
'hide_sitewide' => ! bbp_is_forum_public( $forum_id )
)
);
// Add the activity entry ID as a meta value to the topic
if ( ! empty( $activity_id ) ) {
update_post_meta( $topic_id, '_bbp_activity_id', $activity_id );
}
}
/**
* Delete the activity stream entry when a topic is spammed, trashed, or deleted.
*
* @param int $topic_id
*/
public function topic_delete( $topic_id = 0 ) {
// Get activity ID, bail if it doesn't exist
$activity_id = $this->get_activity_id( $topic_id );
if ( ! empty( $activity_id ) ) {
return bp_activity_delete( array( 'id' => $activity_id ) );
}
return false;
}
/**
* Update the activity stream entry when a topic status changes.
*
* @param int $topic_id
* @param obj $post
* @return Bail early if not a topic, or topic is by anonymous user.
*/
public function topic_update( $topic_id = 0, $post = null ) {
// Bail early if not a topic
if ( get_post_type( $post ) !== bbp_get_topic_post_type() ) {
return;
}
// Bail early if revisions are off
if ( ! bbp_allow_revisions() || ! post_type_supports( bbp_get_topic_post_type(), 'revisions' ) ) {
return;
}
$topic_id = bbp_get_topic_id( $topic_id );
// Bail early if topic is by anonymous user
if ( bbp_is_topic_anonymous( $topic_id ) ) {
return;
}
// Action based on new status
if ( bbp_is_topic_public( $post->ID ) ) {
// Validate topic data
$forum_id = bbp_get_topic_forum_id( $topic_id );
$topic_author_id = bbp_get_topic_author_id( $topic_id );
$this->topic_create( $topic_id, $forum_id, array(), $topic_author_id );
} else {
$this->topic_delete( $topic_id );
}
}
/** Replies ***************************************************************/
/**
* Record an activity stream entry when a reply is created.
*
* @since 2.2.0 bbPress (r4394)
*
* @param int $topic_id
* @param int $forum_id
* @param array $anonymous_data
* @param int $topic_author_id
* @return Bail early if topic is by anonymous user.
*/
public function reply_create( $reply_id = 0, $topic_id = 0, $forum_id = 0, $anonymous_data = array(), $reply_author_id = 0 ) {
// Do not log activity of anonymous users
if ( ! empty( $anonymous_data ) ) {
return;
}
// Bail if site is private
if ( ! bbp_is_site_public() ) {
return;
}
// Validate activity data
$user_id = bbp_get_user_id( $reply_author_id );
$reply_id = bbp_get_reply_id( $reply_id );
$topic_id = bbp_get_topic_id( $topic_id );
$forum_id = bbp_get_forum_id( $forum_id );
// Bail if user is not active
if ( bbp_is_user_inactive( $user_id ) ) {
return;
}
// Bail if reply is not published
if ( ! bbp_is_reply_published( $reply_id ) ) {
return;
}
// Setup links for activity stream
$user_link = bbp_get_user_profile_link( $user_id );
// Reply
$reply_url = bbp_get_reply_url( $reply_id );
$reply_content = get_post_field( 'post_content', $reply_id, 'raw' );
// Topic
$topic_permalink = bbp_get_topic_permalink( $topic_id );
$topic_title = get_post_field( 'post_title', $topic_id, 'raw' );
$topic_link = '' . $topic_title . '';
// Forum
$forum_permalink = bbp_get_forum_permalink( $forum_id );
$forum_title = get_post_field( 'post_title', $forum_id, 'raw' );
$forum_link = '' . $forum_title . '';
// Activity action & text
$activity_text = sprintf(
/* translators: 1: User linked profile, 2: Topic linked title, 3: Forum linked title */
esc_html__( '%1$s replied to the topic %2$s in the forum %3$s', 'bbpress' ),
$user_link,
$topic_link,
$forum_link
);
$activity_action = apply_filters( 'bbp_activity_reply_create', $activity_text, $user_id, $reply_id, $topic_id );
$activity_content = apply_filters( 'bbp_activity_reply_create_excerpt', $reply_content );
// Compile and record the activity stream results
$activity_id = $this->record_activity(
array(
'id' => $this->get_activity_id( $reply_id ),
'user_id' => $user_id,
'action' => $activity_action,
'content' => $activity_content,
'primary_link' => $reply_url,
'type' => $this->reply_create,
'item_id' => $reply_id,
'secondary_item_id' => $topic_id,
'recorded_time' => get_post_time( 'Y-m-d H:i:s', true, $reply_id ),
'hide_sitewide' => ! bbp_is_forum_public( $forum_id )
)
);
// Add the activity entry ID as a meta value to the reply
if ( ! empty( $activity_id ) ) {
update_post_meta( $reply_id, '_bbp_activity_id', $activity_id );
}
}
/**
* Delete the activity stream entry when a reply is spammed, trashed, or deleted.
*
* @param int $reply_id
*/
public function reply_delete( $reply_id ) {
// Get activity ID, bail if it doesn't exist
$activity_id = $this->get_activity_id( $reply_id );
if ( ! empty( $activity_id ) ) {
return bp_activity_delete( array( 'id' => $activity_id ) );
}
return false;
}
/**
* Update the activity stream entry when a reply status changes.
*
* @param int $reply_id
* @param obj $post
* @return Bail early if not a reply, or reply is by anonymous user.
*/
public function reply_update( $reply_id, $post ) {
// Bail early if not a reply
if ( get_post_type( $post ) !== bbp_get_reply_post_type() ) {
return;
}
// Bail early if revisions are off
if ( ! bbp_allow_revisions() || ! post_type_supports( bbp_get_reply_post_type(), 'revisions' ) ) {
return;
}
$reply_id = bbp_get_reply_id( $reply_id );
// Bail early if reply is by anonymous user
if ( bbp_is_reply_anonymous( $reply_id ) ) {
return;
}
// Action based on new status
if ( bbp_get_public_status_id() === $post->post_status ) {
// Validate reply data
$topic_id = bbp_get_reply_topic_id( $reply_id );
$forum_id = bbp_get_reply_forum_id( $reply_id );
$reply_author_id = bbp_get_reply_author_id( $reply_id );
$this->reply_create( $reply_id, $topic_id, $forum_id, array(), $reply_author_id );
} else {
$this->reply_delete( $reply_id );
}
}
}
endif;