HEX
Server: Microsoft-IIS/8.5
System: Windows NT YDAWBH120 6.3 build 9600 (Windows Server 2012 R2 Standard Edition) AMD64
User: tentjecom_web (0)
PHP: 7.4.14
Disabled: NONE
Upload Files
File: D:/HostingSpaces/KWessing/buddypress/bp-groups/bp-groups-classes.php
<?php

/**
 * BuddyPress Groups Classes
 *
 * @package BuddyPress
 * @subpackage GroupsClasses
 */

// Exit if accessed directly
if ( !defined( 'ABSPATH' ) ) exit;

/**
 * BuddyPress Group object.
 */
class BP_Groups_Group {

	/**
	 * ID of the group.
	 *
	 * @access public
	 * @var int
	 */
	public $id;

	/**
	 * User ID of the group's creator.
	 *
	 * @access public
	 * @var int
	 */
	public $creator_id;

	/**
	 * Name of the group.
	 *
	 * @access public
	 * @var string
	 */
	public $name;

	/**
	 * Group slug.
	 *
	 * @access public
	 * @var string
	 */
	public $slug;

	/**
	 * Group description.
	 *
	 * @access public
	 * @var string
	 */
	public $description;

	/**
	 * Group status.
	 *
	 * Core statuses are 'public', 'private', and 'hidden'.
	 *
	 * @access public
	 * @var string
	 */
	public $status;

	/**
	 * Should (legacy) bbPress forums be enabled for this group?
	 *
	 * @access public
	 * @var int
	 */
	public $enable_forum;

	/**
	 * Date the group was created.
	 *
	 * @access public
	 * @var string
	 */
	public $date_created;

	/**
	 * Data about the group's admins.
	 *
	 * @access public
	 * @var array
	 */
	public $admins;

	/**
	 * Data about the group's moderators.
	 *
	 * @access public
	 * @var array
	 */
	public $mods;

	/**
	 * Total count of group members.
	 *
	 * @access public
	 * @var int
	 */
	public $total_member_count;

	/**
	 * Is the current user a member of this group?
	 *
	 * @since BuddyPress (1.2.0)
	 * @var bool
	 */
	public $is_member;

	/**
	 * Does the current user have an outstanding invitation to this group?
	 *
	 * @since BuddyPress (1.9.0)
	 * @var bool
	 */
	public $is_invited;

	/**
	 * Does the current user have a pending membership request to this group?
	 *
	 * @since BuddyPress (1.9.0)
	 * @var bool
	 */
	public $is_pending;

	/**
	 * Timestamp of the last activity that happened in this group.
	 *
	 * @since BuddyPress (1.2.0)
	 * @var string
	 */
	public $last_activity;

	/**
	 * If this is a private or hidden group, does the current user have access?
	 *
	 * @since BuddyPress (1.6.0)
	 * @var bool
	 */
	public $user_has_access;

	/**
	 * Constructor method.
	 *
	 * @param int $id Optional. If the ID of an existing group is provided,
	 *        the object will be pre-populated with info about that group.
	 */
	public function __construct( $id = null ) {
		if ( !empty( $id ) ) {
			$this->id = $id;
			$this->populate();
		}
	}

	/**
	 * Set up data about the current group.
	 */
	public function populate() {
		global $wpdb, $bp;

		if ( $group = $wpdb->get_row( $wpdb->prepare( "SELECT g.* FROM {$bp->groups->table_name} g WHERE g.id = %d", $this->id ) ) ) {
			bp_groups_update_meta_cache( $this->id );

			$this->id                 = $group->id;
			$this->creator_id         = $group->creator_id;
			$this->name               = stripslashes($group->name);
			$this->slug               = $group->slug;
			$this->description        = stripslashes($group->description);
			$this->status             = $group->status;
			$this->enable_forum       = $group->enable_forum;
			$this->date_created       = $group->date_created;
			$this->last_activity      = groups_get_groupmeta( $this->id, 'last_activity' );
			$this->total_member_count = groups_get_groupmeta( $this->id, 'total_member_count' );
			$this->is_member          = BP_Groups_Member::check_is_member( bp_loggedin_user_id(), $this->id );
			$this->is_invited         = BP_Groups_Member::check_has_invite( bp_loggedin_user_id(), $this->id );
			$this->is_pending         = BP_Groups_Member::check_for_membership_request( bp_loggedin_user_id(), $this->id );

			// If this is a private or hidden group, does the current user have access?
			if ( 'private' == $this->status || 'hidden' == $this->status ) {
				if ( $this->is_member && is_user_logged_in() || bp_current_user_can( 'bp_moderate' ) )
					$this->user_has_access = true;
				else
					$this->user_has_access = false;
			} else {
				$this->user_has_access = true;
			}

			// Get group admins and mods
			$admin_mods = $wpdb->get_results( apply_filters( 'bp_group_admin_mods_user_join_filter', $wpdb->prepare( "SELECT u.ID as user_id, u.user_login, u.user_email, u.user_nicename, m.is_admin, m.is_mod FROM {$wpdb->users} u, {$bp->groups->table_name_members} m WHERE u.ID = m.user_id AND m.group_id = %d AND ( m.is_admin = 1 OR m.is_mod = 1 )", $this->id ) ) );
			foreach( (array) $admin_mods as $user ) {
				if ( (int) $user->is_admin )
					$this->admins[] = $user;
				else
					$this->mods[] = $user;
			}
		} else {
			$this->id = 0;
		}
	}

	/**
	 * Save the current group to the database.
	 *
	 * @return bool True on success, false on failure.
	 */
	public function save() {
		global $wpdb, $bp;

		$this->creator_id   = apply_filters( 'groups_group_creator_id_before_save',   $this->creator_id,   $this->id );
		$this->name         = apply_filters( 'groups_group_name_before_save',         $this->name,         $this->id );
 		$this->slug         = apply_filters( 'groups_group_slug_before_save',         $this->slug,         $this->id );
		$this->description  = apply_filters( 'groups_group_description_before_save',  $this->description,  $this->id );
 		$this->status       = apply_filters( 'groups_group_status_before_save',       $this->status,       $this->id );
		$this->enable_forum = apply_filters( 'groups_group_enable_forum_before_save', $this->enable_forum, $this->id );
		$this->date_created = apply_filters( 'groups_group_date_created_before_save', $this->date_created, $this->id );

		do_action_ref_array( 'groups_group_before_save', array( &$this ) );

		if ( !empty( $this->id ) ) {
			$sql = $wpdb->prepare(
				"UPDATE {$bp->groups->table_name} SET
					creator_id = %d,
					name = %s,
					slug = %s,
					description = %s,
					status = %s,
					enable_forum = %d,
					date_created = %s
				WHERE
					id = %d
				",
					$this->creator_id,
					$this->name,
					$this->slug,
					$this->description,
					$this->status,
					$this->enable_forum,
					$this->date_created,
					$this->id
			);
		} else {
			$sql = $wpdb->prepare(
				"INSERT INTO {$bp->groups->table_name} (
					creator_id,
					name,
					slug,
					description,
					status,
					enable_forum,
					date_created
				) VALUES (
					%d, %s, %s, %s, %s, %d, %s
				)",
					$this->creator_id,
					$this->name,
					$this->slug,
					$this->description,
					$this->status,
					$this->enable_forum,
					$this->date_created
			);
		}

		if ( false === $wpdb->query($sql) )
			return false;

		if ( empty( $this->id ) )
			$this->id = $wpdb->insert_id;

		do_action_ref_array( 'groups_group_after_save', array( &$this ) );

		wp_cache_delete( 'bp_groups_group_' . $this->id, 'bp' );

		return true;
	}

	/**
	 * Delete the current group.
	 *
	 * @return bool True on success, false on failure.
	 */
	public function delete() {
		global $wpdb, $bp;

		// Delete groupmeta for the group
		groups_delete_groupmeta( $this->id );

		// Fetch the user IDs of all the members of the group
		$user_ids    = BP_Groups_Member::get_group_member_ids( $this->id );
		$user_id_str = esc_sql( implode( ',', wp_parse_id_list( $user_ids ) ) );

		// Modify group count usermeta for members
		$wpdb->query( "UPDATE {$wpdb->usermeta} SET meta_value = meta_value - 1 WHERE meta_key = 'total_group_count' AND user_id IN ( {$user_id_str} )" );

		// Now delete all group member entries
		BP_Groups_Member::delete_all( $this->id );

		do_action_ref_array( 'bp_groups_delete_group', array( &$this, $user_ids ) );

		wp_cache_delete( 'bp_groups_group_' . $this->id, 'bp' );

		// Finally remove the group entry from the DB
		if ( !$wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->groups->table_name} WHERE id = %d", $this->id ) ) )
			return false;

		return true;
	}

	/** Static Methods ****************************************************/

	/**
	 * Get whether a group exists for a given slug.
	 *
	 * @param string $slug Slug to check.
	 * @param string $table_name Optional. Name of the table to check
	 *        against. Default: $bp->groups->table_name.
	 * @return string|null ID of the group, if one is found, else null.
	 */
	public static function group_exists( $slug, $table_name = false ) {
		global $wpdb, $bp;

		if ( empty( $table_name ) )
			$table_name = $bp->groups->table_name;

		if ( empty( $slug ) )
			return false;

		return $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$table_name} WHERE slug = %s", strtolower( $slug ) ) );
	}

	/**
	 * Get the ID of a group by the group's slug.
	 *
	 * Alias of {@link BP_Groups_Group::group_exists()}.
	 *
	 * @param string $slug See {@link BP_Groups_Group::group_exists()}.
	 * @return string|null See {@link BP_Groups_Group::group_exists()}.
	 */
	public static function get_id_from_slug( $slug ) {
		return BP_Groups_Group::group_exists( $slug );
	}

	/**
	 * Get IDs of users with outstanding invites to a given group from a specified user.
	 *
	 * @param int $user_id ID of the inviting user.
	 * @param int $group_id ID of the group.
	 * @return array IDs of users who have been invited to the group by the
	 *         user but have not yet accepted.
	 */
	public static function get_invites( $user_id, $group_id ) {
		global $wpdb, $bp;
		return $wpdb->get_col( $wpdb->prepare( "SELECT user_id FROM {$bp->groups->table_name_members} WHERE group_id = %d and is_confirmed = 0 AND inviter_id = %d", $group_id, $user_id ) );
	}

	/**
	 * Get a list of a user's groups, filtered by a search string.
	 *
	 * @param string $filter Search term. Matches against 'name' and
	 *        'description' fields.
	 * @param int $user_id ID of the user whose groups are being searched.
	 *        Default: the displayed user.
	 * @param mixed $order Not used.
	 * @param int $limit Optional. The max number of results to return.
	 *        Default: null (no limit).
	 * @param int $page Optional. The page offset of results to return.
	 *        Default: null (no limit).
	 * @return array {
	 *     @type array $groups Array of matched and paginated group objects.
	 *     @type int $total Total count of groups matching the query.
	 * }
	 */
	public static function filter_user_groups( $filter, $user_id = 0, $order = false, $limit = null, $page = null ) {
		global $wpdb, $bp;

		if ( empty( $user_id ) )
			$user_id = bp_displayed_user_id();

		$filter = esc_sql( like_escape( $filter ) );

		$pag_sql = $order_sql = $hidden_sql = '';

		if ( !empty( $limit ) && !empty( $page ) )
			$pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );

		// Get all the group ids for the current user's groups.
		$gids = BP_Groups_Member::get_group_ids( $user_id );

		if ( empty( $gids['groups'] ) )
			return false;

		$gids = esc_sql( implode( ',', wp_parse_id_list( $gids['groups'] ) ) );

		$paged_groups = $wpdb->get_results( "SELECT id as group_id FROM {$bp->groups->table_name} WHERE ( name LIKE '{$filter}%%' OR description LIKE '{$filter}%%' ) AND id IN ({$gids}) {$pag_sql}" );
		$total_groups = $wpdb->get_var( "SELECT COUNT(id) FROM {$bp->groups->table_name} WHERE ( name LIKE '{$filter}%%' OR description LIKE '{$filter}%%' ) AND id IN ({$gids})" );

		return array( 'groups' => $paged_groups, 'total' => $total_groups );
	}

	/**
	 * Get a list of groups, filtered by a search string.
	 *
	 * @param string $filter Search term. Matches against 'name' and
	 *        'description' fields.
	 * @param int $limit Optional. The max number of results to return.
	 *        Default: null (no limit).
	 * @param int $page Optional. The page offset of results to return.
	 *        Default: null (no limit).
	 * @param string $sort_by Column to sort by. Default: false (default
	 *        sort).
	 * @param string $order ASC or DESC. Default: false (default sort).
	 * @return array {
	 *     @type array $groups Array of matched and paginated group objects.
	 *     @type int $total Total count of groups matching the query.
	 * }
	 */
	public static function search_groups( $filter, $limit = null, $page = null, $sort_by = false, $order = false ) {
		global $wpdb, $bp;

		$filter = esc_sql( like_escape( $filter ) );

		$pag_sql = $order_sql = $hidden_sql = '';

		if ( !empty( $limit ) && !empty( $page ) )
			$pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );

		if ( !empty( $sort_by ) && !empty( $order ) ) {
			$sort_by   = esc_sql( $sort_by );
			$order     = esc_sql( $order );
			$order_sql = "ORDER BY {$sort_by} {$order}";
		}

		if ( !bp_current_user_can( 'bp_moderate' ) )
			$hidden_sql = "AND status != 'hidden'";

		$paged_groups = $wpdb->get_results( "SELECT id as group_id FROM {$bp->groups->table_name} WHERE ( name LIKE '%%{$filter}%%' OR description LIKE '%%{$filter}%%' ) {$hidden_sql} {$order_sql} {$pag_sql}" );
		$total_groups = $wpdb->get_var( "SELECT COUNT(id) FROM {$bp->groups->table_name} WHERE ( name LIKE '%%{$filter}%%' OR description LIKE '%%{$filter}%%' ) {$hidden_sql}" );

		return array( 'groups' => $paged_groups, 'total' => $total_groups );
	}

	/**
	 * Check for the existence of a slug.
	 *
	 * @param string $slug Slug to check.
	 * @return string|null The slug, if found. Otherwise null.
	 */
	public static function check_slug( $slug ) {
		global $wpdb, $bp;

		return $wpdb->get_var( $wpdb->prepare( "SELECT slug FROM {$bp->groups->table_name} WHERE slug = %s", $slug ) );
	}

	/**
	 * Get the slug for a given group ID.
	 *
	 * @param int $group_id ID of the group.
	 * @return string|null The slug, if found. Otherwise null.
	 */
	public static function get_slug( $group_id ) {
		global $wpdb, $bp;

		return $wpdb->get_var( $wpdb->prepare( "SELECT slug FROM {$bp->groups->table_name} WHERE id = %d", $group_id ) );
	}

	/**
	 * Check whether a given group has any members.
	 *
	 * @param int $group_id ID of the group.
	 * @return bool True if the group has members, otherwise false.
	 */
	public static function has_members( $group_id ) {
		global $wpdb, $bp;

		$members = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM {$bp->groups->table_name_members} WHERE group_id = %d", $group_id ) );

		if ( empty( $members ) )
			return false;

		return true;
	}

	/**
	 * Check whether a group has outstanding membership requests.
	 *
	 * @param int $group_id ID of the group.
	 * @return int|null The number of outstanding requests, or null if
	 *         none are found.
	 */
	public static function has_membership_requests( $group_id ) {
		global $wpdb, $bp;

		return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM {$bp->groups->table_name_members} WHERE group_id = %d AND is_confirmed = 0", $group_id ) );
	}

	/**
	 * Get outstanding membership requests for a group.
	 *
	 * @param int $group_id ID of the group.
	 * @param int $limit Optional. Max number of results to return.
	 *        Default: null (no limit).
	 * @param int $page Optional. Page offset of results returned. Default:
	 *        null (no limit).
	 * @return array {
	 *     @type array $requests The requested page of located requests.
	 *     @type int $total Total number of requests outstanding for the
	 *           group.
	 * }
	 */
	public static function get_membership_requests( $group_id, $limit = null, $page = null ) {
		global $wpdb, $bp;

		if ( !empty( $limit ) && !empty( $page ) ) {
			$pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
		}

		$paged_requests = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$bp->groups->table_name_members} WHERE group_id = %d AND is_confirmed = 0 AND inviter_id = 0{$pag_sql}", $group_id ) );
		$total_requests = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM {$bp->groups->table_name_members} WHERE group_id = %d AND is_confirmed = 0 AND inviter_id = 0", $group_id ) );

		return array( 'requests' => $paged_requests, 'total' => $total_requests );
	}

	/**
	 * Query for groups.
	 *
	 * @see WP_Meta_Query::queries for a description of the 'meta_query'
	 *      parameter format.
	 *
	 * @param array {
	 *     Array of parameters. All items are optional.
	 *     @type string $type Optional. Shorthand for certain orderby/
	 *           order combinations. 'newest', 'active', 'popular',
	 *           'alphabetical', 'random'. When present, will override
	 *           orderby and order params. Default: null.
	 *     @type string $orderby Optional. Property to sort by.
	 *           'date_created', 'last_activity', 'total_member_count',
	 *           'name', 'random'. Default: 'date_created'.
	 *     @type string $order Optional. Sort order. 'ASC' or 'DESC'.
	 *           Default: 'DESC'.
	 *     @type int $per_page Optional. Number of items to return per page
	 *           of results. Default: null (no limit).
	 *     @type int $page Optional. Page offset of results to return.
	 *           Default: null (no limit).
	 *     @type int $user_id Optional. If provided, results will be limited
	 *           to groups of which the specified user is a member. Default:
	 *           null.
	 *     @type string $search_terms Optional. If provided, only groups
	 *           whose names or descriptions match the search terms will be
	 *           returned. Default: false.
	 *     @type array $meta_query Optional. An array of meta_query
	 *           conditions. See {@link WP_Meta_Query::queries} for
	 *           description.
	 *     @type array|string Optional. Array or comma-separated list of
	 *           group IDs. Results will be limited to groups within the
	 *           list. Default: false.
	 *     @type bool $populate_extras Whether to fetch additional
	 *           information (such as member count) about groups. Default:
	 *           true.
	 *     @type array|string Optional. Array or comma-separated list of
	 *           group IDs. Results will exclude the listed groups.
	 *           Default: false.
	 *     @type bool $show_hidden Whether to include hidden groups in
	 *           results. Default: false.
	 * }
	 * @return array {
	 *     @type array $groups Array of group objects returned by the
	 *           paginated query.
	 *     @type int $total Total count of all groups matching non-
	 *           paginated query params.
	 * }
	 */
	public static function get( $args = array() ) {
		global $wpdb, $bp;

		// Backward compatibility with old method of passing arguments
		if ( ! is_array( $args ) || func_num_args() > 1 ) {
			_deprecated_argument( __METHOD__, '1.7', sprintf( __( 'Arguments passed to %1$s should be in an associative array. See the inline documentation at %2$s for more details.', 'buddypress' ), __METHOD__, __FILE__ ) );

			$old_args_keys = array(
				0 => 'type',
				1 => 'per_page',
				2 => 'page',
				3 => 'user_id',
				4 => 'search_terms',
				5 => 'include',
				6 => 'populate_extras',
				7 => 'exclude',
				8 => 'show_hidden',
			);

			$func_args = func_get_args();
			$args      = bp_core_parse_args_array( $old_args_keys, $func_args );
		}

		$defaults = array(
			'type'            => null,
			'orderby'         => 'date_created',
			'order'           => 'DESC',
			'per_page'        => null,
			'page'            => null,
			'user_id'         => 0,
			'search_terms'    => false,
			'meta_query'      => false,
			'include'         => false,
			'populate_extras' => true,
			'exclude'         => false,
			'show_hidden'     => false,
		);

		$r = wp_parse_args( $args, $defaults );

		$sql       = array();
		$total_sql = array();

		$sql['select'] = "SELECT DISTINCT g.id, g.*, gm1.meta_value AS total_member_count, gm2.meta_value AS last_activity";
		$sql['from']   = " FROM {$bp->groups->table_name_groupmeta} gm1, {$bp->groups->table_name_groupmeta} gm2,";

		if ( ! empty( $r['user_id'] ) ) {
			$sql['members_from'] = " {$bp->groups->table_name_members} m,";
		}

		$sql['group_from'] = " {$bp->groups->table_name} g WHERE";

		if ( ! empty( $r['user_id'] ) ) {
			$sql['user_where'] = " g.id = m.group_id AND";
		}

		$sql['where'] = " g.id = gm1.group_id AND g.id = gm2.group_id AND gm2.meta_key = 'last_activity' AND gm1.meta_key = 'total_member_count'";

		if ( empty( $r['show_hidden'] ) ) {
			$sql['hidden'] = " AND g.status != 'hidden'";
		}

		if ( ! empty( $r['search_terms'] ) ) {
			$search_terms = esc_sql( like_escape( $r['search_terms'] ) );
			$sql['search'] = " AND ( g.name LIKE '%%{$search_terms}%%' OR g.description LIKE '%%{$search_terms}%%' )";
		}

		$meta_query_sql = self::get_meta_query_sql( $r['meta_query'] );

		if ( ! empty( $meta_query_sql['join'] ) ) {
			$sql['from'] .= $meta_query_sql['join'];
		}

		if ( ! empty( $meta_query_sql['where'] ) ) {
			$sql['meta'] = $meta_query_sql['where'];
		}

		if ( ! empty( $r['user_id'] ) ) {
			$sql['user'] = $wpdb->prepare( " AND m.user_id = %d AND m.is_confirmed = 1 AND m.is_banned = 0", $r['user_id'] );
		}

		if ( ! empty( $r['include'] ) ) {
			$include        = implode( ',', wp_parse_id_list( $r['include'] ) );
			$sql['include'] = " AND g.id IN ({$include})";
		}

		if ( ! empty( $r['exclude'] ) ) {
			$exclude        = implode( ',', wp_parse_id_list( $r['exclude'] ) );
			$sql['exclude'] = " AND g.id NOT IN ({$exclude})";
		}

		/** Order/orderby ********************************************/

		$order   = $r['order'];
		$orderby = $r['orderby'];

		// If a 'type' parameter was passed, parse it and overwrite
		// 'order' and 'orderby' params passed to the function
		if (  ! empty( $r['type'] ) ) {
			$order_orderby = self::convert_type_to_order_orderby( $r['type'] );

			// If an invalid type is passed, $order_orderby will be
			// an array with empty values. In this case, we stick
			// with the default values of $order and $orderby
			if ( ! empty( $order_orderby['order'] ) ) {
				$order = $order_orderby['order'];
			}

			if ( ! empty( $order_orderby['orderby'] ) ) {
				$orderby = $order_orderby['orderby'];
			}
		}

		// Sanitize 'order'
		$order = bp_esc_sql_order( $order );

		// Convert 'orderby' into the proper ORDER BY term
		$orderby = self::convert_orderby_to_order_by_term( $orderby );

		// Random order is a special case
		if ( 'rand()' === $orderby ) {
			$sql[] = "ORDER BY rand()";
		} else {
			$sql[] = "ORDER BY {$orderby} {$order}";
		}

		if ( ! empty( $r['per_page'] ) && ! empty( $r['page'] ) ) {
			$sql['pagination'] = $wpdb->prepare( "LIMIT %d, %d", intval( ( $r['page'] - 1 ) * $r['per_page']), intval( $r['per_page'] ) );
		}

		// Get paginated results
		$paged_groups_sql = apply_filters( 'bp_groups_get_paged_groups_sql', join( ' ', (array) $sql ), $sql, $r );
		$paged_groups     = $wpdb->get_results( $paged_groups_sql );

		$total_sql['select'] = "SELECT COUNT(DISTINCT g.id) FROM {$bp->groups->table_name} g, {$bp->groups->table_name_members} gm1, {$bp->groups->table_name_groupmeta} gm2";

		if ( ! empty( $r['user_id'] ) ) {
			$total_sql['select'] .= ", {$bp->groups->table_name_members} m";
		}

		if ( ! empty( $sql['hidden'] ) ) {
			$total_sql['where'][] = "g.status != 'hidden'";
		}

		if ( ! empty( $sql['search'] ) ) {
			$total_sql['where'][] = "( g.name LIKE '%%{$search_terms}%%' OR g.description LIKE '%%{$search_terms}%%' )";
		}

		if ( ! empty( $r['user_id'] ) ) {
			$total_sql['where'][] = $wpdb->prepare( "m.group_id = g.id AND m.user_id = %d AND m.is_confirmed = 1 AND m.is_banned = 0", $r['user_id'] );
		}

		// Temporary implementation of meta_query for total count
		// See #5099
		if ( ! empty( $meta_query_sql['where'] ) ) {
			// Join the groupmeta table
			$total_sql['select'] .= ", ". substr( $meta_query_sql['join'], 0, -2 );

			// Modify the meta_query clause from paged_sql for our syntax
			$meta_query_clause = preg_replace( '/^\s*AND/', '', $meta_query_sql['where'] );
			$total_sql['where'][] = $meta_query_clause;
		}

		// Already escaped in the paginated results block
		if ( ! empty( $include ) ) {
			$total_sql['where'][] = "g.id IN ({$include})";
		}

		// Already escaped in the paginated results block
		if ( ! empty( $exclude ) ) {
			$total_sql['where'][] = "g.id NOT IN ({$exclude})";
		}

		$total_sql['where'][] = "g.id = gm1.group_id";
		$total_sql['where'][] = "g.id = gm2.group_id";
		$total_sql['where'][] = "gm2.meta_key = 'last_activity'";

		$t_sql = $total_sql['select'];

		if ( ! empty( $total_sql['where'] ) ) {
			$t_sql .= " WHERE " . join( ' AND ', (array) $total_sql['where'] );
		}

		// Get total group results
		$total_groups_sql = apply_filters( 'bp_groups_get_total_groups_sql', $t_sql, $total_sql, $r );
		$total_groups     = $wpdb->get_var( $total_groups_sql );

		$group_ids = array();
		foreach ( (array) $paged_groups as $group ) {
			$group_ids[] = $group->id;
		}

		// Populate some extra information instead of querying each time in the loop
		if ( !empty( $r['populate_extras'] ) ) {
			$paged_groups = BP_Groups_Group::get_group_extras( $paged_groups, $group_ids, $r['type'] );
		}

		// Grab all groupmeta
		bp_groups_update_meta_cache( $group_ids );

		unset( $sql, $total_sql );

		return array( 'groups' => $paged_groups, 'total' => $total_groups );
	}

	/**
	 * Get the SQL for the 'meta_query' param in BP_Activity_Activity::get()
	 *
	 * We use WP_Meta_Query to do the heavy lifting of parsing the
	 * meta_query array and creating the necessary SQL clauses. However,
	 * since BP_Activity_Activity::get() builds its SQL differently than
	 * WP_Query, we have to alter the return value (stripping the leading
	 * AND keyword from the 'where' clause).
	 *
	 * @since BuddyPress (1.8.0)
	 * @access protected
	 *
	 * @param array $meta_query An array of meta_query filters. See the
	 *        documentation for {@link WP_Meta_Query} for details.
	 * @return array $sql_array 'join' and 'where' clauses.
	 */
	protected static function get_meta_query_sql( $meta_query = array() ) {
		global $wpdb;

		$sql_array = array(
			'join'  => '',
			'where' => '',
		);

		if ( ! empty( $meta_query ) ) {
			$groups_meta_query = new WP_Meta_Query( $meta_query );

			// WP_Meta_Query expects the table name at
			// $wpdb->group
			$wpdb->groupmeta = buddypress()->groups->table_name_groupmeta;

			$meta_sql = $groups_meta_query->get_sql( 'group', 'g', 'id' );

			// BP_Groups_Group::get uses the comma syntax for table
			// joins, which means that we have to do some regex to
			// convert the INNER JOIN and move the ON clause to a
			// WHERE condition
			//
			// @todo It may be better in the long run to refactor
			// the more general query syntax to accord better with
			// BP/WP convention
			preg_match_all( '/INNER JOIN (.*) ON/', $meta_sql['join'], $matches_a );
			preg_match_all( '/ON \((.*)\)/', $meta_sql['join'], $matches_b );

			if ( ! empty( $matches_a[1] ) && ! empty( $matches_b[1] ) ) {
				$sql_array['join']  = implode( ',', $matches_a[1] ). ', ';

				$sql_array['where'] = '';

				$meta_query_where_clauses = explode( "\n", $meta_sql['where'] );
				foreach( $matches_b[1] as $key => $group_id_clause ) {
					$sql_array['where'] .= ' ' . preg_replace( '/^(AND\s+[\(\s]+)/', '$1' . $group_id_clause . ' AND ', ltrim( $meta_query_where_clauses[ $key ] ) );
				}

			}
		}

		return $sql_array;
	}

	/**
	 * Convert the 'type' parameter to 'order' and 'orderby'.
	 *
	 * @since BuddyPress (1.8.0)
	 * @access protected
	 *
	 * @param string $type The 'type' shorthand param.
	 * @return array {
	 *	@type string $order SQL-friendly order string.
	 *	@type string $orderby SQL-friendly orderby column name.
	 * }
	 */
	protected static function convert_type_to_order_orderby( $type = '' ) {
		$order = $orderby = '';

		switch ( $type ) {
			case 'newest' :
				$order   = 'DESC';
				$orderby = 'date_created';
				break;

			case 'active' :
				$order   = 'DESC';
				$orderby = 'last_activity';
				break;

			case 'popular' :
				$order   = 'DESC';
				$orderby = 'total_member_count';
				break;

			case 'alphabetical' :
				$order   = 'ASC';
				$orderby = 'name';
				break;

			case 'random' :
				$order   = '';
				$orderby = 'random';
				break;
		}

		return array( 'order' => $order, 'orderby' => $orderby );
	}

	/**
	 * Convert the 'orderby' param into a proper SQL term/column.
	 *
	 * @since BuddyPress (1.8.0)
	 * @access protected
	 *
	 * @param string $orderby Orderby term as passed to get().
	 * @return string $order_by_term SQL-friendly orderby term.
	 */
	protected static function convert_orderby_to_order_by_term( $orderby ) {
		$order_by_term = '';

		switch ( $orderby ) {
			case 'date_created' :
			default :
				$order_by_term = 'g.date_created';
				break;

			case 'last_activity' :
				$order_by_term = 'last_activity';
				break;

			case 'total_member_count' :
				$order_by_term = 'CONVERT(gm1.meta_value, SIGNED)';
				break;

			case 'name' :
				$order_by_term = 'g.name';
				break;

			case 'random' :
				$order_by_term = 'rand()';
				break;
		}

		return $order_by_term;
	}

	/**
	 * Get a list of groups, sorted by those that have the most legacy forum topics.
	 *
	 * @param int $limit Optional. The max number of results to return.
	 *        Default: null (no limit).
	 * @param int $page Optional. The page offset of results to return.
	 *        Default: null (no limit).
	 * @param int $user_id Optional. If present, groups will be limited to
	 *        those of which the specified user is a member.
	 * @param string $search_terms Optional. Limit groups to those whose
	 *        name or description field contain the search string.
	 * @param bool $populate_extras Optional. Whether to fetch extra
	 *        information about the groups. Default: true.
	 * @param string|array Optional. Array or comma-separated list of group
	 *        IDs to exclude from results.
	 * @return array {
	 *     @type array $groups Array of group objects returned by the
	 *           paginated query.
	 *     @type int $total Total count of all groups matching non-
	 *           paginated query params.
	 * }
	 */
	public static function get_by_most_forum_topics( $limit = null, $page = null, $user_id = 0, $search_terms = false, $populate_extras = true, $exclude = false ) {
		global $wpdb, $bp, $bbdb;

		if ( empty( $bbdb ) )
			do_action( 'bbpress_init' );

		if ( !empty( $limit ) && !empty( $page ) ) {
			$pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
		}

		if ( !is_user_logged_in() || ( !bp_current_user_can( 'bp_moderate' ) && ( $user_id != bp_loggedin_user_id() ) ) )
			$hidden_sql = " AND g.status != 'hidden'";

		if ( !empty( $search_terms ) ) {
			$search_terms = esc_sql( like_escape( $search_terms ) );
			$search_sql = " AND ( g.name LIKE '%%{$search_terms}%%' OR g.description LIKE '%%{$search_terms}%%' )";
		}

		if ( !empty( $exclude ) ) {
			$exclude     = implode( ',', wp_parse_id_list( $exclude ) );
			$exclude_sql = " AND g.id NOT IN ({$exclude})";
		}

		if ( !empty( $user_id ) ) {
			$user_id      = absint( esc_sql( $user_id ) );
			$paged_groups = $wpdb->get_results( "SELECT DISTINCT g.*, gm1.meta_value as total_member_count, gm2.meta_value as last_activity FROM {$bp->groups->table_name_groupmeta} gm1, {$bp->groups->table_name_groupmeta} gm2, {$bp->groups->table_name_groupmeta} gm3, {$bp->groups->table_name_members} m, {$bbdb->forums} f, {$bp->groups->table_name} g WHERE g.id = m.group_id AND g.id = gm1.group_id AND g.id = gm2.group_id AND g.id = gm3.group_id AND gm2.meta_key = 'last_activity' AND gm1.meta_key = 'total_member_count' AND (gm3.meta_key = 'forum_id' AND gm3.meta_value = f.forum_id) AND f.topics > 0 {$hidden_sql} {$search_sql} AND m.user_id = {$user_id} AND m.is_confirmed = 1 AND m.is_banned = 0 {$exclude_sql} ORDER BY f.topics DESC {$pag_sql}" );
			$total_groups = $wpdb->get_var( "SELECT COUNT(DISTINCT g.id) FROM {$bp->groups->table_name_groupmeta} gm1, {$bp->groups->table_name_groupmeta} gm2, {$bp->groups->table_name_groupmeta} gm3, {$bbdb->forums} f, {$bp->groups->table_name} g WHERE g.id = gm1.group_id AND g.id = gm2.group_id AND g.id = gm3.group_id AND gm2.meta_key = 'last_activity' AND gm1.meta_key = 'total_member_count' AND (gm3.meta_key = 'forum_id' AND gm3.meta_value = f.forum_id) AND f.topics > 0 {$hidden_sql} {$search_sql} AND m.user_id = {$user_id} AND m.is_confirmed = 1 AND m.is_banned = 0 {$exclude_sql}" );
		} else {
			$paged_groups = $wpdb->get_results( "SELECT DISTINCT g.*, gm1.meta_value as total_member_count, gm2.meta_value as last_activity FROM {$bp->groups->table_name_groupmeta} gm1, {$bp->groups->table_name_groupmeta} gm2, {$bp->groups->table_name_groupmeta} gm3, {$bbdb->forums} f, {$bp->groups->table_name} g WHERE g.id = gm1.group_id AND g.id = gm2.group_id AND g.id = gm3.group_id AND gm2.meta_key = 'last_activity' AND gm1.meta_key = 'total_member_count' AND (gm3.meta_key = 'forum_id' AND gm3.meta_value = f.forum_id) AND f.topics > 0 {$hidden_sql} {$search_sql} {$exclude_sql} ORDER BY f.topics DESC {$pag_sql}" );
			$total_groups = $wpdb->get_var( "SELECT COUNT(DISTINCT g.id) FROM {$bp->groups->table_name_groupmeta} gm1, {$bp->groups->table_name_groupmeta} gm2, {$bp->groups->table_name_groupmeta} gm3, {$bbdb->forums} f, {$bp->groups->table_name} g WHERE g.id = gm1.group_id AND g.id = gm2.group_id AND g.id = gm3.group_id AND gm2.meta_key = 'last_activity' AND gm1.meta_key = 'total_member_count' AND (gm3.meta_key = 'forum_id' AND gm3.meta_value = f.forum_id) AND f.topics > 0 {$hidden_sql} {$search_sql} {$exclude_sql}" );
		}

		if ( !empty( $populate_extras ) ) {
			foreach ( (array) $paged_groups as $group ) {
				$group_ids[] = $group->id;
			}
			$paged_groups = BP_Groups_Group::get_group_extras( $paged_groups, $group_ids, 'newest' );
		}

		return array( 'groups' => $paged_groups, 'total' => $total_groups );
	}

	/**
	 * Get a list of groups, sorted by those that have the most legacy forum posts.
	 *
	 * @param int $limit Optional. The max number of results to return.
	 *        Default: null (no limit).
	 * @param int $page Optional. The page offset of results to return.
	 *        Default: null (no limit).
	 * @param int $user_id Optional. If present, groups will be limited to
	 *        those of which the specified user is a member.
	 * @param string $search_terms Optional. Limit groups to those whose
	 *        name or description field contain the search string.
	 * @param bool $populate_extras Optional. Whether to fetch extra
	 *        information about the groups. Default: true.
	 * @param string|array Optional. Array or comma-separated list of group
	 *        IDs to exclude from results.
	 * @return array {
	 *     @type array $groups Array of group objects returned by the
	 *           paginated query.
	 *     @type int $total Total count of all groups matching non-
	 *           paginated query params.
	 * }
	 */
	public static function get_by_most_forum_posts( $limit = null, $page = null, $search_terms = false, $populate_extras = true, $exclude = false ) {
		global $wpdb, $bp, $bbdb;

		if ( empty( $bbdb ) )
			do_action( 'bbpress_init' );

		if ( !empty( $limit ) && !empty( $page ) ) {
			$pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
		}

		if ( !is_user_logged_in() || ( !bp_current_user_can( 'bp_moderate' ) && ( $user_id != bp_loggedin_user_id() ) ) )
			$hidden_sql = " AND g.status != 'hidden'";

		if ( !empty( $search_terms ) ) {
			$search_terms = esc_sql( like_escape( $search_terms ) );
			$search_sql = " AND ( g.name LIKE '%%{$search_terms}%%' OR g.description LIKE '%%{$search_terms}%%' )";
		}

		if ( !empty( $exclude ) ) {
			$exclude     = implode( ',', wp_parse_id_list( $exclude ) );
			$exclude_sql = " AND g.id NOT IN ({$exclude})";
		}

		if ( !empty( $user_id ) ) {
			$user_id = esc_sql( $user_id );
			$paged_groups = $wpdb->get_results( "SELECT DISTINCT g.*, gm1.meta_value as total_member_count, gm2.meta_value as last_activity FROM {$bp->groups->table_name_groupmeta} gm1, {$bp->groups->table_name_groupmeta} gm2, {$bp->groups->table_name_groupmeta} gm3, {$bp->groups->table_name_members} m, {$bbdb->forums} f, {$bp->groups->table_name} g WHERE g.id = m.group_id AND g.id = gm1.group_id AND g.id = gm2.group_id AND g.id = gm3.group_id AND gm2.meta_key = 'last_activity' AND gm1.meta_key = 'total_member_count' AND (gm3.meta_key = 'forum_id' AND gm3.meta_value = f.forum_id) {$hidden_sql} {$search_sql} AND m.user_id = {$user_id} AND m.is_confirmed = 1 AND m.is_banned = 0 {$exclude_sql} ORDER BY f.posts ASC {$pag_sql}" );
			$total_groups = $wpdb->get_results( "SELECT COUNT(DISTINCT g.id) FROM {$bp->groups->table_name_groupmeta} gm1, {$bp->groups->table_name_groupmeta} gm2, {$bp->groups->table_name_groupmeta} gm3, {$bp->groups->table_name_members} m, {$bbdb->forums} f, {$bp->groups->table_name} g WHERE g.id = m.group_id AND g.id = gm1.group_id AND g.id = gm2.group_id AND g.id = gm3.group_id AND gm2.meta_key = 'last_activity' AND gm1.meta_key = 'total_member_count' AND (gm3.meta_key = 'forum_id' AND gm3.meta_value = f.forum_id) AND f.posts > 0 {$hidden_sql} {$search_sql} AND m.user_id = {$user_id} AND m.is_confirmed = 1 AND m.is_banned = 0 {$exclude_sql} " );
		} else {
			$paged_groups = $wpdb->get_results( "SELECT DISTINCT g.*, gm1.meta_value as total_member_count, gm2.meta_value as last_activity FROM {$bp->groups->table_name_groupmeta} gm1, {$bp->groups->table_name_groupmeta} gm2, {$bp->groups->table_name_groupmeta} gm3, {$bbdb->forums} f, {$bp->groups->table_name} g WHERE g.id = gm1.group_id AND g.id = gm2.group_id AND g.id = gm3.group_id AND gm2.meta_key = 'last_activity' AND gm1.meta_key = 'total_member_count' AND (gm3.meta_key = 'forum_id' AND gm3.meta_value = f.forum_id) AND f.posts > 0 {$hidden_sql} {$search_sql} {$exclude_sql} ORDER BY f.posts ASC {$pag_sql}" );
			$total_groups = $wpdb->get_var( "SELECT COUNT(DISTINCT g.id) FROM {$bp->groups->table_name_groupmeta} gm1, {$bp->groups->table_name_groupmeta} gm2, {$bp->groups->table_name_groupmeta} gm3, {$bbdb->forums} f, {$bp->groups->table_name} g WHERE g.id = gm1.group_id AND g.id = gm2.group_id AND g.id = gm3.group_id AND gm2.meta_key = 'last_activity' AND gm1.meta_key = 'total_member_count' AND (gm3.meta_key = 'forum_id' AND gm3.meta_value = f.forum_id) {$hidden_sql} {$search_sql} {$exclude_sql}" );
		}

		if ( !empty( $populate_extras ) ) {
			foreach ( (array) $paged_groups as $group ) {
				$group_ids[] = $group->id;
			}
			$paged_groups = BP_Groups_Group::get_group_extras( $paged_groups, $group_ids, 'newest' );
		}

		return array( 'groups' => $paged_groups, 'total' => $total_groups );
	}

	/**
	 * Get a list of groups whose names start with a given letter.
	 *
	 * @param string $letter The letter.
	 * @param int $limit Optional. The max number of results to return.
	 *        Default: null (no limit).
	 * @param int $page Optional. The page offset of results to return.
	 *        Default: null (no limit).
	 * @param bool $populate_extras Optional. Whether to fetch extra
	 *        information about the groups. Default: true.
	 * @param string|array Optional. Array or comma-separated list of group
	 *        IDs to exclude from results.
	 * @return array {
	 *     @type array $groups Array of group objects returned by the
	 *           paginated query.
	 *     @type int $total Total count of all groups matching non-
	 *           paginated query params.
	 * }
	 */
	public static function get_by_letter( $letter, $limit = null, $page = null, $populate_extras = true, $exclude = false ) {
		global $wpdb, $bp;

		$pag_sql = $hidden_sql = $exclude_sql = '';

		// Multibyte compliance
		if ( function_exists( 'mb_strlen' ) ) {
			if ( mb_strlen( $letter, 'UTF-8' ) > 1 || is_numeric( $letter ) || !$letter ) {
				return false;
			}
		} else {
			if ( strlen( $letter ) > 1 || is_numeric( $letter ) || !$letter ) {
				return false;
			}
		}

		if ( !empty( $exclude ) ) {
			$exclude     = implode( ',', wp_parse_id_list( $exclude ) );
			$exclude_sql = " AND g.id NOT IN ({$exclude})";
		}

		if ( !bp_current_user_can( 'bp_moderate' ) )
			$hidden_sql = " AND status != 'hidden'";

		$letter = esc_sql( like_escape( $letter ) );

		if ( !empty( $limit ) && !empty( $page ) ) {
			$pag_sql      = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
		}

		$total_groups = $wpdb->get_var( "SELECT COUNT(DISTINCT g.id) FROM {$bp->groups->table_name_groupmeta} gm1, {$bp->groups->table_name_groupmeta} gm2, {$bp->groups->table_name} g WHERE g.id = gm1.group_id AND g.id = gm2.group_id AND gm2.meta_key = 'last_activity' AND gm1.meta_key = 'total_member_count' AND g.name LIKE '{$letter}%%' {$hidden_sql} {$exclude_sql}" );

		$paged_groups = $wpdb->get_results( "SELECT g.*, gm1.meta_value as total_member_count, gm2.meta_value as last_activity FROM {$bp->groups->table_name_groupmeta} gm1, {$bp->groups->table_name_groupmeta} gm2, {$bp->groups->table_name} g WHERE g.id = gm1.group_id AND g.id = gm2.group_id AND gm2.meta_key = 'last_activity' AND gm1.meta_key = 'total_member_count' AND g.name LIKE '{$letter}%%' {$hidden_sql} {$exclude_sql} ORDER BY g.name ASC {$pag_sql}" );

		if ( !empty( $populate_extras ) ) {
			foreach ( (array) $paged_groups as $group ) {
				$group_ids[] = $group->id;
			}
			$paged_groups = BP_Groups_Group::get_group_extras( $paged_groups, $group_ids, 'newest' );
		}

		return array( 'groups' => $paged_groups, 'total' => $total_groups );
	}

	/**
	 * Get a list of random groups.
	 *
	 * Use BP_Groups_Group::get() with 'type' = 'random' instead.
	 *
	 * @param int $limit Optional. The max number of results to return.
	 *        Default: null (no limit).
	 * @param int $page Optional. The page offset of results to return.
	 *        Default: null (no limit).
	 * @param int $user_id Optional. If present, groups will be limited to
	 *        those of which the specified user is a member.
	 * @param string $search_terms Optional. Limit groups to those whose
	 *        name or description field contain the search string.
	 * @param bool $populate_extras Optional. Whether to fetch extra
	 *        information about the groups. Default: true.
	 * @param string|array Optional. Array or comma-separated list of group
	 *        IDs to exclude from results.
	 * @return array {
	 *     @type array $groups Array of group objects returned by the
	 *           paginated query.
	 *     @type int $total Total count of all groups matching non-
	 *           paginated query params.
	 * }
	 */
	public static function get_random( $limit = null, $page = null, $user_id = 0, $search_terms = false, $populate_extras = true, $exclude = false ) {
		global $wpdb, $bp;

		$pag_sql = $hidden_sql = $search_sql = $exclude_sql = '';

		if ( !empty( $limit ) && !empty( $page ) )
			$pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );

		if ( !is_user_logged_in() || ( !bp_current_user_can( 'bp_moderate' ) && ( $user_id != bp_loggedin_user_id() ) ) )
			$hidden_sql = "AND g.status != 'hidden'";

		if ( !empty( $search_terms ) ) {
			$search_terms = esc_sql( like_escape( $search_terms ) );
			$search_sql = " AND ( g.name LIKE '%%{$search_terms}%%' OR g.description LIKE '%%{$search_terms}%%' )";
		}

		if ( !empty( $exclude ) ) {
			$exclude     = wp_parse_id_list( $exclude );
			$exclude     = esc_sql( implode( ',', $exclude ) );
			$exclude_sql = " AND g.id NOT IN ({$exclude})";
		}

		if ( !empty( $user_id ) ) {
			$user_id = esc_sql( $user_id );
			$paged_groups = $wpdb->get_results( "SELECT g.*, gm1.meta_value as total_member_count, gm2.meta_value as last_activity FROM {$bp->groups->table_name_groupmeta} gm1, {$bp->groups->table_name_groupmeta} gm2, {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE g.id = m.group_id AND g.id = gm1.group_id AND g.id = gm2.group_id AND gm2.meta_key = 'last_activity' AND gm1.meta_key = 'total_member_count' {$hidden_sql} {$search_sql} AND m.user_id = {$user_id} AND m.is_confirmed = 1 AND m.is_banned = 0 {$exclude_sql} ORDER BY rand() {$pag_sql}" );
			$total_groups = $wpdb->get_var( "SELECT COUNT(DISTINCT m.group_id) FROM {$bp->groups->table_name_members} m LEFT JOIN {$bp->groups->table_name_groupmeta} gm ON m.group_id = gm.group_id INNER JOIN {$bp->groups->table_name} g ON m.group_id = g.id WHERE gm.meta_key = 'last_activity'{$hidden_sql} {$search_sql} AND m.user_id = {$user_id} AND m.is_confirmed = 1 AND m.is_banned = 0 {$exclude_sql}" );
		} else {
			$paged_groups = $wpdb->get_results( "SELECT g.*, gm1.meta_value as total_member_count, gm2.meta_value as last_activity FROM {$bp->groups->table_name_groupmeta} gm1, {$bp->groups->table_name_groupmeta} gm2, {$bp->groups->table_name} g WHERE g.id = gm1.group_id AND g.id = gm2.group_id AND gm2.meta_key = 'last_activity' AND gm1.meta_key = 'total_member_count' {$hidden_sql} {$search_sql} {$exclude_sql} ORDER BY rand() {$pag_sql}" );
			$total_groups = $wpdb->get_var( "SELECT COUNT(DISTINCT g.id) FROM {$bp->groups->table_name_groupmeta} gm INNER JOIN {$bp->groups->table_name} g ON gm.group_id = g.id WHERE gm.meta_key = 'last_activity'{$hidden_sql} {$search_sql} {$exclude_sql}" );
		}

		if ( !empty( $populate_extras ) ) {
			foreach ( (array) $paged_groups as $group ) {
				$group_ids[] = $group->id;
			}
			$paged_groups = BP_Groups_Group::get_group_extras( $paged_groups, $group_ids, 'newest' );
		}

		return array( 'groups' => $paged_groups, 'total' => $total_groups );
	}

	/**
	 * Fetch extra data for a list of groups.
	 *
	 * This method is used throughout the class, by methods that take a
	 * $populate_extras parameter.
	 *
	 * Data fetched:
	 *
	 *     - Logged-in user's status within each group (is_member,
	 *       is_confirmed, is_pending, is_banned)
	 *
	 * @param array $paged_groups Array of groups.
	 * @param string|array Array or comma-separated list of IDs matching
	 *        $paged_groups.
	 * @param string $type Not used.
	 * @return array $paged_groups
	 */
	public static function get_group_extras( &$paged_groups, &$group_ids, $type = false ) {
		global $bp, $wpdb;

		if ( empty( $group_ids ) )
			return $paged_groups;

		// Sanitize group IDs
		$group_ids = implode( ',', wp_parse_id_list( $group_ids ) );

		// Fetch the logged-in user's status within each group
		$user_status_results = $wpdb->get_results( $wpdb->prepare( "SELECT group_id, is_confirmed, invite_sent FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id IN ( {$group_ids} ) AND is_banned = 0", bp_loggedin_user_id() ) );

		// Reindex
		$user_status = array();
		foreach ( $user_status_results as $user_status_result ) {
			$user_status[ $user_status_result->group_id ] = $user_status_result;
		}

		for ( $i = 0, $count = count( $paged_groups ); $i < $count; ++$i ) {
			$is_member = $is_invited = $is_pending = '0';
			$gid = $paged_groups[ $i ]->id;

			if ( isset( $user_status[ $gid ] ) ) {

				// is_confirmed means the user is a member
				if ( $user_status[ $gid ]->is_confirmed ) {
					$is_member = '1';

				// invite_sent means the user has been invited
				} else if ( $user_status[ $gid ]->invite_sent ) {
					$is_invited = '1';

				// User has sent request, but has not been confirmed
				} else {
					$is_pending = '1';
				}
			}

			$paged_groups[ $i ]->is_member = $is_member;
			$paged_groups[ $i ]->is_invited = $is_invited;
			$paged_groups[ $i ]->is_pending = $is_pending;
		}

		$user_banned = $wpdb->get_col( $wpdb->prepare( "SELECT group_id FROM {$bp->groups->table_name_members} WHERE is_banned = 1 AND user_id = %d AND group_id IN ( {$group_ids} )", bp_loggedin_user_id() ) );
		for ( $i = 0, $count = count( $paged_groups ); $i < $count; ++$i ) {
			$paged_groups[$i]->is_banned = false;

			foreach ( (array) $user_banned as $group_id ) {
				if ( $group_id == $paged_groups[$i]->id ) {
					$paged_groups[$i]->is_banned = true;
				}
			}
		}

		return $paged_groups;
	}

	/**
	 * Delete all invitations to a given group.
	 *
	 * @param int $group_id ID of the group whose invitations are being
	 *        deleted.
	 * @return int|null Number of rows records deleted on success, null on
	 *         failure.
	 */
	public static function delete_all_invites( $group_id ) {
		global $wpdb, $bp;

		return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->groups->table_name_members} WHERE group_id = %d AND invite_sent = 1", $group_id ) );
	}

	/**
	 * Get a total group count for the site.
	 *
	 * Will include hidden groups in the count only if
	 * current_user_can( 'bp_moderate' ).
	 *
	 * @return int Group count.
	 */
	public static function get_total_group_count() {
		global $wpdb, $bp;

		$hidden_sql = '';
		if ( !bp_current_user_can( 'bp_moderate' ) )
			$hidden_sql = "WHERE status != 'hidden'";

		return $wpdb->get_var( "SELECT COUNT(id) FROM {$bp->groups->table_name} {$hidden_sql}" );
	}

	/**
	 * Get global count of forum topics in public groups (legacy forums).
	 *
	 * @param $type Optional. If 'unreplied', count will be limited to
	 *        those topics that have received no replies.
	 * @return int Forum topic count.
	 */
	public static function get_global_forum_topic_count( $type ) {
		global $bbdb, $wpdb, $bp;

		if ( 'unreplied' == $type )
			$bp->groups->filter_sql = ' AND t.topic_posts = 1';

		// https://buddypress.trac.wordpress.org/ticket/4306
		$extra_sql = apply_filters( 'get_global_forum_topic_count_extra_sql', $bp->groups->filter_sql, $type );

		// Make sure the $extra_sql begins with an AND
		if ( 'AND' != substr( trim( strtoupper( $extra_sql ) ), 0, 3 ) )
			$extra_sql = ' AND ' . $extra_sql;

		return $wpdb->get_var( "SELECT COUNT(t.topic_id) FROM {$bbdb->topics} AS t, {$bp->groups->table_name} AS g LEFT JOIN {$bp->groups->table_name_groupmeta} AS gm ON g.id = gm.group_id WHERE (gm.meta_key = 'forum_id' AND gm.meta_value = t.forum_id) AND g.status = 'public' AND t.topic_status = '0' AND t.topic_sticky != '2' {$extra_sql} " );
	}

	/**
	 * Get the member count for a group.
	 *
	 * @param int $group_id Group ID.
	 * @return int Count of confirmed members for the group.
	 */
	public static function get_total_member_count( $group_id ) {
		global $wpdb, $bp;

		return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM {$bp->groups->table_name_members} WHERE group_id = %d AND is_confirmed = 1 AND is_banned = 0", $group_id ) );
	}

	/**
	 * Get a total count of all topics of a given status, across groups/forums
	 *
	 * @since BuddyPress (1.5.0)
	 *
	 * @param string $status Which group type to count. 'public', 'private',
	 *        'hidden', or 'all'. Default: 'public'.
	 * @return int The topic count
	 */
	public static function get_global_topic_count( $status = 'public', $search_terms = false ) {
		global $bbdb, $wpdb, $bp;

		switch ( $status ) {
			case 'all' :
				$status_sql = '';
				break;

			case 'hidden' :
				$status_sql = "AND g.status = 'hidden'";
				break;

			case 'private' :
				$status_sql = "AND g.status = 'private'";
				break;

			case 'public' :
			default :
				$status_sql = "AND g.status = 'public'";
				break;
		}

		$sql = array();

		$sql['select'] = "SELECT COUNT(t.topic_id)";
		$sql['from']   = "FROM {$bbdb->topics} AS t INNER JOIN {$bp->groups->table_name_groupmeta} AS gm ON t.forum_id = gm.meta_value INNER JOIN {$bp->groups->table_name} AS g ON gm.group_id = g.id";
		$sql['where']  = "WHERE gm.meta_key = 'forum_id' {$status_sql} AND t.topic_status = '0' AND t.topic_sticky != '2'";

		if ( !empty( $search_terms ) ) {
			$st = esc_sql( like_escape( $search_terms ) );
			$sql['where'] .= " AND (  t.topic_title LIKE '%{$st}%' )";
		}

		return $wpdb->get_var( implode( ' ', $sql ) );
	}

	/**
	 * Get an array containing ids for each group type.
	 *
	 * A bit of a kludge workaround for some issues
	 * with bp_has_groups().
	 *
	 * @since BuddyPress (1.7.0)
	 *
	 * @return array
	 */
	public static function get_group_type_ids() {
		global $wpdb, $bp;

		$ids = array();

		$ids['all']     = $wpdb->get_col( "SELECT id FROM {$bp->groups->table_name}" );
		$ids['public']  = $wpdb->get_col( "SELECT id FROM {$bp->groups->table_name} WHERE status = 'public'" );
		$ids['private'] = $wpdb->get_col( "SELECT id FROM {$bp->groups->table_name} WHERE status = 'private'" );
		$ids['hidden']  = $wpdb->get_col( "SELECT id FROM {$bp->groups->table_name} WHERE status = 'hidden'" );

		return $ids;
	}
}

/**
 * Query for the members of a group.
 *
 * @since BuddyPress (1.8.0)
 */
class BP_Group_Member_Query extends BP_User_Query {

	/**
	 * Array of group member ids, cached to prevent redundant lookups.
	 *
	 * @since BuddyPress (1.8.1)
	 * @var null|array Null if not yet defined, otherwise an array of ints.
	 */
	protected $group_member_ids;

	/**
	 * Set up action hooks.
	 *
	 * @since BuddyPress (1.8.0)
	 */
	public function setup_hooks() {
		// Take this early opportunity to set the default 'type' param
		// to 'last_modified', which will ensure that BP_User_Query
		// trusts our order and does not try to apply its own
		if ( empty( $this->query_vars_raw['type'] ) ) {
			$this->query_vars_raw['type'] = 'last_modified';
		}

		// Set the sort order
		add_action( 'bp_pre_user_query', array( $this, 'set_orderby' ) );

		// Set up our populate_extras method
		add_action( 'bp_user_query_populate_extras', array( $this, 'populate_group_member_extras' ), 10, 2 );
	}

	/**
	 * Get a list of user_ids to include in the IN clause of the main query.
	 *
	 * Overrides BP_User_Query::get_include_ids(), adding our additional
	 * group-member logic.
	 *
	 * @since BuddyPress (1.8.0)
	 *
	 * @param array $include Existing group IDs in the $include parameter,
	 *        as calculated in BP_User_Query.
	 * @return array
	 */
	public function get_include_ids( $include = array() ) {
		// The following args are specific to group member queries, and
		// are not present in the query_vars of a normal BP_User_Query.
		// We loop through to make sure that defaults are set (though
		// values passed to the constructor will, as usual, override
		// these defaults).
		$this->query_vars = wp_parse_args( $this->query_vars, array(
			'group_id'     => 0,
			'group_role'   => array( 'member' ),
			'is_confirmed' => true,
		) );

		$group_member_ids = $this->get_group_member_ids();

		// If the group member query returned no users, bail with an
		// array that will guarantee no matches for BP_User_Query
		if ( empty( $group_member_ids ) ) {
			return array( 0 );
		}

		if ( ! empty( $include ) ) {
			$group_member_ids = array_intersect( $include, $group_member_ids );
		}

		return $group_member_ids;
	}

	/**
	 * Get the members of the queried group.
	 *
	 * @since BuddyPress (1.8.0)
	 *
	 * @return array $ids User IDs of relevant group member ids.
	 */
	protected function get_group_member_ids() {
		global $wpdb;

		if ( is_array( $this->group_member_ids ) ) {
			return $this->group_member_ids;
		}

		$bp  = buddypress();
		$sql = array(
			'select'  => "SELECT user_id FROM {$bp->groups->table_name_members}",
			'where'   => array(),
			'orderby' => '',
			'order'   => '',
			'limit'   => '',
		);

		/** WHERE clauses *****************************************************/

		// Group id
		$sql['where'][] = $wpdb->prepare( "group_id = %d", $this->query_vars['group_id'] );

		// is_confirmed
		$is_confirmed = ! empty( $this->query_vars['is_confirmed'] ) ? 1 : 0;
		$sql['where'][] = $wpdb->prepare( "is_confirmed = %d", $is_confirmed );

		// Role information is stored as follows: admins have
		// is_admin = 1, mods have is_mod = 1, banned have is_banned =
		// 1, and members have all three set to 0.
		$roles = !empty( $this->query_vars['group_role'] ) ? $this->query_vars['group_role'] : array();
		if ( is_string( $roles ) ) {
			$roles = explode( ',', $roles );
		}

		// Sanitize: Only 'admin', 'mod', 'member', and 'banned' are valid
		$allowed_roles = array( 'admin', 'mod', 'member', 'banned' );
		foreach ( $roles as $role_key => $role_value ) {
			if ( ! in_array( $role_value, $allowed_roles ) ) {
				unset( $roles[ $role_key ] );
			}
		}

		$roles = array_unique( $roles );

		// When querying for a set of roles containing 'member' (for
		// which there is no dedicated is_ column), figure out a list
		// of columns *not* to match
		$roles_sql = '';
		if ( in_array( 'member', $roles ) ) {
			$role_columns = array();
			foreach ( array_diff( $allowed_roles, $roles ) as $excluded_role ) {
				$role_columns[] = 'is_' . $excluded_role . ' = 0';
			}

			if ( ! empty( $role_columns ) ) {
				$roles_sql = '(' . implode( ' AND ', $role_columns ) . ')';
			}

		// When querying for a set of roles *not* containing 'member',
		// simply construct a list of is_* = 1 clauses
		} else {
			$role_columns = array();
			foreach ( $roles as $role ) {
				$role_columns[] = 'is_' . $role . ' = 1';
			}

			if ( ! empty( $role_columns ) ) {
				$roles_sql = '(' . implode( ' OR ', $role_columns ) . ')';
			}
		}

		if ( ! empty( $roles_sql ) ) {
			$sql['where'][] = $roles_sql;
		}

		$sql['where'] = ! empty( $sql['where'] ) ? 'WHERE ' . implode( ' AND ', $sql['where'] ) : '';

		/** ORDER BY clause ***************************************************/

		// @todo For now, mimicking legacy behavior of
		// bp_group_has_members(), which has us order by date_modified
		// only. Should abstract it in the future
		$sql['orderby'] = "ORDER BY date_modified";
		$sql['order']   = "DESC";

		/** LIMIT clause ******************************************************/
		$this->group_member_ids = $wpdb->get_col( "{$sql['select']} {$sql['where']} {$sql['orderby']} {$sql['order']} {$sql['limit']}" );

		return $this->group_member_ids;
	}

	/**
	 * Tell BP_User_Query to order by the order of our query results.
	 *
	 * This implementation assumes the 'last_modified' sort order
	 * hardcoded in BP_Group_Member_Query::get_group_member_ids().
	 *
	 * @param BP_User_Query $query BP_User_Query object.
	 */
	public function set_orderby( $query ) {
		$gm_ids = $this->get_group_member_ids();
		if ( empty( $gm_ids ) ) {
			$gm_ids = array( 0 );
		}

		// The first param in the FIELD() clause is the sort column id
		$gm_ids = array_merge( array( 'u.id' ), wp_parse_id_list( $gm_ids ) );
		$gm_ids_sql = implode( ',', $gm_ids );

		$query->uid_clauses['orderby'] = "ORDER BY FIELD(" . $gm_ids_sql . ")";

		// Prevent this filter from running on future BP_User_Query
		// instances on the same page
		remove_action( 'bp_pre_user_query', array( $this, 'set_orderby' ) );
	}

	/**
	 * Fetch additional data required in bp_group_has_members() loops.
	 *
	 * Additional data fetched:
	 *
	 *      - is_banned
	 *      - date_modified
	 *
	 * @since BuddyPress (1.8.0)
	 *
	 * @param object $query BP_User_Query object. Because we're filtering
	 *   the current object, we use $this inside of the method instead
	 * @param string $user_ids_sql Sanitized, comma-separated string of
	 *   the user ids returned by the main query
	 */
	public function populate_group_member_extras( $query, $user_ids_sql ) {
		global $wpdb;

		$bp     = buddypress();
		$extras = $wpdb->get_results( $wpdb->prepare( "SELECT user_id, date_modified, is_banned FROM {$bp->groups->table_name_members} WHERE user_id IN ({$user_ids_sql}) AND group_id = %d", $this->query_vars['group_id'] ) );

		foreach ( (array) $extras as $extra ) {
			if ( isset( $this->results[ $extra->user_id ] ) ) {
				// user_id is provided for backward compatibility
				$this->results[ $extra->user_id ]->user_id       = (int) $extra->user_id;
				$this->results[ $extra->user_id ]->is_banned     = (int) $extra->is_banned;
				$this->results[ $extra->user_id ]->date_modified = $extra->date_modified;
			}
		}

		// Don't filter other BP_User_Query objects on the same page
		remove_action( 'bp_user_query_populate_extras', array( $this, 'populate_group_member_extras' ), 10, 2 );
	}
}

/**
 * BuddyPress Group Membership objects.
 */
class BP_Groups_Member {

	/**
	 * ID of the membership.
	 *
	 * @access public
	 * @var int
	 */
	var $id;

	/**
	 * ID of the group associated with the membership.
	 *
	 * @access public
	 * @var int
	 */
	var $group_id;

	/**
	 * ID of the user associated with the membership.
	 *
	 * @access public
	 * @var int
	 */
	var $user_id;

	/**
	 * ID of the user whose invitation initiated the membership.
	 *
	 * @access public
	 * @var int
	 */
	var $inviter_id;

	/**
	 * Whether the member is an admin of the group.
	 *
	 * @access public
	 * @var int
	 */
	var $is_admin;

	/**
	 * Whether the member is a mod of the group.
	 *
	 * @access public
	 * @var int
	 */
	var $is_mod;

	/**
	 * Whether the member is banned from the group.
	 *
	 * @access public
	 * @var int
	 */
	var $is_banned;

	/**
	 * Title used to describe the group member's role in the group.
	 *
	 * Eg, 'Group Admin'.
	 *
	 * @access public
	 * @var int
	 */
	var $user_title;

	/**
	 * Last modified date of the membership.
	 *
	 * This value is updated when, eg, invitations are accepted.
	 *
	 * @access public
	 * @var string
	 */
	var $date_modified;

	/**
	 * Whether the membership has been confirmed.
	 *
	 * @access public
	 * @var int
	 */
	var $is_confirmed;

	/**
	 * Comments associated with the membership.
	 *
	 * In BP core, these are limited to the optional message users can
	 * include when requesting membership to a private group.
	 *
	 * @access public
	 * @var string
	 */
	var $comments;

	/**
	 * Whether an invitation has been sent for this membership.
	 *
	 * The purpose of this flag is to mark when an invitation has been
	 * "drafted" (the user has been added via the interface at Send
	 * Invites), but the Send button has not been pressed, so the
	 * invitee has not yet been notified.
	 *
	 * @access public
	 * @var int
	 */
	var $invite_sent;

	/**
	 * WP_User object representing the membership's user.
	 *
	 * @access public
	 * @var WP_User
	 */
	var $user;

	/**
	 * Constructor method.
	 *
	 * @param int $user_id Optional. Along with $group_id, can be used to
	 *        look up a membership.
	 * @param int $group_id Optional. Along with $user_id, can be used to
	 *        look up a membership.
	 * @param int $id Optional. The unique ID of the membership object.
	 * @param bool $populate Whether to populate the properties of the
	 *        located membership. Default: true.
	 */
	public function __construct( $user_id = 0, $group_id = 0, $id = false, $populate = true ) {

		// User and group are not empty, and ID is
		if ( !empty( $user_id ) && !empty( $group_id ) && empty( $id ) ) {
			$this->user_id  = $user_id;
			$this->group_id = $group_id;

			if ( !empty( $populate ) ) {
				$this->populate();
			}
		}

		// ID is not empty
		if ( !empty( $id ) ) {
			$this->id = $id;

			if ( !empty( $populate ) ) {
				$this->populate();
			}
		}
	}

	/**
	 * Populate the object's properties.
	 */
	public function populate() {
		global $wpdb, $bp;

		if ( $this->user_id && $this->group_id && !$this->id )
			$sql = $wpdb->prepare( "SELECT * FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d", $this->user_id, $this->group_id );

		if ( !empty( $this->id ) )
			$sql = $wpdb->prepare( "SELECT * FROM {$bp->groups->table_name_members} WHERE id = %d", $this->id );

		$member = $wpdb->get_row($sql);

		if ( !empty( $member ) ) {
			$this->id            = $member->id;
			$this->group_id      = $member->group_id;
			$this->user_id       = $member->user_id;
			$this->inviter_id    = $member->inviter_id;
			$this->is_admin      = $member->is_admin;
			$this->is_mod        = $member->is_mod;
			$this->is_banned     = $member->is_banned;
			$this->user_title    = $member->user_title;
			$this->date_modified = $member->date_modified;
			$this->is_confirmed  = $member->is_confirmed;
			$this->comments      = $member->comments;
			$this->invite_sent   = $member->invite_sent;

			$this->user = new BP_Core_User( $this->user_id );
		}
	}

	/**
	 * Save the membership data to the database.
	 *
	 * @return bool True on success, false on failure.
	 */
	public function save() {
		global $wpdb, $bp;

		$this->user_id       = apply_filters( 'groups_member_user_id_before_save',       $this->user_id,       $this->id );
		$this->group_id      = apply_filters( 'groups_member_group_id_before_save',      $this->group_id,      $this->id );
		$this->inviter_id    = apply_filters( 'groups_member_inviter_id_before_save',    $this->inviter_id,    $this->id );
		$this->is_admin      = apply_filters( 'groups_member_is_admin_before_save',      $this->is_admin,      $this->id );
		$this->is_mod        = apply_filters( 'groups_member_is_mod_before_save',        $this->is_mod,        $this->id );
		$this->is_banned     = apply_filters( 'groups_member_is_banned_before_save',     $this->is_banned,     $this->id );
		$this->user_title    = apply_filters( 'groups_member_user_title_before_save',    $this->user_title,    $this->id );
		$this->date_modified = apply_filters( 'groups_member_date_modified_before_save', $this->date_modified, $this->id );
		$this->is_confirmed  = apply_filters( 'groups_member_is_confirmed_before_save',  $this->is_confirmed,  $this->id );
		$this->comments      = apply_filters( 'groups_member_comments_before_save',      $this->comments,      $this->id );
		$this->invite_sent   = apply_filters( 'groups_member_invite_sent_before_save',   $this->invite_sent,   $this->id );

		do_action_ref_array( 'groups_member_before_save', array( &$this ) );

		if ( !empty( $this->id ) ) {
			$sql = $wpdb->prepare( "UPDATE {$bp->groups->table_name_members} SET inviter_id = %d, is_admin = %d, is_mod = %d, is_banned = %d, user_title = %s, date_modified = %s, is_confirmed = %d, comments = %s, invite_sent = %d WHERE id = %d", $this->inviter_id, $this->is_admin, $this->is_mod, $this->is_banned, $this->user_title, $this->date_modified, $this->is_confirmed, $this->comments, $this->invite_sent, $this->id );
		} else {
			// Ensure that user is not already a member of the group before inserting
			if ( $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d AND is_confirmed = 1 LIMIT 1", $this->user_id, $this->group_id ) ) ) {
				return false;
			}

			$sql = $wpdb->prepare( "INSERT INTO {$bp->groups->table_name_members} ( user_id, group_id, inviter_id, is_admin, is_mod, is_banned, user_title, date_modified, is_confirmed, comments, invite_sent ) VALUES ( %d, %d, %d, %d, %d, %d, %s, %s, %d, %s, %d )", $this->user_id, $this->group_id, $this->inviter_id, $this->is_admin, $this->is_mod, $this->is_banned, $this->user_title, $this->date_modified, $this->is_confirmed, $this->comments, $this->invite_sent );
		}

		if ( !$wpdb->query( $sql ) )
			return false;

		$this->id = $wpdb->insert_id;

		// Update the user's group count
		self::refresh_total_group_count_for_user( $this->user_id );

		// Update the group's member count
		self::refresh_total_member_count_for_group( $this->group_id );

		do_action_ref_array( 'groups_member_after_save', array( &$this ) );

		return true;
	}

	/**
	 * Promote a member to a new status.
	 *
	 * @param string $status The new status. 'mod' or 'admin'.
	 * @return bool True on success, false on failure.
	 */
	public function promote( $status = 'mod' ) {
		if ( 'mod' == $status ) {
			$this->is_admin   = 0;
			$this->is_mod     = 1;
			$this->user_title = __( 'Group Mod', 'buddypress' );
		}

		if ( 'admin' == $status ) {
			$this->is_admin   = 1;
			$this->is_mod     = 0;
			$this->user_title = __( 'Group Admin', 'buddypress' );
		}

		return $this->save();
	}

	/**
	 * Demote membership to Member status (non-admin, non-mod).
	 *
	 * @return bool True on success, false on failure.
	 */
	public function demote() {
		$this->is_mod     = 0;
		$this->is_admin   = 0;
		$this->user_title = false;

		return $this->save();
	}

	/**
	 * Ban the user from the group.
	 *
	 * @return bool True on success, false on failure.
	 */
	public function ban() {
		if ( !empty( $this->is_admin ) )
			return false;

		$this->is_mod = 0;
		$this->is_banned = 1;

		return $this->save();
	}

	/**
	 * Unban the user from the group.
	 *
	 * @return bool True on success, false on failure.
	 */
	public function unban() {
		if ( !empty( $this->is_admin ) )
			return false;

		$this->is_banned = 0;

		return $this->save();
	}

	/**
	 * Mark a pending invitation as accepted.
	 */
	public function accept_invite() {
		$this->inviter_id    = 0;
		$this->is_confirmed  = 1;
		$this->date_modified = bp_core_current_time();
	}

	/**
	 * Confirm a membership request.
	 */
	public function accept_request() {
		$this->is_confirmed = 1;
		$this->date_modified = bp_core_current_time();
	}

	/**
	 * Remove the current membership.
	 *
	 * @return bool True on success, false on failure.
	 */
	public function remove() {
		global $wpdb, $bp;

		$sql = $wpdb->prepare( "DELETE FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d", $this->user_id, $this->group_id );

		if ( !$result = $wpdb->query( $sql ) )
			return false;

		// Update the user's group count
		self::refresh_total_group_count_for_user( $this->user_id );

		// Update the group's member count
		self::refresh_total_member_count_for_group( $this->group_id );

		return $result;
	}

	/** Static Methods ****************************************************/

	/**
	 * Refresh the total_group_count for a user.
	 *
	 * @since BuddyPress (1.8.0)
	 *
	 * @param int $user_id ID of the user.
	 * @return bool True on success, false on failure.
	 */
	public static function refresh_total_group_count_for_user( $user_id ) {
		return bp_update_user_meta( $user_id, 'total_group_count', (int) self::total_group_count( $user_id ) );
	}

	/**
	 * Refresh the total_member_count for a group.
	 *
	 * @since BuddyPress (1.8.0)
	 *
	 * @param int $group_id ID of the group.
	 * @return bool True on success, false on failure.
	 */
	public static function refresh_total_member_count_for_group( $group_id ) {
		return groups_update_groupmeta( $group_id, 'total_member_count', (int) BP_Groups_Group::get_total_member_count( $group_id ) );
	}

	/**
	 * Delete a membership, based on user + group IDs.
	 *
	 * @param int $user_id ID of the user.
	 * @param int $group_id ID of the group.
	 * @return True on success, false on failure.
	 */
	public static function delete( $user_id, $group_id ) {
		global $wpdb, $bp;

		$remove = $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d", $user_id, $group_id ) );

		// Update the user's group count
		self::refresh_total_group_count_for_user( $user_id );

		// Update the group's member count
		self::refresh_total_member_count_for_group( $group_id );

		return $remove;
	}

	/**
	 * Get the IDs of the groups of which a specified user is a member.
	 *
	 * @param int $user_id ID of the user.
	 * @param int $limit Optional. Max number of results to return.
	 *        Default: false (no limit).
	 * @param int $page Optional. Page offset of results to return.
	 *        Default: false (no limit).
	 * @return array {
	 *     @type array $groups Array of groups returned by paginated query.
	 *     @type int $total Count of groups matching query.
	 * }
	 */
	public static function get_group_ids( $user_id, $limit = false, $page = false ) {
		global $wpdb, $bp;

		$pag_sql = '';
		if ( !empty( $limit ) && !empty( $page ) )
			$pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );

		// If the user is logged in and viewing their own groups, we can show hidden and private groups
		if ( $user_id != bp_loggedin_user_id() ) {
			$group_sql = $wpdb->prepare( "SELECT DISTINCT m.group_id FROM {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE g.status != 'hidden' AND m.user_id = %d AND m.is_confirmed = 1 AND m.is_banned = 0{$pag_sql}", $user_id );
			$total_groups = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT m.group_id) FROM {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE g.status != 'hidden' AND m.user_id = %d AND m.is_confirmed = 1 AND m.is_banned = 0", $user_id ) );
		} else {
			$group_sql = $wpdb->prepare( "SELECT DISTINCT group_id FROM {$bp->groups->table_name_members} WHERE user_id = %d AND is_confirmed = 1 AND is_banned = 0{$pag_sql}", $user_id );
			$total_groups = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT group_id) FROM {$bp->groups->table_name_members} WHERE user_id = %d AND is_confirmed = 1 AND is_banned = 0", $user_id ) );
		}

		$groups = $wpdb->get_col( $group_sql );

		return array( 'groups' => $groups, 'total' => (int) $total_groups );
	}

	/**
	 * Get the IDs of the groups of which a specified user is a member, sorted by the date joined.
	 *
	 * @param int $user_id ID of the user.
	 * @param int $limit Optional. Max number of results to return.
	 *        Default: false (no limit).
	 * @param int $page Optional. Page offset of results to return.
	 *        Default: false (no limit).
	 * @param string $filter Optional. Limit results to groups whose name or
	 *        description field matches search terms.
	 * @return array {
	 *     @type array $groups Array of groups returned by paginated query.
	 *     @type int $total Count of groups matching query.
	 * }
	 */
	public static function get_recently_joined( $user_id, $limit = false, $page = false, $filter = false ) {
		global $wpdb, $bp;

		$pag_sql = $hidden_sql = $filter_sql = '';

		if ( !empty( $limit ) && !empty( $page ) )
			$pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );

		if ( !empty( $filter ) ) {
			$filter     = esc_sql( like_escape( $filter ) );
			$filter_sql = " AND ( g.name LIKE '%%{$filter}%%' OR g.description LIKE '%%{$filter}%%' )";
		}

		if ( $user_id != bp_loggedin_user_id() )
			$hidden_sql = " AND g.status != 'hidden'";

		$paged_groups = $wpdb->get_results( $wpdb->prepare( "SELECT g.*, gm1.meta_value as total_member_count, gm2.meta_value as last_activity FROM {$bp->groups->table_name_groupmeta} gm1, {$bp->groups->table_name_groupmeta} gm2, {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE g.id = m.group_id AND g.id = gm1.group_id AND g.id = gm2.group_id AND gm2.meta_key = 'last_activity' AND gm1.meta_key = 'total_member_count'{$hidden_sql}{$filter_sql} AND m.user_id = %d AND m.is_confirmed = 1 AND m.is_banned = 0 ORDER BY m.date_modified DESC {$pag_sql}", $user_id ) );
		$total_groups = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT m.group_id) FROM {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE m.group_id = g.id{$hidden_sql}{$filter_sql} AND m.user_id = %d AND m.is_banned = 0 AND m.is_confirmed = 1 ORDER BY m.date_modified DESC", $user_id ) );

		return array( 'groups' => $paged_groups, 'total' => $total_groups );
	}

	/**
	 * Get the IDs of the groups of which a specified user is an admin.
	 *
	 * @param int $user_id ID of the user.
	 * @param int $limit Optional. Max number of results to return.
	 *        Default: false (no limit).
	 * @param int $page Optional. Page offset of results to return.
	 *        Default: false (no limit).
	 * @param string $filter Optional. Limit results to groups whose name or
	 *        description field matches search terms.
	 * @return array {
	 *     @type array $groups Array of groups returned by paginated query.
	 *     @type int $total Count of groups matching query.
	 * }
	 */
	public static function get_is_admin_of( $user_id, $limit = false, $page = false, $filter = false ) {
		global $wpdb, $bp;

		$pag_sql = $hidden_sql = $filter_sql = '';

		if ( !empty( $limit ) && !empty( $page ) )
			$pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );

		if ( !empty( $filter ) ) {
			$filter     = esc_sql( like_escape( $filter ) );
			$filter_sql = " AND ( g.name LIKE '%%{$filter}%%' OR g.description LIKE '%%{$filter}%%' )";
		}

		if ( $user_id != bp_loggedin_user_id() )
			$hidden_sql = " AND g.status != 'hidden'";

		$paged_groups = $wpdb->get_results( $wpdb->prepare( "SELECT g.*, gm1.meta_value as total_member_count, gm2.meta_value as last_activity FROM {$bp->groups->table_name_groupmeta} gm1, {$bp->groups->table_name_groupmeta} gm2, {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE g.id = m.group_id AND g.id = gm1.group_id AND g.id = gm2.group_id AND gm2.meta_key = 'last_activity' AND gm1.meta_key = 'total_member_count'{$hidden_sql}{$filter_sql} AND m.user_id = %d AND m.is_confirmed = 1 AND m.is_banned = 0 AND m.is_admin = 1 ORDER BY m.date_modified ASC {$pag_sql}", $user_id ) );
		$total_groups = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT m.group_id) FROM {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE m.group_id = g.id{$hidden_sql}{$filter_sql} AND m.user_id = %d AND m.is_confirmed = 1 AND m.is_banned = 0 AND m.is_admin = 1 ORDER BY date_modified ASC", $user_id ) );

		return array( 'groups' => $paged_groups, 'total' => $total_groups );
	}

	/**
	 * Get the IDs of the groups of which a specified user is a moderator.
	 *
	 * @param int $user_id ID of the user.
	 * @param int $limit Optional. Max number of results to return.
	 *        Default: false (no limit).
	 * @param int $page Optional. Page offset of results to return.
	 *        Default: false (no limit).
	 * @param string $filter Optional. Limit results to groups whose name or
	 *        description field matches search terms.
	 * @return array {
	 *     @type array $groups Array of groups returned by paginated query.
	 *     @type int $total Count of groups matching query.
	 * }
	 */
	public static function get_is_mod_of( $user_id, $limit = false, $page = false, $filter = false ) {
		global $wpdb, $bp;

		$pag_sql = $hidden_sql = $filter_sql = '';

		if ( !empty( $limit ) && !empty( $page ) )
			$pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );

		if ( !empty( $filter ) ) {
			$filter     = esc_sql( like_escape( $filter ) );
			$filter_sql = " AND ( g.name LIKE '%%{$filter}%%' OR g.description LIKE '%%{$filter}%%' )";
		}

		if ( $user_id != bp_loggedin_user_id() )
			$hidden_sql = " AND g.status != 'hidden'";

		$paged_groups = $wpdb->get_results( $wpdb->prepare( "SELECT g.*, gm1.meta_value as total_member_count, gm2.meta_value as last_activity FROM {$bp->groups->table_name_groupmeta} gm1, {$bp->groups->table_name_groupmeta} gm2, {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE g.id = m.group_id AND g.id = gm1.group_id AND g.id = gm2.group_id AND gm2.meta_key = 'last_activity' AND gm1.meta_key = 'total_member_count'{$hidden_sql}{$filter_sql} AND m.user_id = %d AND m.is_confirmed = 1 AND m.is_banned = 0 AND m.is_mod = 1 ORDER BY m.date_modified ASC {$pag_sql}", $user_id ) );
		$total_groups = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT m.group_id) FROM {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE m.group_id = g.id{$hidden_sql}{$filter_sql} AND m.user_id = %d AND m.is_confirmed = 1 AND m.is_banned = 0 AND m.is_mod = 1 ORDER BY date_modified ASC", $user_id ) );

		return array( 'groups' => $paged_groups, 'total' => $total_groups );
	}

	/**
	 * Get the count of groups of which the specified user is a member.
	 *
	 * @param int $user_id Optional. Default: ID of the displayed user.
	 * @return int Group count.
	 */
	public static function total_group_count( $user_id = 0 ) {
		global $bp, $wpdb;

		if ( empty( $user_id ) )
			$user_id = bp_displayed_user_id();

		if ( $user_id != bp_loggedin_user_id() && !bp_current_user_can( 'bp_moderate' ) ) {
			return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT m.group_id) FROM {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE m.group_id = g.id AND g.status != 'hidden' AND m.user_id = %d AND m.is_confirmed = 1 AND m.is_banned = 0", $user_id ) );
		} else {
			return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT m.group_id) FROM {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE m.group_id = g.id AND m.user_id = %d AND m.is_confirmed = 1 AND m.is_banned = 0", $user_id ) );
		}
	}

	/**
	 * Get a user's outstanding group invitations.
	 *
	 * @param int $user_id ID of the invitee.
	 * @param int $limit Optional. Max number of results to return.
	 *        Default: false (no limit).
	 * @param int $page Optional. Page offset of results to return.
	 *        Default: false (no limit).
	 * @param string|array $exclude Optional. Array or comma-separated list
	 *        of group IDs to exclude from results.
	 * @return array {
	 *     @type array $groups Array of groups returned by paginated query.
	 *     @type int $total Count of groups matching query.
	 * }
	 */
	public static function get_invites( $user_id, $limit = false, $page = false, $exclude = false ) {
		global $wpdb, $bp;

		$pag_sql = ( !empty( $limit ) && !empty( $page ) ) ? $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) ) : '';

		if ( !empty( $exclude ) ) {
			$exclude     = implode( ',', wp_parse_id_list( $exclude ) );
			$exclude_sql = " AND g.id NOT IN ({$exclude})";
		} else {
			$exclude_sql = '';
		}

		$paged_groups = $wpdb->get_results( $wpdb->prepare( "SELECT g.*, gm1.meta_value as total_member_count, gm2.meta_value as last_activity FROM {$bp->groups->table_name_groupmeta} gm1, {$bp->groups->table_name_groupmeta} gm2, {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE g.id = m.group_id AND g.id = gm1.group_id AND g.id = gm2.group_id AND gm2.meta_key = 'last_activity' AND gm1.meta_key = 'total_member_count' AND m.is_confirmed = 0 AND m.inviter_id != 0 AND m.invite_sent = 1 AND m.user_id = %d {$exclude_sql} ORDER BY m.date_modified ASC {$pag_sql}", $user_id ) );
		$total_groups = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT m.group_id) FROM {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE m.group_id = g.id AND m.is_confirmed = 0 AND m.inviter_id != 0 AND m.invite_sent = 1 AND m.user_id = %d {$exclude_sql} ORDER BY date_modified ASC", $user_id ) );

		return array( 'groups' => $paged_groups, 'total' => $total_groups );
	}

	/**
	 * Check whether a user has an outstanding invitation to a given group.
	 *
	 * @param int $user_id ID of the potential invitee.
	 * @param int $group_id ID of the group.
	 * @param string $type If 'sent', results are limited to those
	 *        invitations that have actually been sent (non-draft).
	 *        Default: 'sent'.
	 * @return int|null The ID of the invitation if found, otherwise null.
	 */
	public static function check_has_invite( $user_id, $group_id, $type = 'sent' ) {
		global $wpdb, $bp;

		if ( empty( $user_id ) )
			return false;

		$sql = "SELECT id FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d AND is_confirmed = 0 AND inviter_id != 0";

		if ( 'sent' == $type )
			$sql .= " AND invite_sent = 1";

		return $wpdb->get_var( $wpdb->prepare( $sql, $user_id, $group_id ) );
	}

	/**
	 * Delete an invitation, by specifying user ID and group ID.
	 *
	 * @param int $user_id ID of the user.
	 * @param int $group_id ID of the group.
	 * @return int Number of records deleted.
	 */
	public static function delete_invite( $user_id, $group_id ) {
		global $wpdb, $bp;

		if ( empty( $user_id ) )
			return false;

		return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d AND is_confirmed = 0 AND inviter_id != 0 AND invite_sent = 1", $user_id, $group_id ) );
	}

	/**
	 * Delete an unconfirmed membership request, by user ID and group ID.
	 *
	 * @param int $user_id ID of the user.
	 * @param int $group_id ID of the group.
	 * @return int Number of records deleted.
	 */
	public static function delete_request( $user_id, $group_id ) {
		global $wpdb, $bp;

		if ( empty( $user_id ) )
			return false;

 		return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d AND is_confirmed = 0 AND inviter_id = 0 AND invite_sent = 0", $user_id, $group_id ) );
	}

	/**
	 * Check whether a user is an admin of a given group.
	 *
	 * @param int $user_id ID of the user.
	 * @param int $group_id ID of the group.
	 * @param int|null ID of the membership if the user is an admin,
	 *        otherwise null.
	 */
	public static function check_is_admin( $user_id, $group_id ) {
		global $wpdb, $bp;

		if ( empty( $user_id ) )
			return false;

		return $wpdb->query( $wpdb->prepare( "SELECT id FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d AND is_admin = 1 AND is_banned = 0", $user_id, $group_id ) );
	}

	/**
	 * Check whether a user is a mod of a given group.
	 *
	 * @param int $user_id ID of the user.
	 * @param int $group_id ID of the group.
	 * @param int|null ID of the membership if the user is a mod,
	 *        otherwise null.
	 */
	public static function check_is_mod( $user_id, $group_id ) {
		global $wpdb, $bp;

		if ( empty( $user_id ) )
			return false;

		return $wpdb->query( $wpdb->prepare( "SELECT id FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d AND is_mod = 1 AND is_banned = 0", $user_id, $group_id ) );
	}

	/**
	 * Check whether a user is a member of a given group.
	 *
	 * @param int $user_id ID of the user.
	 * @param int $group_id ID of the group.
	 * @param int|null ID of the membership if the user is a member,
	 *        otherwise null.
	 */
	public static function check_is_member( $user_id, $group_id ) {
		global $wpdb, $bp;

		if ( empty( $user_id ) )
			return false;

		return $wpdb->query( $wpdb->prepare( "SELECT id FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d AND is_confirmed = 1 AND is_banned = 0", $user_id, $group_id ) );
	}

	/**
	 * Check whether a user is banned from a given group.
	 *
	 * @param int $user_id ID of the user.
	 * @param int $group_id ID of the group.
	 * @param int|null ID of the membership if the user is banned,
	 *        otherwise null.
	 */
	public static function check_is_banned( $user_id, $group_id ) {
		global $wpdb, $bp;

		if ( empty( $user_id ) )
			return false;

		return $wpdb->get_var( $wpdb->prepare( "SELECT is_banned FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d", $user_id, $group_id ) );
	}

	/**
	 * Is the specified user the creator of the group?
	 *
	 * @since BuddyPress (1.2.6)
	 *
	 * @param int $user_id ID of the user.
	 * @param int $group_id ID of the group.
	 * @return int|null ID of the group if the user is the creator,
	 *         otherwise false.
	 */
	public static function check_is_creator( $user_id, $group_id ) {
		global $bp, $wpdb;

		if ( empty( $user_id ) )
			return false;

		return $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$bp->groups->table_name} WHERE creator_id = %d AND id = %d", $user_id, $group_id ) );
	}

	/**
	 * Check whether a user has an outstanding membership request for a given group.
	 *
	 * @param int $user_id ID of the user.
	 * @param int $group_id ID of the group.
	 * @return int|null ID of the membership if found, otherwise false.
	 */
	public static function check_for_membership_request( $user_id, $group_id ) {
		global $wpdb, $bp;

		if ( empty( $user_id ) )
			return false;

		return $wpdb->query( $wpdb->prepare( "SELECT id FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d AND is_confirmed = 0 AND is_banned = 0 AND inviter_id = 0", $user_id, $group_id ) );
	}

	/**
	 * Get a list of randomly selected IDs of groups that the member belongs to.
	 *
	 * @param int $user_id ID of the user.
	 * @param int $total_groups Max number of group IDs to return. Default: 5.
	 * @return array Group IDs.
	 */
	public static function get_random_groups( $user_id = 0, $total_groups = 5 ) {
		global $wpdb, $bp;

		// If the user is logged in and viewing their random groups, we can show hidden and private groups
		if ( bp_is_my_profile() ) {
			return $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT group_id FROM {$bp->groups->table_name_members} WHERE user_id = %d AND is_confirmed = 1 AND is_banned = 0 ORDER BY rand() LIMIT %d", $user_id, $total_groups ) );
		} else {
			return $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT m.group_id FROM {$bp->groups->table_name_members} m, {$bp->groups->table_name} g WHERE m.group_id = g.id AND g.status != 'hidden' AND m.user_id = %d AND m.is_confirmed = 1 AND m.is_banned = 0 ORDER BY rand() LIMIT %d", $user_id, $total_groups ) );
		}
	}

	/**
	 * Get the IDs of all a given group's members.
	 *
	 * @param int $group_id ID of the group.
	 * @return array IDs of all group members.
	 */
	public static function get_group_member_ids( $group_id ) {
		global $bp, $wpdb;

		return $wpdb->get_col( $wpdb->prepare( "SELECT user_id FROM {$bp->groups->table_name_members} WHERE group_id = %d AND is_confirmed = 1 AND is_banned = 0", $group_id ) );
	}

	/**
	 * Get a list of all a given group's admins.
	 *
	 * @param int $group_id ID of the group.
	 * @return array Info about group admins (user_id + date_modified).
	 */
	public static function get_group_administrator_ids( $group_id ) {
		global $bp, $wpdb;

		return $wpdb->get_results( $wpdb->prepare( "SELECT user_id, date_modified FROM {$bp->groups->table_name_members} WHERE group_id = %d AND is_admin = 1 AND is_banned = 0", $group_id ) );
	}

	/**
	 * Get a list of all a given group's moderators.
	 *
	 * @param int $group_id ID of the group.
	 * @return array Info about group mods (user_id + date_modified).
	 */
	public static function get_group_moderator_ids( $group_id ) {
		global $bp, $wpdb;

		return $wpdb->get_results( $wpdb->prepare( "SELECT user_id, date_modified FROM {$bp->groups->table_name_members} WHERE group_id = %d AND is_mod = 1 AND is_banned = 0", $group_id ) );
	}

	/**
	 * Get the IDs users with outstanding membership requests to the group.
	 *
	 * @param int $group_id ID of the group.
	 * @return array IDs of users with outstanding membership requests.
	 */
	public static function get_all_membership_request_user_ids( $group_id ) {
		global $bp, $wpdb;

		return $wpdb->get_col( $wpdb->prepare( "SELECT user_id FROM {$bp->groups->table_name_members} WHERE group_id = %d AND is_confirmed = 0 AND inviter_id = 0", $group_id ) );
	}

	/**
	 * Get members of a group.
	 *
	 * @deprecated BuddyPress (1.8.0)
	 */
	public static function get_all_for_group( $group_id, $limit = false, $page = false, $exclude_admins_mods = true, $exclude_banned = true, $exclude = false ) {
		global $bp, $wpdb;

		_deprecated_function( __METHOD__, '1.8', 'BP_Group_Member_Query' );

		$pag_sql = '';
		if ( !empty( $limit ) && !empty( $page ) )
			$pag_sql = $wpdb->prepare( "LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );

		$exclude_admins_sql = '';
		if ( !empty( $exclude_admins_mods ) )
			$exclude_admins_sql = "AND is_admin = 0 AND is_mod = 0";

		$banned_sql = '';
		if ( !empty( $exclude_banned ) )
			$banned_sql = " AND is_banned = 0";

		$exclude_sql = '';
		if ( !empty( $exclude ) ) {
			$exclude     = implode( ',', wp_parse_id_list( $exclude ) );
			$exclude_sql = " AND m.user_id NOT IN ({$exclude})";
		}

		if ( bp_is_active( 'xprofile' ) ) {
			$members = $wpdb->get_results( apply_filters( 'bp_group_members_user_join_filter', $wpdb->prepare( "SELECT m.user_id, m.date_modified, m.is_banned, u.user_login, u.user_nicename, u.user_email, pd.value as display_name FROM {$bp->groups->table_name_members} m, {$wpdb->users} u, {$bp->profile->table_name_data} pd WHERE u.ID = m.user_id AND u.ID = pd.user_id AND pd.field_id = 1 AND group_id = %d AND is_confirmed = 1 {$banned_sql} {$exclude_admins_sql} {$exclude_sql} ORDER BY m.date_modified DESC {$pag_sql}", $group_id ) ) );
		} else {
			$members = $wpdb->get_results( apply_filters( 'bp_group_members_user_join_filter', $wpdb->prepare( "SELECT m.user_id, m.date_modified, m.is_banned, u.user_login, u.user_nicename, u.user_email, u.display_name FROM {$bp->groups->table_name_members} m, {$wpdb->users} u WHERE u.ID = m.user_id AND group_id = %d AND is_confirmed = 1 {$banned_sql} {$exclude_admins_sql} {$exclude_sql} ORDER BY m.date_modified DESC {$pag_sql}", $group_id ) ) );
		}

		if ( empty( $members ) ) {
			return false;
		}

		if ( empty( $pag_sql ) ) {
			$total_member_count = count( $members );
		} else {
			$total_member_count = $wpdb->get_var( apply_filters( 'bp_group_members_count_user_join_filter', $wpdb->prepare( "SELECT COUNT(user_id) FROM {$bp->groups->table_name_members} m WHERE group_id = %d AND is_confirmed = 1 {$banned_sql} {$exclude_admins_sql} {$exclude_sql}", $group_id ) ) );
		}

		// Fetch whether or not the user is a friend
		foreach ( (array) $members as $user )
			$user_ids[] = $user->user_id;

		$user_ids = implode( ',', wp_parse_id_list( $user_ids ) );

		if ( bp_is_active( 'friends' ) ) {
			$friend_status = $wpdb->get_results( $wpdb->prepare( "SELECT initiator_user_id, friend_user_id, is_confirmed FROM {$bp->friends->table_name} WHERE (initiator_user_id = %d AND friend_user_id IN ( {$user_ids} ) ) OR (initiator_user_id IN ( {$user_ids} ) AND friend_user_id = %d )", bp_loggedin_user_id(), bp_loggedin_user_id() ) );
			for ( $i = 0, $count = count( $members ); $i < $count; ++$i ) {
				foreach ( (array) $friend_status as $status ) {
					if ( $status->initiator_user_id == $members[$i]->user_id || $status->friend_user_id == $members[$i]->user_id ) {
						$members[$i]->is_friend = $status->is_confirmed;
					}
				}
			}
		}

		return array( 'members' => $members, 'count' => $total_member_count );
	}

	/**
	 * Delete all memberships for a given group.
	 *
	 * @param int $group_id ID of the group.
	 * @return int Number of records deleted.
	 */
	public static function delete_all( $group_id ) {
		global $wpdb, $bp;

		return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->groups->table_name_members} WHERE group_id = %d", $group_id ) );
	}

	/**
	 * Delete all group membership information for the specified user.
	 *
	 * @since BuddyPress (1.0.0)
	 *
	 * @param int $user_id ID of the user.
	 */
	public static function delete_all_for_user( $user_id ) {
		global $bp, $wpdb;

		// Get all the group ids for the current user's groups and update counts
		$group_ids = BP_Groups_Member::get_group_ids( $user_id );
		foreach ( $group_ids['groups'] as $group_id ) {
			groups_update_groupmeta( $group_id, 'total_member_count', groups_get_total_member_count( $group_id ) - 1 );

			// If current user is the creator of a group and is the sole admin, delete that group to avoid counts going out-of-sync
			if ( groups_is_user_admin( $user_id, $group_id ) && count( groups_get_group_admins( $group_id ) ) < 2 && groups_is_user_creator( $user_id, $group_id ) )
				groups_delete_group( $group_id );
		}

		return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->groups->table_name_members} WHERE user_id = %d", $user_id ) );
	}
}

/**
 * API for creating group extensions without having to hardcode the content into
 * the theme.
 *
 * To implement, extend this class. In your constructor, pass an optional array
 * of arguments to parent::init() to configure your widget. The config array
 * supports the following values:
 *   - 'slug' A unique identifier for your extension. This value will be used
 *     to build URLs, so make it URL-safe
 *   - 'name' A translatable name for your extension. This value is used to
       populate the navigation tab, as well as the default titles for admin/
       edit/create tabs.
 *   - 'visibility' Set to 'public' (default) for your extension (the main tab
 *     as well as the widget) to be available to anyone who can access the
 *     group, 'private' otherwise.
 *   - 'nav_item_position' An integer explaining where the nav item should
 *     appear in the tab list
 *   - 'enable_nav_item' Set to true for your extension's main tab to be
 *     available to anyone who can access the group.
 *   - 'nav_item_name' The translatable text you want to appear in the nav tab.
 *     Defaults to the value of 'name'.
 *   - 'display_hook' The WordPress action that the widget_display() method is
 *     hooked to
 *   - 'template_file' The template file that will be used to load the content
 *     of your main extension tab. Defaults to 'groups/single/plugins.php'.
 *   - 'screens' A multi-dimensional array, described below
 *
 * BP_Group_Extension uses the concept of "settings screens". There are three
 * contexts for settings screens:
 *   - 'create', which inserts a new step into the group creation process
 *   - 'edit', which adds a tab for your extension into the Admin section of
 *     a group
 *   - 'admin', which adds a metabox to the Groups administration panel in the
 *     WordPress Dashboard
 * Each of these settings screens is populated by a pair of methods: one that
 * creates the markup for the screen, and one that processes form data
 * submitted from the screen. If your plugin needs screens in all three
 * contexts, and if the markup and form processing logic will be the same in
 * each case, you can define two methods to handle all of the screens:
 *   function settings_screen() {}
 *   function settings_screen_save() {}
 * If one or more of your settings screen needs separate logic, you may define
 * context-specific methods, for example:
 *   function edit_screen() {}
 *   function edit_screen_save() {}
 * BP_Group_Extension will use the more specific methods if they are available.
 *
 * You can further customize the settings screens (tab names, etc) by passing
 * an optional 'screens' parameter to the init array. The format is as follows:
 *   'screens' => array(
 *       'create' => array(
 *	     'slug' => 'foo',
 *	     'name' => 'Foo',
 *	     'position' => 55,
 *	     'screen_callback' => 'my_create_screen_callback',
 *	     'screen_save_callback' => 'my_create_screen_save_callback',
 *	 ),
 *	 'edit' => array( // ...
 *   ),
 * Only provide those arguments that you actually want to change from the
 * default configuration. BP_Group_Extension will do the rest.
 *
 * Note that the 'edit' screen accepts an additional parameter: 'submit_text',
 * which defines the text of the Submit button automatically added to the Edit
 * screen of the extension (defaults to 'Save Changes'). Also, the 'admin'
 * screen accepts two additional parameters: 'metabox_priority' and
 * 'metabox_context'. See the docs for add_meta_box() for more details on these
 * arguments.
 *
 * Prior to BuddyPress 1.7, group extension configurations were set slightly
 * differently. The legacy method is still supported, though deprecated.
 *
 * @package BuddyPress
 * @subpackage Groups
 * @since BuddyPress (1.1.0)
 */
class BP_Group_Extension {

	/** Public ************************************************************/

	/**
	 * Information about this extension's screens.
	 *
	 * @since BuddyPress (1.8.0)
	 * @var array
	 */
	public $screens = array();

	/**
	 * The name of the extending class.
	 *
	 * @since BuddyPress (1.8.0)
	 * @var string
	 */
	public $class_name = '';

	/**
	 * A ReflectionClass object of the current extension.
	 *
	 * @since BuddyPress (1.8.0)
	 * @var ReflectionClass
	 */
	public $class_reflection = null;

	/**
	 * Parsed configuration paramaters for the extension.
	 *
	 * @since BuddyPress (1.8.0)
	 * @var array
	 */
	public $params = array();

	/**
	 * The ID of the current group.
	 *
	 * @since BuddyPress (1.8.0)
	 * @var int
	 */
	public $group_id = 0;

	/**
	 * The slug of the current extension.
	 *
	 * @var string
	 */
	public $slug = '';

	/**
	 * The translatable name of the current extension.
	 *
	 * @var string
	 */
	public $name = '';

	/**
	 * The visibility of the extension tab. 'public' or 'private'.
	 *
	 * @var string
	 */
	public $visibility = 'public';

	/**
	 * The numeric position of the main nav item.
	 *
	 * @var int
	 */
	public $nav_item_position = 81;

	/**
	 * Whether to show the nav item.
	 *
	 * @var bool
	 */
	public $enable_nav_item = true;

	/**
	 * The text of the nav item. Defaults to self::name.
	 *
	 * @var string
	 */
	public $nav_item_name = '';

	/**
	 * The WP action that self::widget_display() is attached to.
	 *
	 * Default: 'groups_custom_group_boxes'.
	 *
	 * @var string
	 */
	public $display_hook = 'groups_custom_group_boxes';

	/**
	 * The template file used to load the plugin content.
	 *
	 * Default: 'groups/single/plugins'.
	 *
	 * @var string
	 */
	public $template_file = 'groups/single/plugins';

	/** Protected *********************************************************/

	/**
	 * Has the extension been initialized?
	 *
	 * @since BuddyPress (1.8.0)
	 * @var bool
	 */
	protected $initialized = false;

	/**
	 * Extension properties as set by legacy extensions.
	 *
	 * @since BuddyPress (1.8.0)
	 * @var array
	 */
	protected $legacy_properties = array();

	/**
	 * Converted legacy parameters.
	 *
	 * These are the extension properties as set by legacy extensions, but
	 * then converted to match the new format for params.
	 *
	 * @since BuddyPress (1.8.0)
	 * @var array
	 */
	protected $legacy_properties_converted = array();

	/**
	 * Miscellaneous data as set by the __set() magic method.
	 *
	 * @since BuddyPress (1.8.0)
	 * @var array
	 */
	protected $data = array();

	/** Screen Overrides **************************************************/

	/*
	 * Screen override methods are how your extension will display content
	 * and handle form submits. Your extension should only override those
	 * methods that it needs for its purposes.
	 */

	// The content of the group tab
	public function display() {}

	// Content displayed in a widget sidebar, if applicable
	public function widget_display() {}

	// *_screen() displays the settings form for the given context
	// *_screen_save() processes data submitted via the settings form
	// The settings_* methods are generic fallbacks, which can optionally
	// be overridden by the more specific edit_*, create_*, and admin_*
	// versions.
	public function settings_screen( $group_id = null ) {}
	public function settings_screen_save( $group_id = null ) {}
	public function edit_screen( $group_id = null ) {}
	public function edit_screen_save( $group_id = null ) {}
	public function create_screen( $group_id = null ) {}
	public function create_screen_save( $group_id = null ) {}
	public function admin_screen( $group_id = null ) {}
	public function admin_screen_save( $group_id = null ) {}

	/** Setup *************************************************************/

	/**
	 * Initialize the extension, using your config settings
	 *
	 * Your plugin should call this method at the very end of its
	 * constructor, like so:
	 *
	 *   public function __construct() {
	 *       $args = array(
	 *           'slug' => 'my-group-extension',
	 *           'name' => 'My Group Extension',
	 *           // ...
	 *       );
	 *
	 *       parent::init( $args );
	 *   }
	 *
	 * @since BuddyPress (1.8)
	 * @param array $args {
	 *     Array of initialization arguments.
	 *     @type string $slug Unique, URL-safe identifier for your
	 *           extension.
	 *     @type string $name Translatable name for your extension. Used to
	 *           populate navigation items.
	 *     @type string $visibility Optional. Set to 'public' for your
	 *           extension (the main tab as well as the widget) to be
	 *           available to anyone who can access the group; set to
	 *           'private' otherwise. Default: 'public'.
	 *     @type int $nav_item_position Optional. Location of the nav item
	 *           in the tab list. Default: 81.
	 *     @type bool $enable_nav_item Optional. Whether the extension's
	 *           tab should be accessible to anyone who can view the group.
	 *           Default: true.
	 *     @type string $nav_item_name Optional. The translatable text you
	 *           want to appear in the nav tab. Default: the value of $name.
	 *     @type string $display_hook Optional. The WordPress action that
	 *           the widget_display() method is hooked to.
	 *           Default: 'groups_custom_group_boxes'.
	 *     @type string $template_file Optional. Theme-relative path to the
	 *           template file BP should use to load the content of your
	 *           main extension tab. Default: 'groups/single/plugins.php'.
	 *     @type array $screens A multi-dimensional array of configuration
	 *           information for the extension screens. See docblock of
	 *           {@link BP_Group_Extension} for more details.
	 * }
	 */
	public function init( $args = array() ) {

		// Before this init() method was introduced, plugins were
		// encouraged to set their config directly. For backward
		// compatibility with these plugins, we detect whether this is
		// one of those legacy plugins, and parse any legacy arguments
		// with those passed to init()
		$this->parse_legacy_properties();
		$args = $this->parse_args_r( $args, $this->legacy_properties_converted );

		// Parse with defaults
		$this->params = $this->parse_args_r( $args, array(
			'slug'              => $this->slug,
			'name'              => $this->name,
			'visibility'        => $this->visibility,
			'nav_item_position' => $this->nav_item_position,
			'enable_nav_item'   => (bool) $this->enable_nav_item,
			'nav_item_name'     => $this->nav_item_name,
			'display_hook'      => $this->display_hook,
			'template_file'     => $this->template_file,
			'screens'           => $this->get_default_screens(),
		) );

		$this->initialized = true;
	}

	/**
	 * The main setup routine for the extension.
	 *
	 * This method contains the primary logic for setting up an extension's
	 * configuration, setting up backward compatibility for legacy plugins,
	 * and hooking the extension's screen functions into WP and BP.
	 *
	 * Marked 'public' because it must be accessible to add_action().
	 * However, you should never need to invoke this method yourself - it
	 * is called automatically at the right point in the load order by
	 * bp_register_group_extension().
	 *
	 * @since BuddyPress (1.1.0)
	 */
	public function _register() {

		// Detect and parse properties set by legacy extensions
		$this->parse_legacy_properties();

		// Initialize, if necessary. This should only happen for
		// legacy extensions that don't call parent::init() themselves
		if ( true !== $this->initialized ) {
			$this->init();
		}

		// Set some config values, based on the parsed params
		$this->group_id          = $this->get_group_id();
		$this->slug              = $this->params['slug'];
		$this->name              = $this->params['name'];
		$this->visibility        = $this->params['visibility'];
		$this->nav_item_position = $this->params['nav_item_position'];
		$this->nav_item_name     = $this->params['nav_item_name'];
		$this->display_hook      = $this->params['display_hook'];
		$this->template_file     = $this->params['template_file'];

		// Configure 'screens': create, admin, and edit contexts
		$this->setup_screens();

		// Mirror configuration data so it's accessible to plugins
		// that look for it in its old locations
		$this->setup_legacy_properties();

		// Hook the extension into BuddyPress
		$this->setup_display_hooks();
		$this->setup_create_hooks();
		$this->setup_edit_hooks();
		$this->setup_admin_hooks();
	}

	/**
	 * Set up some basic info about the Extension.
	 *
	 * Here we collect the name of the extending class, as well as a
	 * ReflectionClass that is used in get_screen_callback() to determine
	 * whether your extension overrides certain callback methods.
	 *
	 * @since BuddyPress (1.8.0)
	 */
	protected function setup_class_info() {
		if ( empty( $this->class_name ) ) {
			$this->class_name = get_class( $this );
		}

		if ( is_null( $this->class_reflection ) ) {
			$this->class_reflection = new ReflectionClass( $this->class_name );
		}
	}

	/**
	 * Get the current group ID.
	 *
	 * Check for:
	 *   - current group
	 *   - new group
	 *   - group admin
	 *
	 * @since BuddyPress (1.8.0)
	 */
	public static function get_group_id() {

		// Usually this will work
		$group_id = bp_get_current_group_id();

		// On the admin, get the group id out of the $_GET params
		if ( empty( $group_id ) && is_admin() && ( isset( $_GET['page'] ) && ( 'bp-groups' === $_GET['page'] ) ) && ! empty( $_GET['gid'] ) ) {
			$group_id = (int) $_GET['gid'];
		}

		// This fallback will only be hit when the create step is very
		// early
		if ( empty( $group_id ) && bp_get_new_group_id() ) {
			$group_id = bp_get_new_group_id();
		}

		// On some setups, the group id has to be fetched out of the
		// $_POST array
		// @todo Figure out why this is happening during group creation
		if ( empty( $group_id ) && isset( $_POST['group_id'] ) ) {
			$group_id = (int) $_POST['group_id'];
		}

		return $group_id;
	}

	/**
	 * Gather configuration data about your screens.
	 *
	 * @since BuddyPress (1.8.0)
	 */
	protected function get_default_screens() {
		$this->setup_class_info();

		$screens = array(
			'create' => array(
				'position' => 81,
			),
			'edit'   => array(
				'submit_text' => __( 'Save Changes', 'buddypress' ),
			),
			'admin'  => array(
				'metabox_context'  => 'normal',
				'metabox_priority' => 'core',
			),
		);

		foreach ( $screens as $context => &$screen ) {
			$screen['enabled']     = true;
			$screen['name']        = $this->name;
			$screen['slug']        = $this->slug;

			$screen['screen_callback']      = $this->get_screen_callback( $context, 'screen'      );
			$screen['screen_save_callback'] = $this->get_screen_callback( $context, 'screen_save' );
		}

		return $screens;
	}

	/**
	 * Set up screens array based on params.
	 *
	 * @since BuddyPress (1.8.0)
	 */
	protected function setup_screens() {
		foreach ( (array) $this->params['screens'] as $context => $screen ) {
			if ( empty( $screen['slug'] ) ) {
				$screen['slug'] = $this->slug;
			}

			if ( empty( $screen['name'] ) ) {
				$screen['name'] = $this->name;
			}

			$this->screens[ $context ] = $screen;
		}
	}

	/** Display ***********************************************************/

	/**
	 * Hook this extension's group tab into BuddyPress, if necessary.
	 *
	 * @since BuddyPress (1.8.0)
	 */
	protected function setup_display_hooks() {

		// Bail if not a group
		if ( ! bp_is_group() ) {
			return;
		}

		// Bail if the current user doesn't have access
		if ( ( 'public' !== $this->visibility ) && ! buddypress()->groups->current_group->user_has_access ) {
			return;
		}

		if ( true === $this->enable_nav_item ) {
			bp_core_new_subnav_item( array(
				'name'            => ! $this->nav_item_name ? $this->name : $this->nav_item_name,
				'slug'            => $this->slug,
				'parent_slug'     => bp_get_current_group_slug(),
				'parent_url'      => bp_get_group_permalink( groups_get_current_group() ),
				'position'        => $this->nav_item_position,
				'item_css_id'     => 'nav-' . $this->slug,
				'screen_function' => array( &$this, '_display_hook' ),
				'user_has_access' => $this->enable_nav_item
			) );

			// When we are viewing the extension display page, set the title and options title
			if ( bp_is_current_action( $this->slug ) ) {
				add_action( 'bp_template_content_header', create_function( '', 'echo "' . esc_attr( $this->name ) . '";' ) );
				add_action( 'bp_template_title',          create_function( '', 'echo "' . esc_attr( $this->name ) . '";' ) );
			}
		}

		// Hook the group home widget
		if ( ! bp_current_action() && bp_is_current_action( 'home' ) ) {
			add_action( $this->display_hook, array( &$this, 'widget_display' ) );
		}
	}

	/**
	 * Hook the main display method, and loads the template file
	 */
	public function _display_hook() {
		add_action( 'bp_template_content', array( &$this, 'display' ) );
		bp_core_load_template( apply_filters( 'bp_core_template_plugin', $this->template_file ) );
	}

	/** Create ************************************************************/

	/**
	 * Hook this extension's Create step into BuddyPress, if necessary.
	 *
	 * @since BuddyPress (1.8.0)
	 */
	protected function setup_create_hooks() {
		if ( ! $this->is_screen_enabled( 'create' ) ) {
			return;
		}

		$screen = $this->screens['create'];

		// Insert the group creation step for the new group extension
		buddypress()->groups->group_creation_steps[ $screen['slug'] ] = array(
			'name'     => $screen['name'],
			'slug'     => $screen['slug'],
			'position' => $screen['position'],
		);

		// The maybe_ methods check to see whether the create_*
		// callbacks should be invoked (ie, are we on the
		// correct group creation step). Hooked in separate
		// methods because current creation step info not yet
		// available at this point
		add_action( 'groups_custom_create_steps', array( $this, 'maybe_create_screen' ) );
		add_action( 'groups_create_group_step_save_' . $screen['slug'], array( $this, 'maybe_create_screen_save' ) );
	}

	/**
	 * Call the create_screen() method, if we're on the right page.
	 *
	 * @since BuddyPress (1.8.0)
	 */
	public function maybe_create_screen() {
		if ( ! bp_is_group_creation_step( $this->screens['create']['slug'] ) ) {
			return;
		}

		call_user_func( $this->screens['create']['screen_callback'], $this->group_id );
		$this->nonce_field( 'create' );

		// The create screen requires an additional nonce field
		// due to a quirk in the way the templates are built
		wp_nonce_field( 'groups_create_save_' . bp_get_groups_current_create_step(), '_wpnonce', false );
	}

	/**
	 * Call the create_screen_save() method, if we're on the right page.
	 *
	 * @since BuddyPress (1.8.0)
	 */
	public function maybe_create_screen_save() {
		if ( ! bp_is_group_creation_step( $this->screens['create']['slug'] ) ) {
			return;
		}

		$this->check_nonce( 'create' );
		call_user_func( $this->screens['create']['screen_save_callback'], $this->group_id );
	}

	/** Edit **************************************************************/

	/**
	 * Hook this extension's Edit panel into BuddyPress, if necessary.
	 *
	 * @since BuddyPress (1.8.0)
	 */
	protected function setup_edit_hooks() {

		// Bail if not an edit screen
		if ( ! $this->is_screen_enabled( 'edit' ) || ! bp_is_item_admin() ) {
			return;
		}

		$screen = $this->screens['edit'];

		$position = isset( $screen['position'] ) ? (int) $screen['position'] : 10;

		// Add the tab
		// @todo BP should be using bp_core_new_subnav_item()
		add_action( 'groups_admin_tabs', create_function( '$current, $group_slug',
			'$selected = "";
			if ( "' . esc_attr( $screen['slug'] ) . '" == $current )
				$selected = " class=\"current\"";
			echo "<li{$selected}><a href=\"' . trailingslashit( bp_get_root_domain() . '/' . bp_get_groups_root_slug() . '/{$group_slug}/admin/' . esc_attr( $screen['slug'] ) ) . '\">' . esc_attr( $screen['name'] ) . '</a></li>";'
		), $position, 2 );

		// Catch the edit screen and forward it to the plugin template
		if ( bp_is_groups_component() && bp_is_current_action( 'admin' ) && bp_is_action_variable( $screen['slug'], 0 ) ) {
			$this->call_edit_screen_save( $this->group_id );

			add_action( 'groups_custom_edit_steps', array( &$this, 'call_edit_screen' ) );

			// Determine the proper template and save for later
			// loading
			if ( '' !== bp_locate_template( array( 'groups/single/home.php' ), false ) ) {
				$this->edit_screen_template = '/groups/single/home';
			} else {
				add_action( 'bp_template_content_header', create_function( '', 'echo "<ul class=\"content-header-nav\">"; bp_group_admin_tabs(); echo "</ul>";' ) );
				add_action( 'bp_template_content', array( &$this, 'call_edit_screen' ) );
				$this->edit_screen_template = '/groups/single/plugins';
			}

			// We load the template at bp_screens, to give all
			// extensions a chance to load
			add_action( 'bp_screens', array( $this, 'call_edit_screen_template_loader' ) );
		}
	}

	/**
	 * Call the edit_screen() method.
	 *
	 * Previous versions of BP_Group_Extension required plugins to provide
	 * their own Submit button and nonce fields when building markup. In
	 * BP 1.8, this requirement was lifted - BP_Group_Extension now handles
	 * all required submit buttons and nonces.
	 *
	 * We put the edit screen markup into an output buffer before echoing.
	 * This is so that we can check for the presence of a hardcoded submit
	 * button, as would be present in legacy plugins; if one is found, we
	 * do not auto-add our own button.
	 *
	 * @since BuddyPress (1.8.0)
	 */
	public function call_edit_screen() {
		ob_start();
		call_user_func( $this->screens['edit']['screen_callback'], $this->group_id );
		$screen = ob_get_contents();
		ob_end_clean();

		echo $this->maybe_add_submit_button( $screen );

		$this->nonce_field( 'edit' );
	}

	/**
	 * Check the nonce, and call the edit_screen_save() method.
	 *
	 * @since BuddyPress (1.8.0)
	 */
	public function call_edit_screen_save() {
		if ( empty( $_POST ) ) {
			return;
		}

		// When DOING_AJAX, the POST global will be populated, but we
		// should assume it's a save
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
			return;
		}

		$this->check_nonce( 'edit' );
		call_user_func( $this->screens['edit']['screen_save_callback'], $this->group_id );
	}

	/**
	 * Load the template that houses the Edit screen.
	 *
	 * Separated out into a callback so that it can run after all other
	 * Group Extensions have had a chance to register their navigation, to
	 * avoid missing tabs.
	 *
	 * Hooked to 'bp_screens'.
	 *
	 * @since BuddyPress (1.8.0)
	 * @access public So that do_action() has access. Do not call directly.
	 *
	 * @see BP_Group_Extension::setup_edit_hooks()
	 */
	public function call_edit_screen_template_loader() {
		bp_core_load_template( $this->edit_screen_template );
	}

	/**
	 * Add a submit button to the edit form, if it needs one.
	 *
	 * There's an inconsistency in the way that the group Edit and Create
	 * screens are rendered: the Create screen has a submit button built
	 * in, but the Edit screen does not. This function allows plugin
	 * authors to write markup that does not contain the submit button for
	 * use on both the Create and Edit screens - BP will provide the button
	 * if one is not found.
	 *
	 * @since BuddyPress (1.8.0)
	 *
	 * @param string $screen The screen markup, captured in the output
	 *        buffer.
	 * @param string $screen The same markup, with a submit button added.
	 */
	protected function maybe_add_submit_button( $screen = '' ) {
		if ( $this->has_submit_button( $screen ) ) {
			return $screen;
		}

		return $screen . sprintf(
			'<div id="%s"><input type="submit" name="save" value="%s" id="%s"></div>',
			'bp-group-edit-' . $this->slug . '-submit-wrapper',
			$this->screens['edit']['submit_text'],
			'bp-group-edit-' . $this->slug . '-submit'
		);
	}

	/**
	 * Does the given markup have a submit button?
	 *
	 * @since BuddyPress (1.8.0)
	 *
	 * @param string $screen The markup to check.
	 * @return bool True if a Submit button is found, otherwise false.
	 */
	public static function has_submit_button( $screen = '' ) {
		$pattern = "/<input[^>]+type=[\'\"]submit[\'\"]/";
		preg_match( $pattern, $screen, $matches );
		return ! empty( $matches[0] );
	}

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

	/**
	 * Hook this extension's Admin metabox into BuddyPress, if necessary.
	 *
	 * @since BuddyPress (1.8.0)
	 */
	protected function setup_admin_hooks() {
		if ( ! $this->is_screen_enabled( 'admin' ) || ! is_admin() ) {
			return;
		}

		// Hook the admin screen markup function to the content hook
		add_action( 'bp_groups_admin_meta_box_content_' . $this->slug, array( $this, 'call_admin_screen' ) );

		// Initialize the metabox
		add_action( 'bp_groups_admin_meta_boxes', array( $this, '_meta_box_display_callback' ) );

		// Catch the metabox save
		add_action( 'bp_group_admin_edit_after', array( $this, 'call_admin_screen_save' ), 10 );
	}

	/**
	 * Call the admin_screen() method, and add a nonce field.
	 *
	 * @since BuddyPress (1.8.0)
	 */
	public function call_admin_screen() {
		call_user_func( $this->screens['admin']['screen_callback'], $this->group_id );
		$this->nonce_field( 'admin' );
	}

	/**
	 * Check the nonce, and call the admin_screen_save() method
	 *
	 * @since BuddyPress (1.8.0)
	 */
	public function call_admin_screen_save() {
		$this->check_nonce( 'admin' );
		call_user_func( $this->screens['admin']['screen_save_callback'], $this->group_id );
	}

	/**
	 * Create the Dashboard meta box for this extension.
	 *
	 * @since BuddyPress (1.7.0)
	 */
	public function _meta_box_display_callback() {
		$group_id = isset( $_GET['gid'] ) ? (int) $_GET['gid'] : 0;
		$screen   = $this->screens['admin'];

		add_meta_box(
			$screen['slug'],
			$screen['name'],
			create_function( '', 'do_action( "bp_groups_admin_meta_box_content_' . $this->slug . '", ' . $group_id . ' );' ),
			get_current_screen()->id,
			$screen['metabox_context'],
			$screen['metabox_priority']
		);
	}


	/** Utilities *********************************************************/

	/**
	 * Generate the nonce fields for a settings form.
	 *
	 * The nonce field name (the second param passed to wp_nonce_field)
	 * contains this extension's slug and is thus unique to this extension.
	 * This is necessary because in some cases (namely, the Dashboard),
	 * more than one extension may generate nonces on the same page, and we
	 * must avoid name clashes.
	 *
	 * @since BuddyPress (1.8.0)
	 *
	 * @uses wp_nonce_field()
	 *
	 * @param string $context Screen context. 'create', 'edit', or 'admin'.
	 */
	public function nonce_field( $context = '' ) {
		wp_nonce_field( 'bp_group_extension_' . $this->slug . '_' . $context, '_bp_group_' . $context . '_nonce_' . $this->slug );
	}

	/**
	 * Check the nonce on a submitted settings form.
	 *
	 * @since BuddyPress (1.8.0)
	 *
	 * @uses check_admin_referer()
	 *
	 * @param string $context Screen context. 'create', 'edit', or 'admin'.
	 */
	public function check_nonce( $context = '' ) {
		check_admin_referer( 'bp_group_extension_' . $this->slug . '_' . $context, '_bp_group_' . $context . '_nonce_' . $this->slug );
	}

	/**
	 * Is the specified screen enabled?
	 *
	 * To be enabled, a screen must both have the 'enabled' key set to true
	 * (legacy: $this->enable_create_step, etc), and its screen_callback
	 * must also exist and be callable.
	 *
	 * @since BuddyPress (1.8.0)
	 *
	 * @param string $context Screen context. 'create', 'edit', or 'admin'.
	 *
	 * @return bool True if the screen is enabled, otherwise false.
	 */
	public function is_screen_enabled( $context = '' ) {
		$enabled = false;

		if ( isset( $this->screens[ $context ] ) ) {
			$enabled = $this->screens[ $context ]['enabled'] && is_callable( $this->screens[ $context ]['screen_callback'] );
		}

		return (bool) $enabled;
	}

	/**
	 * Get the appropriate screen callback for the specified context/type.
	 *
	 * BP Group Extensions have three special "screen contexts": create,
	 * admin, and edit. Each of these contexts has a corresponding
	 * _screen() and _screen_save() method, which allow group extension
	 * plugins to define different markup and logic for each context.
	 *
	 * BP also supports fallback settings_screen() and
	 * settings_screen_save() methods, which can be used to define markup
	 * and logic that is shared between context. For each context, you may
	 * either provide context-specific methods, or you can let BP fall back
	 * on the shared settings_* callbacks.
	 *
	 * For example, consider a BP_Group_Extension implementation that looks
	 * like this:
	 *
	 *   // ...
	 *   function create_screen( $group_id ) { ... }
	 *   function create_screen_save( $group_id ) { ... }
	 *   function settings_screen( $group_id ) { ... }
	 *   function settings_screen_save( $group_id ) { ... }
	 *   // ...
	 *
	 * BP_Group_Extension will use your create_* methods for the Create
	 * steps, and will use your generic settings_* methods for the Edit
	 * and Admin contexts. This schema allows plugin authors maximum
	 * flexibility without having to repeat themselves.
	 *
	 * The get_screen_callback() method uses a ReflectionClass object to
	 * determine whether your extension has provided a given callback.
	 *
	 * @since BuddyPress (1.8.0)
	 *
	 * @param string $context Screen context. 'create', 'edit', or 'admin'.
	 * @param string $type Screen type. 'screen' or 'screen_save'. Default:
	 *        'screen'.
	 * @return callable A callable function handle.
	 */
	public function get_screen_callback( $context = '', $type = 'screen' ) {
		$callback = '';

		// Try the context-specific callback first
		$method  = $context . '_' . $type;
		$rmethod = $this->class_reflection->getMethod( $method );
		if ( isset( $rmethod->class ) && $this->class_name === $rmethod->class ) {
			$callback = array( $this, $method );
		}

		if ( empty( $callback ) ) {
			$fallback_method  = 'settings_' . $type;
			$rfallback_method = $this->class_reflection->getMethod( $fallback_method );
			if ( isset( $rfallback_method->class ) && $this->class_name === $rfallback_method->class ) {
				$callback = array( $this, $fallback_method );
			}
		}

		return $callback;
	}

	/**
	 * Recursive argument parsing.
	 *
	 * This acts like a multi-dimensional version of wp_parse_args() (minus
	 * the querystring parsing - you must pass arrays).
	 *
	 * Values from $a override those from $b; keys in $b that don't exist
	 * in $a are passed through.
	 *
	 * This is different from array_merge_recursive(), both because of the
	 * order of preference ($a overrides $b) and because of the fact that
	 * array_merge_recursive() combines arrays deep in the tree, rather
	 * than overwriting the b array with the a array.
	 *
	 * The implementation of this function is specific to the needs of
	 * BP_Group_Extension, where we know that arrays will always be
	 * associative, and that an argument under a given key in one array
	 * will be matched by a value of identical depth in the other one. The
	 * function is NOT designed for general use, and will probably result
	 * in unexpected results when used with data in the wild. See, eg,
	 * http://core.trac.wordpress.org/ticket/19888
	 *
	 * @since BuddyPress (1.8.0)
	 *
	 * @param array $a First set of arguments.
	 * @param array $b Second set of arguments.
	 * @return array Parsed arguments.
	 */
	public static function parse_args_r( &$a, $b ) {
		$a = (array) $a;
		$b = (array) $b;
		$r = $b;

		foreach ( $a as $k => &$v ) {
			if ( is_array( $v ) && isset( $r[ $k ] ) ) {
				$r[ $k ] = self::parse_args_r( $v, $r[ $k ] );
			} else {
				$r[ $k ] = $v;
			}
		}

		return $r;
	}

	/** Legacy Support ********************************************************/

	/*
	 * In BuddyPress 1.8, the recommended technique for configuring
	 * extensions changed from directly setting various object properties
	 * in the class constructor, to passing a configuration array to
	 * parent::init(). The following methods ensure that extensions created
	 * in the old way continue to work, by converting legacy configuration
	 * data to the new format.
	 */

	/**
	 * Provide access to otherwise unavailable object properties.
	 *
	 * This magic method is here for backward compatibility with plugins
	 * that refer to config properties that have moved to a different
	 * location (such as enable_create_step, which is now at
	 * $this->screens['create']['enabled']
	 *
	 * The legacy_properties array is set up in
	 * self::setup_legacy_properties().
	 *
	 * @since BuddyPress (1.8.0)
	 *
	 * @param string $key Property name.
	 * @return mixed The value if found, otherwise null.
	 */
	public function __get( $key ) {
		if ( isset( $this->legacy_properties[ $key ] ) ) {
			return $this->legacy_properties[ $key ];
		} elseif ( isset( $this->data[ $key ] ) ) {
			return $this->data[ $key ];
		} else {
			return null;
		}
	}

	/**
	 * Provide a fallback for isset( $this->foo ) when foo is unavailable.
	 *
	 * This magic method is here for backward compatibility with plugins
	 * that have set their class config options directly in the class
	 * constructor. The parse_legacy_properties() method of the current
	 * class needs to check whether any legacy keys have been put into the
	 * $this->data array.
	 *
	 * @since BuddyPress (1.8.0)
	 *
	 * @param string $key Property name.
	 * @return bool True if the value is set, otherwise false.
	 */
	public function __isset( $key ) {
		if ( isset( $this->legacy_properties[ $key ] ) ) {
			return true;
		} elseif ( isset( $this->data[ $key ] ) ) {
			return true;
		} else {
			return false;
		}
	}

	/**
	 * Allow plugins to set otherwise unavailable object properties.
	 *
	 * This magic method is here for backward compatibility with plugins
	 * that may attempt to modify the group extension by manually assigning
	 * a value to an object property that no longer exists, such as
	 * $this->enable_create_step.
	 *
	 * @since BuddyPress (1.8.0)
	 *
	 * @param string $key Property name.
	 * @param mixed $value Property value.
	 */
	public function __set( $key, $value ) {

		if ( empty( $this->initialized ) ) {
			$this->data[ $key ] = $value;
		}

		switch ( $key ) {
			case 'enable_create_step' :
				$this->screens['create']['enabled'] = $value;
				break;

			case 'enable_edit_item' :
				$this->screens['edit']['enabled'] = $value;
				break;

			case 'enable_admin_item' :
				$this->screens['admin']['enabled'] = $value;
				break;

			case 'create_step_position' :
				$this->screens['create']['position'] = $value;
				break;

			// Note: 'admin' becomes 'edit' to distinguish from Dashboard 'admin'
			case 'admin_name' :
				$this->screens['edit']['name'] = $value;
				break;

			case 'admin_slug' :
				$this->screens['edit']['slug'] = $value;
				break;

			case 'create_name' :
				$this->screens['create']['name'] = $value;
				break;

			case 'create_slug' :
				$this->screens['create']['slug'] = $value;
				break;

			case 'admin_metabox_context' :
				$this->screens['admin']['metabox_context'] = $value;
				break;

			case 'admin_metabox_priority' :
				$this->screens['admin']['metabox_priority'] = $value;
				break;

			default :
				$this->data[ $key ] = $value;
				break;
		}
	}

	/**
	 * Return a list of legacy properties.
	 *
	 * The legacy implementation of BP_Group_Extension used all of these
	 * object properties for configuration. Some have been moved.
	 *
	 * @since BuddyPress (1.8.0)
	 *
	 * @return array List of legacy property keys.
	 */
	protected function get_legacy_property_list() {
		return array(
			'name',
			'slug',
			'admin_name',
			'admin_slug',
			'create_name',
			'create_slug',
			'visibility',
			'create_step_position',
			'nav_item_position',
			'admin_metabox_context',
			'admin_metabox_priority',
			'enable_create_step',
			'enable_nav_item',
			'enable_edit_item',
			'enable_admin_item',
			'nav_item_name',
			'display_hook',
			'template_file',
		);
	}

	/**
	 * Parse legacy properties.
	 *
	 * The old standard for BP_Group_Extension was for plugins to register
	 * their settings as properties in their constructor. The new method is
	 * to pass a config array to the init() method. In order to support
	 * legacy plugins, we slurp up legacy properties, and later on we'll
	 * parse them into the new init() array.
	 *
	 * @since BuddyPress (1.8.0)
	 */
	protected function parse_legacy_properties() {

		// Only run this one time
		if ( ! empty( $this->legacy_properties_converted ) ) {
			return;
		}

		$properties = $this->get_legacy_property_list();

		// By-reference variable for convenience
		$lpc =& $this->legacy_properties_converted;

		foreach ( $properties as $property ) {

			// No legacy config exists for this key
			if ( ! isset( $this->{$property} ) ) {
				continue;
			}

			// Grab the value and record it as appropriate
			$value = $this->{$property};

			switch ( $property ) {
				case 'enable_create_step' :
					$lpc['screens']['create']['enabled'] = (bool) $value;
					break;

				case 'enable_edit_item' :
					$lpc['screens']['edit']['enabled'] = (bool) $value;
					break;

				case 'enable_admin_item' :
					$lpc['screens']['admin']['enabled'] = (bool) $value;
					break;

				case 'create_step_position' :
					$lpc['screens']['create']['position'] = $value;
					break;

				// Note: 'admin' becomes 'edit' to distinguish from Dashboard 'admin'
				case 'admin_name' :
					$lpc['screens']['edit']['name'] = $value;
					break;

				case 'admin_slug' :
					$lpc['screens']['edit']['slug'] = $value;
					break;

				case 'create_name' :
					$lpc['screens']['create']['name'] = $value;
					break;

				case 'create_slug' :
					$lpc['screens']['create']['slug'] = $value;
					break;

				case 'admin_metabox_context' :
					$lpc['screens']['admin']['metabox_context'] = $value;
					break;

				case 'admin_metabox_priority' :
					$lpc['screens']['admin']['metabox_priority'] = $value;
					break;

				default :
					$lpc[ $property ] = $value;
					break;
			}
		}
	}

	/**
	 * Set up legacy properties.
	 *
	 * This method is responsible for ensuring that all legacy config
	 * properties are stored in an array $this->legacy_properties, so that
	 * they remain available to plugins that reference the variables at
	 * their old locations.
	 *
	 * @since BuddyPress (1.8.0)
	 *
	 * @see BP_Group_Extension::__get()
	 */
	protected function setup_legacy_properties() {

		// Only run this one time
		if ( ! empty( $this->legacy_properties ) ) {
			return;
		}

		$properties = $this->get_legacy_property_list();
		$params     = $this->params;
		$lp         =& $this->legacy_properties;

		foreach ( $properties as $property ) {
			switch ( $property ) {
				case 'enable_create_step' :
					$lp['enable_create_step'] = $params['screens']['create']['enabled'];
					break;

				case 'enable_edit_item' :
					$lp['enable_edit_item'] = $params['screens']['edit']['enabled'];
					break;

				case 'enable_admin_item' :
					$lp['enable_admin_item'] = $params['screens']['admin']['enabled'];
					break;

				case 'create_step_position' :
					$lp['create_step_position'] = $params['screens']['create']['position'];
					break;

				// Note: 'admin' becomes 'edit' to distinguish from Dashboard 'admin'
				case 'admin_name' :
					$lp['admin_name'] = $params['screens']['edit']['name'];
					break;

				case 'admin_slug' :
					$lp['admin_slug'] = $params['screens']['edit']['slug'];
					break;

				case 'create_name' :
					$lp['create_name'] = $params['screens']['create']['name'];
					break;

				case 'create_slug' :
					$lp['create_slug'] = $params['screens']['create']['slug'];
					break;

				case 'admin_metabox_context' :
					$lp['admin_metabox_context'] = $params['screens']['admin']['metabox_context'];
					break;

				case 'admin_metabox_priority' :
					$lp['admin_metabox_priority'] = $params['screens']['admin']['metabox_priority'];
					break;

				default :
					// All other items get moved over
					$lp[ $property ] = $params[ $property ];

					// Also reapply to the object, for backpat
					$this->{$property} = $params[ $property ];

					break;
			}
		}
	}
}

/**
 * Register a new Group Extension.
 *
 * @param string Name of the Extension class.
 * @return bool|null Returns false on failure, otherwise null.
 */
function bp_register_group_extension( $group_extension_class = '' ) {

	if ( ! class_exists( $group_extension_class ) ) {
		return false;
	}

	// Register the group extension on the bp_init action so we have access
	// to all plugins.
	add_action( 'bp_init', create_function( '', '
		$extension = new ' . $group_extension_class . ';
		add_action( "bp_actions", array( &$extension, "_register" ), 8 );
		add_action( "admin_init", array( &$extension, "_register" ) );
	' ), 11 );
}