Skip to:
Content

BuddyPress.org

Changeset 9485


Ignore:
Timestamp:
02/15/2015 12:48:56 AM (10 years ago)
Author:
djpaul
Message:

Split each component's classes file, and move each individual class into its own file.

While historically manageable, the previous approach of having the majority of each component's classes in the same file is growing unwieldly with each new version of BuddyPress, and causing an avoidable increase in code complexity.

The existing -classes.php files are now a loading wrapper for the components' classes.

This change only affect classes that were explicitly declared within the -classes.php files. Any other classes, for example those in some components' template functions files, remain untouched for now.

Fixes #6083

Location:
trunk/src
Files:
50 added
9 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/bp-activity/bp-activity-classes.php

    r9385 r9485  
    1010defined( 'ABSPATH' ) || exit;
    1111
    12 /**
    13  * Database interaction class for the BuddyPress activity component.
    14  *
    15  * Instance methods are available for creating/editing an activity,
    16  * static methods for querying activities.
    17  *
    18  * @since BuddyPress (1.0)
    19  */
    20 class BP_Activity_Activity {
    21 
    22     /** Properties ************************************************************/
    23 
    24     /**
    25      * ID of the activity item.
    26      *
    27      * @var int
    28      */
    29     var $id;
    30 
    31     /**
    32      * ID of the associated item.
    33      *
    34      * @var int
    35      */
    36     var $item_id;
    37 
    38     /**
    39      * ID of the associated secondary item.
    40      *
    41      * @var int
    42      */
    43     var $secondary_item_id;
    44 
    45     /**
    46      * ID of user associated with the activity item.
    47      *
    48      * @var int
    49      */
    50     var $user_id;
    51 
    52     /**
    53      * The primary URL for the activity in RSS feeds.
    54      *
    55      * @var string
    56      */
    57     var $primary_link;
    58 
    59     /**
    60      * BuddyPress component the activity item relates to.
    61      *
    62      * @var string
    63      */
    64     var $component;
    65 
    66     /**
    67      * Activity type, eg 'new_blog_post'.
    68      *
    69      * @var string
    70      */
    71     var $type;
    72 
    73     /**
    74      * Description of the activity, eg 'Alex updated his profile.'
    75      *
    76      * @var string
    77      */
    78     var $action;
    79 
    80     /**
    81      * The content of the activity item.
    82      *
    83      * @var string
    84      */
    85     var $content;
    86 
    87     /**
    88      * The date the activity item was recorded, in 'Y-m-d h:i:s' format.
    89      *
    90      * @var string
    91      */
    92     var $date_recorded;
    93 
    94     /**
    95      * Whether the item should be hidden in sitewide streams.
    96      *
    97      * @var int
    98      */
    99     var $hide_sitewide = false;
    100 
    101     /**
    102      * Node boundary start for activity or activity comment.
    103      *
    104      * @var int
    105      */
    106     var $mptt_left;
    107 
    108     /**
    109      * Node boundary end for activity or activity comment.
    110      *
    111      * @var int
    112      */
    113     var $mptt_right;
    114 
    115     /**
    116      * Whether this item is marked as spam.
    117      *
    118      * @var int
    119      */
    120     var $is_spam;
    121 
    122     /**
    123      * Constructor method.
    124      *
    125      * @param int $id Optional. The ID of a specific activity item.
    126      */
    127     public function __construct( $id = false ) {
    128         if ( !empty( $id ) ) {
    129             $this->id = $id;
    130             $this->populate();
    131         }
    132     }
    133 
    134     /**
    135      * Populate the object with data about the specific activity item.
    136      */
    137     public function populate() {
    138         global $wpdb;
    139 
    140         $row = wp_cache_get( $this->id, 'bp_activity' );
    141 
    142         if ( false === $row ) {
    143             $bp  = buddypress();
    144             $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$bp->activity->table_name} WHERE id = %d", $this->id ) );
    145 
    146             wp_cache_set( $this->id, $row, 'bp_activity' );
    147         }
    148 
    149         if ( ! empty( $row ) ) {
    150             $this->id                = (int) $row->id;
    151             $this->item_id           = (int) $row->item_id;
    152             $this->secondary_item_id = (int) $row->secondary_item_id;
    153             $this->user_id           = (int) $row->user_id;
    154             $this->primary_link      = $row->primary_link;
    155             $this->component         = $row->component;
    156             $this->type              = $row->type;
    157             $this->action            = $row->action;
    158             $this->content           = $row->content;
    159             $this->date_recorded     = $row->date_recorded;
    160             $this->hide_sitewide     = $row->hide_sitewide;
    161             $this->mptt_left         = (int) $row->mptt_left;
    162             $this->mptt_right        = (int) $row->mptt_right;
    163             $this->is_spam           = $row->is_spam;
    164         }
    165 
    166         // Generate dynamic 'action' when possible
    167         $action = bp_activity_generate_action_string( $this );
    168         if ( false !== $action ) {
    169             $this->action = $action;
    170 
    171         // If no callback is available, use the literal string from
    172         // the database row
    173         } elseif ( ! empty( $row->action ) ) {
    174             $this->action = $row->action;
    175 
    176         // Provide a fallback to avoid PHP notices
    177         } else {
    178             $this->action = '';
    179         }
    180     }
    181 
    182     /**
    183      * Save the activity item to the database.
    184      *
    185      * @return bool True on success.
    186      */
    187     public function save() {
    188         global $wpdb;
    189 
    190         $bp = buddypress();
    191 
    192         $this->id                = apply_filters_ref_array( 'bp_activity_id_before_save',                array( $this->id,                &$this ) );
    193         $this->item_id           = apply_filters_ref_array( 'bp_activity_item_id_before_save',           array( $this->item_id,           &$this ) );
    194         $this->secondary_item_id = apply_filters_ref_array( 'bp_activity_secondary_item_id_before_save', array( $this->secondary_item_id, &$this ) );
    195         $this->user_id           = apply_filters_ref_array( 'bp_activity_user_id_before_save',           array( $this->user_id,           &$this ) );
    196         $this->primary_link      = apply_filters_ref_array( 'bp_activity_primary_link_before_save',      array( $this->primary_link,      &$this ) );
    197         $this->component         = apply_filters_ref_array( 'bp_activity_component_before_save',         array( $this->component,         &$this ) );
    198         $this->type              = apply_filters_ref_array( 'bp_activity_type_before_save',              array( $this->type,              &$this ) );
    199         $this->action            = apply_filters_ref_array( 'bp_activity_action_before_save',            array( $this->action,            &$this ) );
    200         $this->content           = apply_filters_ref_array( 'bp_activity_content_before_save',           array( $this->content,           &$this ) );
    201         $this->date_recorded     = apply_filters_ref_array( 'bp_activity_date_recorded_before_save',     array( $this->date_recorded,     &$this ) );
    202         $this->hide_sitewide     = apply_filters_ref_array( 'bp_activity_hide_sitewide_before_save',     array( $this->hide_sitewide,     &$this ) );
    203         $this->mptt_left         = apply_filters_ref_array( 'bp_activity_mptt_left_before_save',         array( $this->mptt_left,         &$this ) );
    204         $this->mptt_right        = apply_filters_ref_array( 'bp_activity_mptt_right_before_save',        array( $this->mptt_right,        &$this ) );
    205         $this->is_spam           = apply_filters_ref_array( 'bp_activity_is_spam_before_save',           array( $this->is_spam,           &$this ) );
    206 
    207         /**
    208          * Fires before the current activity item gets saved.
    209          *
    210          * Please use this hook to filter the properties above. Each part will be passed in.
    211          *
    212          * @since BuddyPress (1.0.0)
    213          *
    214          * @param BP_Activity_Activity Current instance of the activity item being saved.
    215          */
    216         do_action_ref_array( 'bp_activity_before_save', array( &$this ) );
    217 
    218         if ( empty( $this->component ) || empty( $this->type ) ) {
    219             return false;
    220         }
    221 
    222         if ( empty( $this->primary_link ) ) {
    223             $this->primary_link = bp_loggedin_user_domain();
    224         }
    225 
    226         // If we have an existing ID, update the activity item, otherwise insert it.
    227         if ( ! empty( $this->id ) ) {
    228             $q = $wpdb->prepare( "UPDATE {$bp->activity->table_name} SET user_id = %d, component = %s, type = %s, action = %s, content = %s, primary_link = %s, date_recorded = %s, item_id = %d, secondary_item_id = %d, hide_sitewide = %d, is_spam = %d WHERE id = %d", $this->user_id, $this->component, $this->type, $this->action, $this->content, $this->primary_link, $this->date_recorded, $this->item_id, $this->secondary_item_id, $this->hide_sitewide, $this->is_spam, $this->id );
    229         } else {
    230             $q = $wpdb->prepare( "INSERT INTO {$bp->activity->table_name} ( user_id, component, type, action, content, primary_link, date_recorded, item_id, secondary_item_id, hide_sitewide, is_spam ) VALUES ( %d, %s, %s, %s, %s, %s, %s, %d, %d, %d, %d )", $this->user_id, $this->component, $this->type, $this->action, $this->content, $this->primary_link, $this->date_recorded, $this->item_id, $this->secondary_item_id, $this->hide_sitewide, $this->is_spam );
    231         }
    232 
    233         if ( false === $wpdb->query( $q ) ) {
    234             return false;
    235         }
    236 
    237         // If this is a new activity item, set the $id property
    238         if ( empty( $this->id ) ) {
    239             $this->id = $wpdb->insert_id;
    240 
    241         // If an existing activity item, prevent any changes to the content generating new @mention notifications.
    242         } else {
    243             add_filter( 'bp_activity_at_name_do_notifications', '__return_false' );
    244         }
    245 
    246         /**
    247          * Fires after an activity item has been saved to the database.
    248          *
    249          * @since BuddyPress (1.0.0)
    250          *
    251          * @param BP_Activity_Activity Reference to current instance of activity being saved.
    252          */
    253         do_action_ref_array( 'bp_activity_after_save', array( &$this ) );
    254 
    255         return true;
    256     }
    257 
    258     /** Static Methods ***************************************************/
    259 
    260     /**
    261      * Get activity items, as specified by parameters
    262      *
    263      * @see BP_Activity_Activity::get_filter_sql() for a description of the
    264      *      'filter' parameter.
    265      * @see WP_Meta_Query::queries for a description of the 'meta_query'
    266      *      parameter format.
    267      *
    268      * @param array $args {
    269      *     An array of arguments. All items are optional.
    270      *
    271      *     @type int          $page              Which page of results to fetch. Using page=1 without per_page will result
    272      *                                           in no pagination. Default: 1.
    273      *     @type int|bool     $per_page          Number of results per page. Default: 25.
    274      *     @type int|bool     $max               Maximum number of results to return. Default: false (unlimited).
    275      *     @type string       $sort              ASC or DESC. Default: 'DESC'.
    276      *     @type array        $exclude           Array of activity IDs to exclude. Default: false.
    277      *     @type array        $in                Array of ids to limit query by (IN). Default: false.
    278      *     @type array        $meta_query        Array of meta_query conditions. See WP_Meta_Query::queries.
    279      *     @type array        $date_query        Array of date_query conditions. See first parameter of
    280      *                                           WP_Date_Query::__construct().
    281      *     @type array        $filter_query      Array of advanced query conditions. See BP_Activity_Query::__construct().
    282      *     @type string|array $scope             Pre-determined set of activity arguments.
    283      *     @type array        $filter            See BP_Activity_Activity::get_filter_sql().
    284      *     @type string       $search_terms      Limit results by a search term. Default: false.
    285      *     @type bool         $display_comments  Whether to include activity comments. Default: false.
    286      *     @type bool         $show_hidden       Whether to show items marked hide_sitewide. Default: false.
    287      *     @type string       $spam              Spam status. Default: 'ham_only'.
    288      *     @type bool         $update_meta_cache Whether to pre-fetch metadata for queried activity items. Default: true.
    289      *     @type string|bool  $count_total       If true, an additional DB query is run to count the total activity items
    290      *                                           for the query. Default: false.
    291      * }
    292      * @return array The array returned has two keys:
    293      *     - 'total' is the count of located activities
    294      *     - 'activities' is an array of the located activities
    295      */
    296     public static function get( $args = array() ) {
    297         global $wpdb;
    298 
    299         // Backward compatibility with old method of passing arguments
    300         if ( !is_array( $args ) || func_num_args() > 1 ) {
    301             _deprecated_argument( __METHOD__, '1.6', 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__ ) );
    302 
    303             $old_args_keys = array(
    304                 0 => 'max',
    305                 1 => 'page',
    306                 2 => 'per_page',
    307                 3 => 'sort',
    308                 4 => 'search_terms',
    309                 5 => 'filter',
    310                 6 => 'display_comments',
    311                 7 => 'show_hidden',
    312                 8 => 'exclude',
    313                 9 => 'in',
    314                 10 => 'spam'
    315             );
    316 
    317             $func_args = func_get_args();
    318             $args      = bp_core_parse_args_array( $old_args_keys, $func_args );
    319         }
    320 
    321         $bp = buddypress();
    322         $r  = wp_parse_args( $args, array(
    323             'page'              => 1,          // The current page
    324             'per_page'          => 25,         // Activity items per page
    325             'max'               => false,      // Max number of items to return
    326             'sort'              => 'DESC',     // ASC or DESC
    327             'exclude'           => false,      // Array of ids to exclude
    328             'in'                => false,      // Array of ids to limit query by (IN)
    329             'meta_query'        => false,      // Filter by activitymeta
    330             'date_query'        => false,      // Filter by date
    331             'filter_query'      => false,      // Advanced filtering - see BP_Activity_Query
    332             'filter'            => false,      // See self::get_filter_sql()
    333             'scope'             => false,      // Preset activity arguments
    334             'search_terms'      => false,      // Terms to search by
    335             'display_comments'  => false,      // Whether to include activity comments
    336             'show_hidden'       => false,      // Show items marked hide_sitewide
    337             'spam'              => 'ham_only', // Spam status
    338             'update_meta_cache' => true,
    339             'count_total'       => false,
    340         ) );
    341 
    342         // Select conditions
    343         $select_sql = "SELECT DISTINCT a.id";
    344 
    345         $from_sql   = " FROM {$bp->activity->table_name} a";
    346 
    347         $join_sql   = '';
    348 
    349         // Where conditions
    350         $where_conditions = array();
    351 
    352         // Excluded types
    353         $excluded_types = array();
    354 
    355         // Scope takes precedence
    356         if ( ! empty( $r['scope'] ) ) {
    357             $scope_query = self::get_scope_query_sql( $r['scope'], $r );
    358 
    359             // Add our SQL conditions if matches were found
    360             if ( ! empty( $scope_query['sql'] ) ) {
    361                 $where_conditions['scope_query_sql'] = $scope_query['sql'];
    362             }
    363 
    364             // override some arguments if needed
    365             if ( ! empty( $scope_query['override'] ) ) {
    366                 $r = self::array_replace_recursive( $r, $scope_query['override'] );
    367             }
    368 
    369         // Advanced filtering
    370         } elseif ( ! empty( $r['filter_query'] ) ) {
    371             $filter_query = new BP_Activity_Query( $r['filter_query'] );
    372             $sql          = $filter_query->get_sql();
    373             if ( ! empty( $sql ) ) {
    374                 $where_conditions['filter_query_sql'] = $sql;
    375             }
    376         }
    377 
    378         // Regular filtering
    379         if ( $r['filter'] && $filter_sql = BP_Activity_Activity::get_filter_sql( $r['filter'] ) ) {
    380             $where_conditions['filter_sql'] = $filter_sql;
    381         }
    382 
    383         // Spam
    384         if ( 'ham_only' == $r['spam'] ) {
    385             $where_conditions['spam_sql'] = 'a.is_spam = 0';
    386         } elseif ( 'spam_only' == $r['spam'] ) {
    387             $where_conditions['spam_sql'] = 'a.is_spam = 1';
    388         }
    389 
    390         // Searching
    391         if ( $r['search_terms'] ) {
    392             $search_terms_like = '%' . bp_esc_like( $r['search_terms'] ) . '%';
    393             $where_conditions['search_sql'] = $wpdb->prepare( 'a.content LIKE %s', $search_terms_like );
    394         }
    395 
    396         // Sorting
    397         $sort = $r['sort'];
    398         if ( $sort != 'ASC' && $sort != 'DESC' ) {
    399             $sort = 'DESC';
    400         }
    401 
    402         // Hide Hidden Items?
    403         if ( ! $r['show_hidden'] ) {
    404             $where_conditions['hidden_sql'] = "a.hide_sitewide = 0";
    405         }
    406 
    407         // Exclude specified items
    408         if ( ! empty( $r['exclude'] ) ) {
    409             $exclude = implode( ',', wp_parse_id_list( $r['exclude'] ) );
    410             $where_conditions['exclude'] = "a.id NOT IN ({$exclude})";
    411         }
    412 
    413         // The specific ids to which you want to limit the query
    414         if ( ! empty( $r['in'] ) ) {
    415             $in = implode( ',', wp_parse_id_list( $r['in'] ) );
    416             $where_conditions['in'] = "a.id IN ({$in})";
    417         }
    418 
    419         // Process meta_query into SQL
    420         $meta_query_sql = self::get_meta_query_sql( $r['meta_query'] );
    421 
    422         if ( ! empty( $meta_query_sql['join'] ) ) {
    423             $join_sql .= $meta_query_sql['join'];
    424         }
    425 
    426         if ( ! empty( $meta_query_sql['where'] ) ) {
    427             $where_conditions[] = $meta_query_sql['where'];
    428         }
    429 
    430         // Process date_query into SQL
    431         $date_query_sql = self::get_date_query_sql( $r['date_query'] );
    432 
    433         if ( ! empty( $date_query_sql ) ) {
    434             $where_conditions['date'] = $date_query_sql;
    435         }
    436 
    437         // Alter the query based on whether we want to show activity item
    438         // comments in the stream like normal comments or threaded below
    439         // the activity.
    440         if ( false === $r['display_comments'] || 'threaded' === $r['display_comments'] ) {
    441             $excluded_types[] = 'activity_comment';
    442         }
    443 
    444         // Exclude 'last_activity' items unless the 'action' filter has
    445         // been explicitly set
    446         if ( empty( $r['filter']['object'] ) ) {
    447             $excluded_types[] = 'last_activity';
    448         }
    449 
    450         // Build the excluded type sql part
    451         if ( ! empty( $excluded_types ) ) {
    452             $not_in = "'" . implode( "', '", esc_sql( $excluded_types ) ) . "'";
    453             $where_conditions['excluded_types'] = "a.type NOT IN ({$not_in})";
    454         }
    455 
    456         /**
    457          * Filters the MySQL WHERE conditions for the Activity items get method.
    458          *
    459          * @since BuddyPress (1.9.0)
    460          *
    461          * @param array  $where_conditions Current conditions for MySQL WHERE statement.
    462          * @param array  $r Parsed arguments passed into method.
    463          * @param string $select_sql Current SELECT MySQL statement at point of execution.
    464          * @param string $from_sql Current FROM MySQL statement at point of execution.
    465          * @param string $join_sql Current INNER JOIN MySQL statement at point of execution.
    466          */
    467         $where_conditions = apply_filters( 'bp_activity_get_where_conditions', $where_conditions, $r, $select_sql, $from_sql, $join_sql );
    468 
    469         // Join the where conditions together
    470         $where_sql = 'WHERE ' . join( ' AND ', $where_conditions );
    471 
    472         /**
    473          * Filters the preferred order of indexes for activity item.
    474          *
    475          * @since BuddyPress (1.6.0)
    476          *
    477          * @param array Array of indexes in preferred order.
    478          */
    479         $indexes = apply_filters( 'bp_activity_preferred_index_order', array( 'user_id', 'item_id', 'secondary_item_id', 'date_recorded', 'component', 'type', 'hide_sitewide', 'is_spam' ) );
    480 
    481         foreach( $indexes as $key => $index ) {
    482             if ( false !== strpos( $where_sql, $index ) ) {
    483                 $the_index = $index;
    484                 break; // Take the first one we find
    485             }
    486         }
    487 
    488         if ( !empty( $the_index ) ) {
    489             $index_hint_sql = "USE INDEX ({$the_index})";
    490         } else {
    491             $index_hint_sql = '';
    492         }
    493 
    494         // Sanitize page and per_page parameters
    495         $page     = absint( $r['page']     );
    496         $per_page = absint( $r['per_page'] );
    497 
    498         $retval = array(
    499             'activities'     => null,
    500             'total'          => null,
    501             'has_more_items' => null,
    502         );
    503 
    504         /**
    505          * Filters if BuddyPress should use legacy query structure over current structure for version 2.0+.
    506          *
    507          * It is not recommended to use the legacy structure, but allowed to if needed.
    508          *
    509          * @since BuddyPress (2.0.0)
    510          *
    511          * @param bool                 Whether to use legacy structure or not.
    512          * @param BP_Activity_Activity Current method being called.
    513          * @param array                $r Parsed arguments passed into method.
    514          */
    515         if ( apply_filters( 'bp_use_legacy_activity_query', false, __METHOD__, $r ) ) {
    516 
    517             // Legacy queries joined against the user table
    518             $select_sql = "SELECT DISTINCT a.*, u.user_email, u.user_nicename, u.user_login, u.display_name";
    519             $from_sql   = " FROM {$bp->activity->table_name} a LEFT JOIN {$wpdb->users} u ON a.user_id = u.ID";
    520 
    521             if ( ! empty( $page ) && ! empty( $per_page ) ) {
    522                 $pag_sql    = $wpdb->prepare( "LIMIT %d, %d", absint( ( $page - 1 ) * $per_page ), $per_page );
    523 
    524                 /** this filter is documented in bp-activity/bp-activity-classes.php */
    525                 $activities = $wpdb->get_results( apply_filters( 'bp_activity_get_user_join_filter', "{$select_sql} {$from_sql} {$join_sql} {$where_sql} ORDER BY a.date_recorded {$sort} {$pag_sql}", $select_sql, $from_sql, $where_sql, $sort, $pag_sql ) );
    526             } else {
    527                 $pag_sql    = '';
    528 
    529                 /**
    530                  * Filters the legacy MySQL query statement so plugins can alter before results are fetched.
    531                  *
    532                  * @since BuddyPress (1.5.0)
    533                  *
    534                  * @param string Concatenated MySQL statement pieces to be query results with for legacy query.
    535                  * @param string $select_sql Final SELECT MySQL statement portion for legacy query.
    536                  * @param string $from_sql Final FROM MySQL statement portion for legacy query.
    537                  * @param string $where_sql Final WHERE MySQL statement portion for legacy query.
    538                  * @param string $sort Final sort direction for legacy query.
    539                  */
    540                 $activities = $wpdb->get_results( apply_filters( 'bp_activity_get_user_join_filter', "{$select_sql} {$from_sql} {$join_sql} {$where_sql} ORDER BY a.date_recorded {$sort}", $select_sql, $from_sql, $where_sql, $sort, $pag_sql ) );
    541             }
    542 
    543         } else {
    544             // Query first for activity IDs
    545             $activity_ids_sql = "{$select_sql} {$from_sql} {$join_sql} {$where_sql} ORDER BY a.date_recorded {$sort}";
    546 
    547             if ( ! empty( $per_page ) && ! empty( $page ) ) {
    548                 // We query for $per_page + 1 items in order to
    549                 // populate the has_more_items flag
    550                 $activity_ids_sql .= $wpdb->prepare( " LIMIT %d, %d", absint( ( $page - 1 ) * $per_page ), $per_page + 1 );
    551             }
    552 
    553             /**
    554              * Filters the paged activities MySQL statement.
    555              *
    556              * @since BuddyPress (2.0.0)
    557              *
    558              * @param string $activity_ids_sql MySQL statement used to query for Activity IDs.
    559              * @param array  $r Array of arguments passed into method.
    560              */
    561             $activity_ids_sql = apply_filters( 'bp_activity_paged_activities_sql', $activity_ids_sql, $r );
    562 
    563             $activity_ids = $wpdb->get_col( $activity_ids_sql );
    564 
    565             $retval['has_more_items'] = ! empty( $per_page ) && count( $activity_ids ) > $per_page;
    566 
    567             // If we've fetched more than the $per_page value, we
    568             // can discard the extra now
    569             if ( ! empty( $per_page ) && count( $activity_ids ) === $per_page + 1 ) {
    570                 array_pop( $activity_ids );
    571             }
    572 
    573             $activities = self::get_activity_data( $activity_ids );
    574         }
    575 
    576         // Get the fullnames of users so we don't have to query in the loop
    577         $activities = self::append_user_fullnames( $activities );
    578 
    579         // Get activity meta
    580         $activity_ids = array();
    581         foreach ( (array) $activities as $activity ) {
    582             $activity_ids[] = $activity->id;
    583         }
    584 
    585         if ( ! empty( $activity_ids ) && $r['update_meta_cache'] ) {
    586             bp_activity_update_meta_cache( $activity_ids );
    587         }
    588 
    589         if ( $activities && $r['display_comments'] ) {
    590             $activities = BP_Activity_Activity::append_comments( $activities, $r['spam'] );
    591         }
    592 
    593         // Pre-fetch data associated with activity users and other objects
    594         BP_Activity_Activity::prefetch_object_data( $activities );
    595 
    596         // Generate action strings
    597         $activities = BP_Activity_Activity::generate_action_strings( $activities );
    598 
    599         $retval['activities'] = $activities;
    600 
    601         // If $max is set, only return up to the max results
    602         if ( ! empty( $r['count_total'] ) ) {
    603 
    604             /**
    605              * Filters the total activities MySQL statement.
    606              *
    607              * @since BuddyPress (1.5.0)
    608              *
    609              * @param string MySQL statement used to query for total activities.
    610              * @param string $where_sql MySQL WHERE statement portion.
    611              * @param string $sort sort direction for query.
    612              */
    613             $total_activities_sql = apply_filters( 'bp_activity_total_activities_sql', "SELECT count(DISTINCT a.id) FROM {$bp->activity->table_name} a {$join_sql} {$where_sql}", $where_sql, $sort );
    614             $total_activities     = $wpdb->get_var( $total_activities_sql );
    615 
    616             if ( !empty( $r['max'] ) ) {
    617                 if ( (int) $total_activities > (int) $r['max'] ) {
    618                     $total_activities = $r['max'];
    619                 }
    620             }
    621 
    622             $retval['total'] = $total_activities;
    623         }
    624 
    625         return $retval;
    626     }
    627 
    628     /**
    629      * Convert activity IDs to activity objects, as expected in template loop.
    630      *
    631      * @since 2.0
    632      *
    633      * @param array $activity_ids Array of activity IDs.
    634      * @return array
    635      */
    636     protected static function get_activity_data( $activity_ids = array() ) {
    637         global $wpdb;
    638 
    639         // Bail if no activity ID's passed
    640         if ( empty( $activity_ids ) ) {
    641             return array();
    642         }
    643 
    644         // Get BuddyPress
    645         $bp = buddypress();
    646 
    647         $activities   = array();
    648         $uncached_ids = bp_get_non_cached_ids( $activity_ids, 'bp_activity' );
    649 
    650         // Prime caches as necessary
    651         if ( ! empty( $uncached_ids ) ) {
    652             // Format the activity ID's for use in the query below
    653             $uncached_ids_sql = implode( ',', wp_parse_id_list( $uncached_ids ) );
    654 
    655             // Fetch data from activity table, preserving order
    656             $queried_adata = $wpdb->get_results( "SELECT * FROM {$bp->activity->table_name} WHERE id IN ({$uncached_ids_sql})");
    657 
    658             // Put that data into the placeholders created earlier,
    659             // and add it to the cache
    660             foreach ( (array) $queried_adata as $adata ) {
    661                 wp_cache_set( $adata->id, $adata, 'bp_activity' );
    662             }
    663         }
    664 
    665         // Now fetch data from the cache
    666         foreach ( $activity_ids as $activity_id ) {
    667             $activities[] = wp_cache_get( $activity_id, 'bp_activity' );
    668         }
    669 
    670         // Then fetch user data
    671         $user_query = new BP_User_Query( array(
    672             'user_ids'        => wp_list_pluck( $activities, 'user_id' ),
    673             'populate_extras' => false,
    674         ) );
    675 
    676         // Associated located user data with activity items
    677         foreach ( $activities as $a_index => $a_item ) {
    678             $a_user_id = intval( $a_item->user_id );
    679             $a_user    = isset( $user_query->results[ $a_user_id ] ) ? $user_query->results[ $a_user_id ] : '';
    680 
    681             if ( !empty( $a_user ) ) {
    682                 $activities[ $a_index ]->user_email    = $a_user->user_email;
    683                 $activities[ $a_index ]->user_nicename = $a_user->user_nicename;
    684                 $activities[ $a_index ]->user_login    = $a_user->user_login;
    685                 $activities[ $a_index ]->display_name  = $a_user->display_name;
    686             }
    687         }
    688 
    689         return $activities;
    690     }
    691 
    692     /**
    693      * Append xProfile fullnames to an activity array.
    694      *
    695      * @since BuddyPress (2.0.0)
    696      *
    697      * @param array $activities Activities array.
    698      * @return array
    699      */
    700     protected static function append_user_fullnames( $activities ) {
    701 
    702         if ( bp_is_active( 'xprofile' ) && ! empty( $activities ) ) {
    703             $activity_user_ids = wp_list_pluck( $activities, 'user_id' );
    704 
    705             if ( ! empty( $activity_user_ids ) ) {
    706                 $fullnames = bp_core_get_user_displaynames( $activity_user_ids );
    707                 if ( ! empty( $fullnames ) ) {
    708                     foreach ( (array) $activities as $i => $activity ) {
    709                         if ( ! empty( $fullnames[ $activity->user_id ] ) ) {
    710                             $activities[ $i ]->user_fullname = $fullnames[ $activity->user_id ];
    711                         }
    712                     }
    713                 }
    714             }
    715         }
    716 
    717         return $activities;
    718     }
    719 
    720     /**
    721      * Pre-fetch data for objects associated with activity items.
    722      *
    723      * Activity items are associated with users, and often with other
    724      * BuddyPress data objects. Here, we pre-fetch data about these
    725      * associated objects, so that inline lookups - done primarily when
    726      * building action strings - do not result in excess database queries.
    727      *
    728      * The only object data required for activity component activity types
    729      * (activity_update and activity_comment) is related to users, and that
    730      * info is fetched separately in BP_Activity_Activity::get_activity_data().
    731      * So this method contains nothing but a filter that allows other
    732      * components, such as bp-friends and bp-groups, to hook in and prime
    733      * their own caches at the beginning of an activity loop.
    734      *
    735      * @since BuddyPress (2.0.0)
    736      *
    737      * @param array $activities Array of activities.
    738      */
    739     protected static function prefetch_object_data( $activities ) {
    740 
    741         /**
    742          * Filters inside prefetch_object_data method to aid in pre-fetching object data associated with activity item.
    743          *
    744          * @since BuddyPress (2.0.0)
    745          *
    746          * @param array $activities Array of activities.
    747          */
    748         return apply_filters( 'bp_activity_prefetch_object_data', $activities );
    749     }
    750 
    751     /**
    752      * Generate action strings for the activities located in BP_Activity_Activity::get().
    753      *
    754      * If no string can be dynamically generated for a given item
    755      * (typically because the activity type has not been properly
    756      * registered), the static 'action' value pulled from the database will
    757      * be left in place.
    758      *
    759      * @since BuddyPress (2.0.0)
    760      *
    761      * @param array $activities Array of activities.
    762      * @return array
    763      */
    764     protected static function generate_action_strings( $activities ) {
    765         foreach ( $activities as $key => $activity ) {
    766             $generated_action = bp_activity_generate_action_string( $activity );
    767             if ( false !== $generated_action ) {
    768                 $activity->action = $generated_action;
    769             }
    770 
    771             $activities[ $key ] = $activity;
    772         }
    773 
    774         return $activities;
    775     }
    776 
    777     /**
    778      * Get the SQL for the 'meta_query' param in BP_Activity_Activity::get().
    779      *
    780      * We use WP_Meta_Query to do the heavy lifting of parsing the
    781      * meta_query array and creating the necessary SQL clauses. However,
    782      * since BP_Activity_Activity::get() builds its SQL differently than
    783      * WP_Query, we have to alter the return value (stripping the leading
    784      * AND keyword from the 'where' clause).
    785      *
    786      * @since BuddyPress (1.8)
    787      *
    788      * @param array $meta_query An array of meta_query filters. See the
    789      *   documentation for WP_Meta_Query for details.
    790      * @return array $sql_array 'join' and 'where' clauses.
    791      */
    792     public static function get_meta_query_sql( $meta_query = array() ) {
    793         global $wpdb;
    794 
    795         $sql_array = array(
    796             'join'  => '',
    797             'where' => '',
    798         );
    799 
    800         if ( ! empty( $meta_query ) ) {
    801             $activity_meta_query = new WP_Meta_Query( $meta_query );
    802 
    803             // WP_Meta_Query expects the table name at
    804             // $wpdb->activitymeta
    805             $wpdb->activitymeta = buddypress()->activity->table_name_meta;
    806 
    807             $meta_sql = $activity_meta_query->get_sql( 'activity', 'a', 'id' );
    808 
    809             // Strip the leading AND - BP handles it in get()
    810             $sql_array['where'] = preg_replace( '/^\sAND/', '', $meta_sql['where'] );
    811             $sql_array['join']  = $meta_sql['join'];
    812         }
    813 
    814         return $sql_array;
    815     }
    816 
    817     /**
    818      * Get the SQL for the 'date_query' param in BP_Activity_Activity::get().
    819      *
    820      * We use BP_Date_Query, which extends WP_Date_Query, to do the heavy lifting
    821      * of parsing the date_query array and creating the necessary SQL clauses.
    822      * However, since BP_Activity_Activity::get() builds its SQL differently than
    823      * WP_Query, we have to alter the return value (stripping the leading AND
    824      * keyword from the query).
    825      *
    826      * @since BuddyPress (2.1.0)
    827      *
    828      * @param array $date_query An array of date_query parameters. See the
    829      *        documentation for the first parameter of WP_Date_Query.
    830      * @return string
    831      */
    832     public static function get_date_query_sql( $date_query = array() ) {
    833         $sql = '';
    834 
    835         // Date query
    836         if ( ! empty( $date_query ) && is_array( $date_query ) && class_exists( 'BP_Date_Query' ) ) {
    837             $date_query = new BP_Date_Query( $date_query, 'date_recorded' );
    838             $sql = preg_replace( '/^\sAND/', '', $date_query->get_sql() );
    839         }
    840 
    841         return $sql;
    842     }
    843 
    844     /**
    845      * Get the SQL for the 'scope' param in BP_Activity_Activity::get().
    846      *
    847      * A scope is a predetermined set of activity arguments.  This method is used
    848      * to grab these activity arguments and override any existing args if needed.
    849      *
    850      * Can handle multiple scopes.
    851      *
    852      * @since BuddyPress (2.2.0)
    853      *
    854      * @param  mixed $scope  The activity scope. Accepts string or array of scopes
    855      * @param  array $r      Current activity arguments. Same as those of BP_Activity_Activity::get(),
    856      *                       but merged with defaults.
    857      * @return array 'sql' WHERE SQL string and 'override' activity args
    858      */
    859     public static function get_scope_query_sql( $scope = false, $r = array() ) {
    860 
    861         // Define arrays for future use
    862         $query_args = array();
    863         $override   = array();
    864         $retval     = array();
    865 
    866         // Check for array of scopes
    867         if ( is_array( $scope ) ) {
    868             $scopes = $scope;
    869 
    870         // Explode a comma separated string of scopes
    871         } elseif ( is_string( $scope ) ) {
    872             $scopes = explode( ',', $scope );
    873         }
    874 
    875         // Bail if no scope passed
    876         if ( empty( $scopes ) ) {
    877             return false;
    878         }
    879 
    880         // Helper to easily grab the 'user_id'
    881         if ( ! empty( $r['filter']['user_id'] ) ) {
    882             $r['user_id'] = $r['filter']['user_id'];
    883         }
    884 
    885         // parse each scope; yes! we handle multiples!
    886         foreach ( $scopes as $scope ) {
    887             $scope_args = array();
    888 
    889             /**
    890              * Plugins can hook here to set their activity arguments for custom scopes.
    891              *
    892              * This is a dynamic filter based on the activity scope. eg:
    893              *   - 'bp_activity_set_groups_scope_args'
    894              *   - 'bp_activity_set_friends_scope_args'
    895              *
    896              * To see how this filter is used, plugin devs should check out:
    897              *   - bp_groups_filter_activity_scope() - used for 'groups' scope
    898              *   - bp_friends_filter_activity_scope() - used for 'friends' scope
    899              *
    900              * @since BuddyPress (2.2.0)
    901              *
    902              *  @param array {
    903              *     Activity query clauses.
    904              *
    905              *     @type array {
    906              *         Activity arguments for your custom scope.
    907              *         See {@link BP_Activity_Query::_construct()} for more details.
    908              *     }
    909              *     @type array $override Optional. Override existing activity arguments passed by $r.
    910              * }
    911              * @param array $r Current activity arguments passed in BP_Activity_Activity::get()
    912              */
    913             $scope_args = apply_filters( "bp_activity_set_{$scope}_scope_args", array(), $r );
    914 
    915             if ( ! empty( $scope_args ) ) {
    916                 // merge override properties from other scopes
    917                 // this might be a problem...
    918                 if ( ! empty( $scope_args['override'] ) ) {
    919                     $override = array_merge( $override, $scope_args['override'] );
    920                     unset( $scope_args['override'] );
    921                 }
    922 
    923                 // save scope args
    924                 if ( ! empty( $scope_args ) ) {
    925                     $query_args[] = $scope_args;
    926                 }
    927             }
    928         }
    929 
    930         if ( ! empty( $query_args ) ) {
    931             // set relation to OR
    932             $query_args['relation'] = 'OR';
    933 
    934             $query = new BP_Activity_Query( $query_args );
    935             $sql   = $query->get_sql();
    936             if ( ! empty( $sql ) ) {
    937                 $retval['sql'] = $sql;
    938             }
    939         }
    940 
    941         if ( ! empty( $override ) ) {
    942             $retval['override'] = $override;
    943         }
    944 
    945         return $retval;
    946     }
    947 
    948     /**
    949      * In BuddyPress 1.2.x, this was used to retrieve specific activity stream items (for example, on an activity's permalink page).
    950      *
    951      * As of 1.5.x, use BP_Activity_Activity::get() with an 'in' parameter instead.
    952      *
    953      * @since BuddyPress (1.2)
    954      *
    955      * @deprecated 1.5
    956      * @deprecated Use BP_Activity_Activity::get() with an 'in' parameter instead.
    957      *
    958      * @param mixed $activity_ids Array or comma-separated string of activity IDs to retrieve
    959      * @param int $max Maximum number of results to return. (Optional; default is no maximum)
    960      * @param int $page The set of results that the user is viewing. Used in pagination. (Optional; default is 1)
    961      * @param int $per_page Specifies how many results per page. Used in pagination. (Optional; default is 25)
    962      * @param string MySQL column sort; ASC or DESC. (Optional; default is DESC)
    963      * @param bool $display_comments Retrieve an activity item's associated comments or not. (Optional; default is false)
    964      * @return array
    965      */
    966     public static function get_specific( $activity_ids, $max = false, $page = 1, $per_page = 25, $sort = 'DESC', $display_comments = false ) {
    967         _deprecated_function( __FUNCTION__, '1.5', 'Use BP_Activity_Activity::get() with the "in" parameter instead.' );
    968         return BP_Activity_Activity::get( $max, $page, $per_page, $sort, false, false, $display_comments, false, false, $activity_ids );
    969     }
    970 
    971     /**
    972      * Get the first activity ID that matches a set of criteria.
    973      *
    974      * @param int    $user_id           User ID to filter by
    975      * @param string $component         Component to filter by
    976      * @param string $type              Activity type to filter by
    977      * @param int    $item_id           Associated item to filter by
    978      * @param int    $secondary_item_id Secondary associated item to filter by
    979      * @param string $action            Action to filter by
    980      * @param string $content           Content to filter by
    981      * @param string $date_recorded     Date to filter by
    982      *
    983      * @todo Should parameters be optional?
    984      *
    985      * @return int|bool Activity ID on success, false if none is found.
    986      */
    987     public static function get_id( $user_id, $component, $type, $item_id, $secondary_item_id, $action, $content, $date_recorded ) {
    988         global $wpdb;
    989 
    990         $bp = buddypress();
    991 
    992         $where_args = false;
    993 
    994         if ( ! empty( $user_id ) ) {
    995             $where_args[] = $wpdb->prepare( "user_id = %d", $user_id );
    996         }
    997 
    998         if ( ! empty( $component ) ) {
    999             $where_args[] = $wpdb->prepare( "component = %s", $component );
    1000         }
    1001 
    1002         if ( ! empty( $type ) ) {
    1003             $where_args[] = $wpdb->prepare( "type = %s", $type );
    1004         }
    1005 
    1006         if ( ! empty( $item_id ) ) {
    1007             $where_args[] = $wpdb->prepare( "item_id = %d", $item_id );
    1008         }
    1009 
    1010         if ( ! empty( $secondary_item_id ) ) {
    1011             $where_args[] = $wpdb->prepare( "secondary_item_id = %d", $secondary_item_id );
    1012         }
    1013 
    1014         if ( ! empty( $action ) ) {
    1015             $where_args[] = $wpdb->prepare( "action = %s", $action );
    1016         }
    1017 
    1018         if ( ! empty( $content ) ) {
    1019             $where_args[] = $wpdb->prepare( "content = %s", $content );
    1020         }
    1021 
    1022         if ( ! empty( $date_recorded ) ) {
    1023             $where_args[] = $wpdb->prepare( "date_recorded = %s", $date_recorded );
    1024         }
    1025 
    1026         if ( ! empty( $where_args ) ) {
    1027             $where_sql = 'WHERE ' . join( ' AND ', $where_args );
    1028             return $wpdb->get_var( "SELECT id FROM {$bp->activity->table_name} {$where_sql}" );
    1029         }
    1030 
    1031         return false;
    1032     }
    1033 
    1034     /**
    1035      * Delete activity items from the database.
    1036      *
    1037      * To delete a specific activity item, pass an 'id' parameter.
    1038      * Otherwise use the filters.
    1039      *
    1040      * @since BuddyPress (1.2)
    1041      *
    1042      * @param array $args {
    1043      *     @int $id Optional. The ID of a specific item to delete.
    1044      *     @string $action Optional. The action to filter by.
    1045      *     @string $content Optional. The content to filter by.
    1046      *     @string $component Optional. The component name to filter by.
    1047      *     @string $type Optional. The activity type to filter by.
    1048      *     @string $primary_link Optional. The primary URL to filter by.
    1049      *     @int $user_id Optional. The user ID to filter by.
    1050      *     @int $item_id Optional. The associated item ID to filter by.
    1051      *     @int $secondary_item_id Optional. The secondary associated item ID to filter by.
    1052      *     @string $date_recorded Optional. The date to filter by.
    1053      *     @int $hide_sitewide Optional. Default: false.
    1054      * }
    1055      * @return array|bool An array of deleted activity IDs on success, false on failure.
    1056      */
    1057     public static function delete( $args = array() ) {
    1058         global $wpdb;
    1059 
    1060         $bp = buddypress();
    1061 
    1062         $defaults = array(
    1063             'id'                => false,
    1064             'action'            => false,
    1065             'content'           => false,
    1066             'component'         => false,
    1067             'type'              => false,
    1068             'primary_link'      => false,
    1069             'user_id'           => false,
    1070             'item_id'           => false,
    1071             'secondary_item_id' => false,
    1072             'date_recorded'     => false,
    1073             'hide_sitewide'     => false
    1074         );
    1075         $params = wp_parse_args( $args, $defaults );
    1076         extract( $params );
    1077 
    1078         $where_args = false;
    1079 
    1080         if ( !empty( $id ) )
    1081             $where_args[] = $wpdb->prepare( "id = %d", $id );
    1082 
    1083         if ( !empty( $user_id ) )
    1084             $where_args[] = $wpdb->prepare( "user_id = %d", $user_id );
    1085 
    1086         if ( !empty( $action ) )
    1087             $where_args[] = $wpdb->prepare( "action = %s", $action );
    1088 
    1089         if ( !empty( $content ) )
    1090             $where_args[] = $wpdb->prepare( "content = %s", $content );
    1091 
    1092         if ( !empty( $component ) )
    1093             $where_args[] = $wpdb->prepare( "component = %s", $component );
    1094 
    1095         if ( !empty( $type ) )
    1096             $where_args[] = $wpdb->prepare( "type = %s", $type );
    1097 
    1098         if ( !empty( $primary_link ) )
    1099             $where_args[] = $wpdb->prepare( "primary_link = %s", $primary_link );
    1100 
    1101         if ( !empty( $item_id ) )
    1102             $where_args[] = $wpdb->prepare( "item_id = %d", $item_id );
    1103 
    1104         if ( !empty( $secondary_item_id ) )
    1105             $where_args[] = $wpdb->prepare( "secondary_item_id = %d", $secondary_item_id );
    1106 
    1107         if ( !empty( $date_recorded ) )
    1108             $where_args[] = $wpdb->prepare( "date_recorded = %s", $date_recorded );
    1109 
    1110         if ( !empty( $hide_sitewide ) )
    1111             $where_args[] = $wpdb->prepare( "hide_sitewide = %d", $hide_sitewide );
    1112 
    1113         if ( !empty( $where_args ) )
    1114             $where_sql = 'WHERE ' . join( ' AND ', $where_args );
    1115         else
    1116             return false;
    1117 
    1118         // Fetch the activity IDs so we can delete any comments for this activity item
    1119         $activity_ids = $wpdb->get_col( "SELECT id FROM {$bp->activity->table_name} {$where_sql}" );
    1120 
    1121         if ( ! $wpdb->query( "DELETE FROM {$bp->activity->table_name} {$where_sql}" ) ) {
    1122             return false;
    1123         }
    1124 
    1125         // Handle accompanying activity comments and meta deletion
    1126         if ( $activity_ids ) {
    1127             $activity_ids_comma          = implode( ',', wp_parse_id_list( $activity_ids ) );
    1128             $activity_comments_where_sql = "WHERE type = 'activity_comment' AND item_id IN ({$activity_ids_comma})";
    1129 
    1130             // Fetch the activity comment IDs for our deleted activity items
    1131             $activity_comment_ids = $wpdb->get_col( "SELECT id FROM {$bp->activity->table_name} {$activity_comments_where_sql}" );
    1132 
    1133             // We have activity comments!
    1134             if ( ! empty( $activity_comment_ids ) ) {
    1135                 // Delete activity comments
    1136                 $wpdb->query( "DELETE FROM {$bp->activity->table_name} {$activity_comments_where_sql}" );
    1137 
    1138                 // Merge activity IDs with activity comment IDs
    1139                 $activity_ids = array_merge( $activity_ids, $activity_comment_ids );
    1140             }
    1141 
    1142             // Delete all activity meta entries for activity items and activity comments
    1143             BP_Activity_Activity::delete_activity_meta_entries( $activity_ids );
    1144         }
    1145 
    1146         return $activity_ids;
    1147     }
    1148 
    1149     /**
    1150      * Delete the comments associated with a set of activity items.
    1151      *
    1152      * @since BuddyPress (1.2)
    1153      *
    1154      * @todo Mark as deprecated?  Method is no longer used internally.
    1155      *
    1156      * @param array $activity_ids Activity IDs whose comments should be deleted.
    1157      * @param bool $delete_meta Should we delete the activity meta items for these comments?
    1158      * @return bool True on success.
    1159      */
    1160     public static function delete_activity_item_comments( $activity_ids = array(), $delete_meta = true ) {
    1161         global $wpdb;
    1162 
    1163         $bp = buddypress();
    1164 
    1165         $delete_meta  = (bool) $delete_meta;
    1166         $activity_ids = implode( ',', wp_parse_id_list( $activity_ids ) );
    1167 
    1168         if ( $delete_meta ) {
    1169             // Fetch the activity comment IDs for our deleted activity items
    1170             $activity_comment_ids = $wpdb->get_col( "SELECT id FROM {$bp->activity->table_name} WHERE type = 'activity_comment' AND item_id IN ({$activity_ids})" );
    1171 
    1172             if ( ! empty( $activity_comment_ids ) ) {
    1173                 self::delete_activity_meta_entries( $activity_comment_ids );
    1174             }
    1175         }
    1176 
    1177         return $wpdb->query( "DELETE FROM {$bp->activity->table_name} WHERE type = 'activity_comment' AND item_id IN ({$activity_ids})" );
    1178     }
    1179 
    1180     /**
    1181      * Delete the meta entries associated with a set of activity items.
    1182      *
    1183      * @since BuddyPress (1.2)
    1184      *
    1185      * @param array $activity_ids Activity IDs whose meta should be deleted.
    1186      * @return bool True on success.
    1187      */
    1188     public static function delete_activity_meta_entries( $activity_ids = array() ) {
    1189         $activity_ids = wp_parse_id_list( $activity_ids );
    1190 
    1191         foreach ( $activity_ids as $activity_id ) {
    1192             bp_activity_delete_meta( $activity_id );
    1193         }
    1194 
    1195         return true;
    1196     }
    1197 
    1198     /**
    1199      * Append activity comments to their associated activity items.
    1200      *
    1201      * @since BuddyPress (1.2)
    1202      *
    1203      * @global wpdb $wpdb WordPress database object
    1204      *
    1205      * @param array $activities Activities to fetch comments for.
    1206      * @param bool $spam Optional. 'ham_only' (default), 'spam_only' or 'all'.
    1207      * @return array The updated activities with nested comments.
    1208      */
    1209     public static function append_comments( $activities, $spam = 'ham_only' ) {
    1210         $activity_comments = array();
    1211 
    1212         // Now fetch the activity comments and parse them into the correct position in the activities array.
    1213         foreach ( (array) $activities as $activity ) {
    1214             $top_level_parent_id = 'activity_comment' == $activity->type ? $activity->item_id : 0;
    1215             $activity_comments[$activity->id] = BP_Activity_Activity::get_activity_comments( $activity->id, $activity->mptt_left, $activity->mptt_right, $spam, $top_level_parent_id );
    1216         }
    1217 
    1218         // Merge the comments with the activity items
    1219         foreach ( (array) $activities as $key => $activity ) {
    1220             if ( isset( $activity_comments[$activity->id] ) ) {
    1221                 $activities[$key]->children = $activity_comments[$activity->id];
    1222             }
    1223         }
    1224 
    1225         return $activities;
    1226     }
    1227 
    1228     /**
    1229      * Get activity comments that are associated with a specific activity ID.
    1230      *
    1231      * @since BuddyPress (1.2)
    1232      *
    1233      * @global wpdb $wpdb WordPress database object.
    1234      *
    1235      * @param int $activity_id Activity ID to fetch comments for.
    1236      * @param int $left Left-most node boundary.
    1237      * @param into $right Right-most node boundary.
    1238      * @param bool $spam Optional. 'ham_only' (default), 'spam_only' or 'all'.
    1239      * @param int $top_level_parent_id Optional. The id of the root-level parent activity item.
    1240      * @return array The updated activities with nested comments.
    1241      */
    1242     public static function get_activity_comments( $activity_id, $left, $right, $spam = 'ham_only', $top_level_parent_id = 0 ) {
    1243         global $wpdb;
    1244 
    1245         if ( empty( $top_level_parent_id ) ) {
    1246             $top_level_parent_id = $activity_id;
    1247         }
    1248 
    1249         $comments = wp_cache_get( $activity_id, 'bp_activity_comments' );
    1250 
    1251         // We store the string 'none' to cache the fact that the
    1252         // activity item has no comments
    1253         if ( 'none' === $comments ) {
    1254             $comments = false;
    1255 
    1256         // A true cache miss
    1257         } elseif ( empty( $comments ) ) {
    1258 
    1259             $bp = buddypress();
    1260 
    1261             // Select the user's fullname with the query
    1262             if ( bp_is_active( 'xprofile' ) ) {
    1263                 $fullname_select = ", pd.value as user_fullname";
    1264                 $fullname_from = ", {$bp->profile->table_name_data} pd ";
    1265                 $fullname_where = "AND pd.user_id = a.user_id AND pd.field_id = 1";
    1266 
    1267             // Prevent debug errors
    1268             } else {
    1269                 $fullname_select = $fullname_from = $fullname_where = '';
    1270             }
    1271 
    1272             // Don't retrieve activity comments marked as spam
    1273             if ( 'ham_only' == $spam ) {
    1274                 $spam_sql = 'AND a.is_spam = 0';
    1275             } elseif ( 'spam_only' == $spam ) {
    1276                 $spam_sql = 'AND a.is_spam = 1';
    1277             } else {
    1278                 $spam_sql = '';
    1279             }
    1280 
    1281             // Legacy query - not recommended
    1282             $func_args = func_get_args();
    1283 
    1284             /**
    1285              * Filters if BuddyPress should use the legacy activity query.
    1286              *
    1287              * @since BuddyPress (2.0.0)
    1288              *
    1289              * @param bool                 Whether or not to use the legacy query.
    1290              * @param BP_Activity_Activity Magic method referring to currently called method.
    1291              * @param array $func_args     Array of the method's argument list.
    1292              */
    1293             if ( apply_filters( 'bp_use_legacy_activity_query', false, __METHOD__, $func_args ) ) {
    1294 
    1295                 /**
    1296                  * Filters the MySQL prepared statement for the legacy activity query.
    1297                  *
    1298                  * @since BuddyPress (1.5.0)
    1299                  *
    1300                  * @param string Prepared statement for the activity query.
    1301                  * @param int    $activity_id Activity ID to fetch comments for.
    1302                  * @param int    $left Left-most node boundary.
    1303                  * @param int    $right Right-most node boundary.
    1304                  * @param string $spam_sql SQL Statement portion to differentiate between ham or spam.
    1305                  */
    1306                 $sql = apply_filters( 'bp_activity_comments_user_join_filter', $wpdb->prepare( "SELECT a.*, u.user_email, u.user_nicename, u.user_login, u.display_name{$fullname_select} FROM {$bp->activity->table_name} a, {$wpdb->users} u{$fullname_from} WHERE u.ID = a.user_id {$fullname_where} AND a.type = 'activity_comment' {$spam_sql} AND a.item_id = %d AND a.mptt_left > %d AND a.mptt_left < %d ORDER BY a.date_recorded ASC", $top_level_parent_id, $left, $right ), $activity_id, $left, $right, $spam_sql );
    1307 
    1308                 $descendants = $wpdb->get_results( $sql );
    1309 
    1310             // We use the mptt BETWEEN clause to limit returned
    1311             // descendants to the correct part of the tree.
    1312             } else {
    1313                 $sql = $wpdb->prepare( "SELECT id FROM {$bp->activity->table_name} a WHERE a.type = 'activity_comment' {$spam_sql} AND a.item_id = %d and a.mptt_left > %d AND a.mptt_left < %d ORDER BY a.date_recorded ASC", $top_level_parent_id, $left, $right );
    1314 
    1315                 $descendant_ids = $wpdb->get_col( $sql );
    1316                 $descendants    = self::get_activity_data( $descendant_ids );
    1317                 $descendants    = self::append_user_fullnames( $descendants );
    1318             }
    1319 
    1320             $ref = array();
    1321 
    1322             // Loop descendants and build an assoc array
    1323             foreach ( (array) $descendants as $d ) {
    1324                 $d->children = array();
    1325 
    1326                 // If we have a reference on the parent
    1327                 if ( isset( $ref[ $d->secondary_item_id ] ) ) {
    1328                     $ref[ $d->secondary_item_id ]->children[ $d->id ] = $d;
    1329                     $ref[ $d->id ] =& $ref[ $d->secondary_item_id ]->children[ $d->id ];
    1330 
    1331                 // If we don't have a reference on the parent, put in the root level
    1332                 } else {
    1333                     $comments[ $d->id ] = $d;
    1334                     $ref[ $d->id ] =& $comments[ $d->id ];
    1335                 }
    1336             }
    1337 
    1338             // Calculate depth for each item
    1339             foreach ( $ref as &$r ) {
    1340                 $depth = 1;
    1341                 $parent_id = $r->secondary_item_id;
    1342                 while ( $parent_id !== $r->item_id ) {
    1343                     $depth++;
    1344 
    1345                     // When display_comments=stream, the
    1346                     // parent comment may not be part of
    1347                     // the returned results, so we manually
    1348                     // fetch it
    1349                     if ( empty( $ref[ $parent_id ] ) ) {
    1350                         $direct_parent = new BP_Activity_Activity( $parent_id );
    1351                         if ( isset( $direct_parent->secondary_item_id ) ) {
    1352                             $parent_id = $direct_parent->secondary_item_id;
    1353                         } else {
    1354                             // Something went wrong
    1355                             // Short-circuit the
    1356                             // depth calculation
    1357                             $parent_id = $r->item_id;
    1358                         }
    1359                     } else {
    1360                         $parent_id = $ref[ $parent_id ]->secondary_item_id;
    1361                     }
    1362                 }
    1363                 $r->depth = $depth;
    1364             }
    1365 
    1366             // If we cache a value of false, it'll count as a cache
    1367             // miss the next time the activity comments are fetched.
    1368             // Storing the string 'none' is a hack workaround to
    1369             // avoid unnecessary queries.
    1370             if ( false === $comments ) {
    1371                 $cache_value = 'none';
    1372             } else {
    1373                 $cache_value = $comments;
    1374             }
    1375 
    1376             wp_cache_set( $activity_id, $cache_value, 'bp_activity_comments' );
    1377         }
    1378 
    1379         return $comments;
    1380     }
    1381 
    1382     /**
    1383      * Rebuild nested comment tree under an activity or activity comment.
    1384      *
    1385      * @since BuddyPress (1.2)
    1386      *
    1387      * @global wpdb $wpdb WordPress database object
    1388      *
    1389      * @param  int $parent_id ID of an activity or activity comment
    1390      * @param  int $left      Node boundary start for activity or activity comment
    1391      * @return int Right      Node boundary of activity or activity comment
    1392      */
    1393     public static function rebuild_activity_comment_tree( $parent_id, $left = 1 ) {
    1394         global $wpdb;
    1395 
    1396         $bp = buddypress();
    1397 
    1398         // The right value of this node is the left value + 1
    1399         $right = intval( $left + 1 );
    1400 
    1401         // Get all descendants of this node
    1402         $comments    = BP_Activity_Activity::get_child_comments( $parent_id );
    1403         $descendants = wp_list_pluck( $comments, 'id' );
    1404 
    1405         // Loop the descendants and recalculate the left and right values
    1406         foreach ( (array) $descendants as $descendant_id ) {
    1407             $right = BP_Activity_Activity::rebuild_activity_comment_tree( $descendant_id, $right );
    1408         }
    1409 
    1410         // We've got the left value, and now that we've processed the children
    1411         // of this node we also know the right value
    1412         if ( 1 === $left ) {
    1413             $wpdb->query( $wpdb->prepare( "UPDATE {$bp->activity->table_name} SET mptt_left = %d, mptt_right = %d WHERE id = %d", $left, $right, $parent_id ) );
    1414         } else {
    1415             $wpdb->query( $wpdb->prepare( "UPDATE {$bp->activity->table_name} SET mptt_left = %d, mptt_right = %d WHERE type = 'activity_comment' AND id = %d", $left, $right, $parent_id ) );
    1416         }
    1417 
    1418         // Return the right value of this node + 1
    1419         return intval( $right + 1 );
    1420     }
    1421 
    1422     /**
    1423      * Get child comments of an activity or activity comment.
    1424      *
    1425      * @since BuddyPress (1.2)
    1426      *
    1427      * @global wpdb $wpdb WordPress database object.
    1428      *
    1429      * @param int $parent_id ID of an activity or activity comment.
    1430      * @return object Numerically indexed array of child comments.
    1431      */
    1432     public static function get_child_comments( $parent_id ) {
    1433         global $wpdb;
    1434 
    1435         $bp = buddypress();
    1436 
    1437         return $wpdb->get_results( $wpdb->prepare( "SELECT id FROM {$bp->activity->table_name} WHERE type = 'activity_comment' AND secondary_item_id = %d", $parent_id ) );
    1438     }
    1439 
    1440     /**
    1441      * Get a list of components that have recorded activity associated with them.
    1442      *
    1443      * @param bool $skip_last_activity If true, components will not be
    1444      *        included if the only activity type associated with them is
    1445      *        'last_activity'. (Since 2.0.0, 'last_activity' is stored in
    1446      *        the activity table, but these items are not full-fledged
    1447      *        activity items.) Default: true.
    1448      * @return array List of component names.
    1449      */
    1450     public static function get_recorded_components( $skip_last_activity = true ) {
    1451         global $wpdb;
    1452 
    1453         $bp = buddypress();
    1454 
    1455         if ( true === $skip_last_activity ) {
    1456             $components = $wpdb->get_col( "SELECT DISTINCT component FROM {$bp->activity->table_name} WHERE action != '' AND action != 'last_activity' ORDER BY component ASC" );
    1457         } else {
    1458             $components = $wpdb->get_col( "SELECT DISTINCT component FROM {$bp->activity->table_name} ORDER BY component ASC" );
    1459         }
    1460 
    1461         return $components;
    1462     }
    1463 
    1464     /**
    1465      * Get sitewide activity items for use in an RSS feed.
    1466      *
    1467      * @param int $limit Optional. Number of items to fetch. Default: 35.
    1468      * @return array $activity_feed List of activity items, with RSS data added.
    1469      */
    1470     public static function get_sitewide_items_for_feed( $limit = 35 ) {
    1471         $activities    = bp_activity_get_sitewide( array( 'max' => $limit ) );
    1472         $activity_feed = array();
    1473 
    1474         for ( $i = 0, $count = count( $activities ); $i < $count; ++$i ) {
    1475             $title                            = explode( '<span', $activities[$i]['content'] );
    1476             $activity_feed[$i]['title']       = trim( strip_tags( $title[0] ) );
    1477             $activity_feed[$i]['link']        = $activities[$i]['primary_link'];
    1478             $activity_feed[$i]['description'] = @sprintf( $activities[$i]['content'], '' );
    1479             $activity_feed[$i]['pubdate']     = $activities[$i]['date_recorded'];
    1480         }
    1481 
    1482         return $activity_feed;
    1483     }
    1484 
    1485     /**
    1486      * Create SQL IN clause for filter queries.
    1487      *
    1488      * @since BuddyPress (1.5)
    1489      *
    1490      * @see BP_Activity_Activity::get_filter_sql()
    1491      *
    1492      * @param string $field The database field.
    1493      * @param array|bool $items The values for the IN clause, or false when none are found.
    1494      */
    1495     public static function get_in_operator_sql( $field, $items ) {
    1496         global $wpdb;
    1497 
    1498         // split items at the comma
    1499         if ( ! is_array( $items ) ) {
    1500             $items = explode( ',', $items );
    1501         }
    1502 
    1503         // array of prepared integers or quoted strings
    1504         $items_prepared = array();
    1505 
    1506         // clean up and format each item
    1507         foreach ( $items as $item ) {
    1508             // clean up the string
    1509             $item = trim( $item );
    1510             // pass everything through prepare for security and to safely quote strings
    1511             $items_prepared[] = ( is_numeric( $item ) ) ? $wpdb->prepare( '%d', $item ) : $wpdb->prepare( '%s', $item );
    1512         }
    1513 
    1514         // build IN operator sql syntax
    1515         if ( count( $items_prepared ) )
    1516             return sprintf( '%s IN ( %s )', trim( $field ), implode( ',', $items_prepared ) );
    1517         else
    1518             return false;
    1519     }
    1520 
    1521     /**
    1522      * Create filter SQL clauses.
    1523      *
    1524      * @since BuddyPress (1.5.0)
    1525      *
    1526      * @param array $filter_array {
    1527      *     Fields and values to filter by.
    1528      *     @type array|string|id $user_id User ID(s).
    1529      *     @type array|string $object Corresponds to the 'component'
    1530      *           column in the database.
    1531      *     @type array|string $action Corresponds to the 'type' column
    1532      *           in the database.
    1533      *     @type array|string|int $primary_id Corresponds to the 'item_id'
    1534      *           column in the database.
    1535      *     @type array|string|int $secondary_id Corresponds to the
    1536      *           'secondary_item_id' column in the database.
    1537      *     @type int $offset Return only those items with an ID greater
    1538      *           than the offset value.
    1539      *     @type string $since Return only those items that have a
    1540      *           date_recorded value greater than a given MySQL-formatted
    1541      *           date.
    1542      * }
    1543      * @return string The filter clause, for use in a SQL query.
    1544      */
    1545     public static function get_filter_sql( $filter_array ) {
    1546 
    1547         $filter_sql = array();
    1548 
    1549         if ( !empty( $filter_array['user_id'] ) ) {
    1550             $user_sql = BP_Activity_Activity::get_in_operator_sql( 'a.user_id', $filter_array['user_id'] );
    1551             if ( !empty( $user_sql ) )
    1552                 $filter_sql[] = $user_sql;
    1553         }
    1554 
    1555         if ( !empty( $filter_array['object'] ) ) {
    1556             $object_sql = BP_Activity_Activity::get_in_operator_sql( 'a.component', $filter_array['object'] );
    1557             if ( !empty( $object_sql ) )
    1558                 $filter_sql[] = $object_sql;
    1559         }
    1560 
    1561         if ( !empty( $filter_array['action'] ) ) {
    1562             $action_sql = BP_Activity_Activity::get_in_operator_sql( 'a.type', $filter_array['action'] );
    1563             if ( ! empty( $action_sql ) )
    1564                 $filter_sql[] = $action_sql;
    1565         }
    1566 
    1567         if ( !empty( $filter_array['primary_id'] ) ) {
    1568             $pid_sql = BP_Activity_Activity::get_in_operator_sql( 'a.item_id', $filter_array['primary_id'] );
    1569             if ( !empty( $pid_sql ) )
    1570                 $filter_sql[] = $pid_sql;
    1571         }
    1572 
    1573         if ( !empty( $filter_array['secondary_id'] ) ) {
    1574             $sid_sql = BP_Activity_Activity::get_in_operator_sql( 'a.secondary_item_id', $filter_array['secondary_id'] );
    1575             if ( !empty( $sid_sql ) )
    1576                 $filter_sql[] = $sid_sql;
    1577         }
    1578 
    1579         if ( ! empty( $filter_array['offset'] ) ) {
    1580             $sid_sql = absint( $filter_array['offset'] );
    1581             $filter_sql[] = "a.id >= {$sid_sql}";
    1582         }
    1583 
    1584         if ( ! empty( $filter_array['since'] ) ) {
    1585             // Validate that this is a proper Y-m-d H:i:s date
    1586             // Trick: parse to UNIX date then translate back
    1587             $translated_date = date( 'Y-m-d H:i:s', strtotime( $filter_array['since'] ) );
    1588             if ( $translated_date === $filter_array['since'] ) {
    1589                 $filter_sql[] = "a.date_recorded > '{$translated_date}'";
    1590             }
    1591         }
    1592 
    1593         if ( empty( $filter_sql ) )
    1594             return false;
    1595 
    1596         return join( ' AND ', $filter_sql );
    1597     }
    1598 
    1599     /**
    1600      * Get the date/time of last recorded activity.
    1601      *
    1602      * @since BuddyPress (1.2)
    1603      *
    1604      * @return string ISO timestamp.
    1605      */
    1606     public static function get_last_updated() {
    1607         global $wpdb;
    1608 
    1609         $bp = buddypress();
    1610 
    1611         return $wpdb->get_var( "SELECT date_recorded FROM {$bp->activity->table_name} ORDER BY date_recorded DESC LIMIT 1" );
    1612     }
    1613 
    1614     /**
    1615      * Get favorite count for a given user.
    1616      *
    1617      * @since BuddyPress (1.2)
    1618      *
    1619      * @param int The ID of the user whose favorites you're counting.
    1620      * @return int A count of the user's favorites.
    1621      */
    1622     public static function total_favorite_count( $user_id ) {
    1623 
    1624         // Get activities from user meta
    1625         $favorite_activity_entries = bp_get_user_meta( $user_id, 'bp_favorite_activities', true );
    1626         if ( ! empty( $favorite_activity_entries ) ) {
    1627             return count( maybe_unserialize( $favorite_activity_entries ) );
    1628         }
    1629 
    1630         // No favorites
    1631         return 0;
    1632     }
    1633 
    1634     /**
    1635      * Check whether an activity item exists with a given string content.
    1636      *
    1637      * @param string $content The content to filter by.
    1638      * @return int|bool The ID of the first matching item if found, otherwise false.
    1639      */
    1640     public static function check_exists_by_content( $content ) {
    1641         global $wpdb;
    1642 
    1643         $bp = buddypress();
    1644 
    1645         return $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$bp->activity->table_name} WHERE content = %s", $content ) );
    1646     }
    1647 
    1648     /**
    1649      * Hide all activity for a given user.
    1650      *
    1651      * @param int $user_id The ID of the user whose activity you want to mark hidden.
    1652      * @param int
    1653      */
    1654     public static function hide_all_for_user( $user_id ) {
    1655         global $wpdb;
    1656 
    1657         $bp = buddypress();
    1658 
    1659         return $wpdb->get_var( $wpdb->prepare( "UPDATE {$bp->activity->table_name} SET hide_sitewide = 1 WHERE user_id = %d", $user_id ) );
    1660     }
    1661 
    1662     /**
    1663      * PHP-agnostic version of {@link array_replace_recursive()}.
    1664      *
    1665      * array_replace_recursive() is a PHP 5.3 function.  BuddyPress (and
    1666      * WordPress) currently supports down to PHP 5.2, so this method is a workaround
    1667      * for PHP 5.2.
    1668      *
    1669      * Note: array_replace_recursive() supports infinite arguments, but for our use-
    1670      * case, we only need to support two arguments.
    1671      *
    1672      * Subject to removal once WordPress makes PHP 5.3.0 the minimum requirement.
    1673      *
    1674      * @since BuddyPress (2.2.0)
    1675      *
    1676      * @see http://php.net/manual/en/function.array-replace-recursive.php#109390
    1677      *
    1678      * @param  array $base         Array with keys needing to be replaced
    1679      * @param  array $replacements Array with the replaced keys
    1680      * @return array
    1681      */
    1682     protected static function array_replace_recursive( $base = array(), $replacements = array() ) {
    1683         if ( function_exists( 'array_replace_recursive' ) ) {
    1684             return array_replace_recursive( $base, $replacements );
    1685         }
    1686 
    1687         // PHP 5.2-compatible version
    1688         // http://php.net/manual/en/function.array-replace-recursive.php#109390
    1689         foreach ( array_slice( func_get_args(), 1 ) as $replacements ) {
    1690             $bref_stack = array( &$base );
    1691             $head_stack = array( $replacements );
    1692 
    1693             do {
    1694                 end( $bref_stack );
    1695 
    1696                 $bref = &$bref_stack[ key( $bref_stack ) ];
    1697                 $head = array_pop( $head_stack );
    1698 
    1699                 unset( $bref_stack[ key($bref_stack) ] );
    1700 
    1701                 foreach ( array_keys( $head ) as $key ) {
    1702                     if ( isset( $key, $bref ) && is_array( $bref[$key] ) && is_array( $head[$key] ) ) {
    1703                         $bref_stack[] = &$bref[ $key ];
    1704                         $head_stack[] = $head[ $key ];
    1705                     } else {
    1706                         $bref[ $key ] = $head[ $key ];
    1707                     }
    1708                 }
    1709             } while( count( $head_stack ) );
    1710         }
    1711 
    1712         return $base;
    1713     }
    1714 }
    1715 
    1716 /**
    1717  * Class for generating the WHERE SQL clause for advanced activity fetching.
    1718  *
    1719  * This is notably used in {@link BP_Activity_Activity::get()} with the
    1720  * 'filter_query' parameter.
    1721  *
    1722  * @since BuddyPress (2.2.0)
    1723  */
    1724 class BP_Activity_Query extends BP_Recursive_Query {
    1725     /**
    1726      * Array of activity queries.
    1727      *
    1728      * See {@see BP_Activity_Query::__construct()} for information on query arguments.
    1729      *
    1730      * @since BuddyPress (2.2.0)
    1731      * @access public
    1732      * @var array
    1733      */
    1734     public $queries = array();
    1735 
    1736     /**
    1737      * Table alias.
    1738      *
    1739      * @since BuddyPress (2.2.0)
    1740      * @access public
    1741      * @var string
    1742      */
    1743     public $table_alias = '';
    1744 
    1745     /**
    1746      * Supported DB columns.
    1747      *
    1748      * See the 'wp_bp_activity' DB table schema.
    1749      *
    1750      * @since BuddyPress (2.2.0)
    1751      * @access public
    1752      * @var array
    1753      */
    1754     public $db_columns = array(
    1755         'id', 'user_id', 'component', 'type', 'action', 'content',
    1756         'item_id', 'secondary_item_id', 'hide_sitewide', 'is_spam',
    1757     );
    1758 
    1759     /**
    1760      * Constructor.
    1761      *
    1762      * @since BuddyPress (2.2.0)
    1763      *
    1764      * @param array $query {
    1765      *     Array of query clauses.
    1766      *
    1767      *     @type array {
    1768      *         @type string $column   Required. The column to query against. Basically, any DB column in the main
    1769      *                                'wp_bp_activity' table.
    1770      *         @type string $value    Required. Value to filter by.
    1771      *         @type string $compare  Optional. The comparison operator. Default '='.
    1772      *                                Accepts '=', '!=', '>', '>=', '<', '<=', 'IN', 'NOT IN', 'LIKE',
    1773      *                                'NOT LIKE', BETWEEN', 'NOT BETWEEN', 'REGEXP', 'NOT REGEXP', 'RLIKE'
    1774      *         @type string $relation Optional. The boolean relationship between the activity queries.
    1775      *                                Accepts 'OR', 'AND'. Default 'AND'.
    1776      *         @type array {
    1777      *             Optional. Another fully-formed activity query. See parameters above.
    1778      *         }
    1779      *     }
    1780      * }
    1781      */
    1782     public function __construct( $query = array() ) {
    1783         if ( ! is_array( $query ) ) {
    1784             return;
    1785         }
    1786 
    1787         $this->queries = $this->sanitize_query( $query );
    1788     }
    1789 
    1790     /**
    1791      * Generates WHERE SQL clause to be appended to a main query.
    1792      *
    1793      * @since BuddyPress (2.2.0)
    1794      * @access public
    1795      *
    1796      * @param string $alias An existing table alias that is compatible with the current query clause.
    1797      *               Default: 'a'. BP_Activity_Activity::get() uses 'a', so we default to that.
    1798      * @return string SQL fragment to append to the main WHERE clause.
    1799      */
    1800     public function get_sql( $alias = 'a' ) {
    1801         if ( ! empty( $alias ) ) {
    1802             $this->table_alias = sanitize_title( $alias );
    1803         }
    1804 
    1805         $sql = $this->get_sql_clauses();
    1806 
    1807         // we only need the 'where' clause
    1808         //
    1809         // also trim trailing "AND" clause from parent BP_Recursive_Query class
    1810         // since it's not necessary for our needs
    1811         return preg_replace( '/^\sAND/', '', $sql['where'] );
    1812     }
    1813 
    1814     /**
    1815      * Generate WHERE clauses for a first-order clause.
    1816      *
    1817      * @since BuddyPress (2.2.0)
    1818      * @access protected
    1819      *
    1820      * @param  array $clause       Array of arguments belonging to the clause.
    1821      * @param  array $parent_query Parent query to which the clause belongs.
    1822      * @return array {
    1823      *     @type array $where Array of subclauses for the WHERE statement.
    1824      *     @type array $join  Empty array. Not used.
    1825      * }
    1826      */
    1827     protected function get_sql_for_clause( $clause, $parent_query ) {
    1828         global $wpdb;
    1829 
    1830         $sql_chunks = array(
    1831             'where' => array(),
    1832             'join' => array(),
    1833         );
    1834 
    1835         $column = isset( $clause['column'] ) ? $this->validate_column( $clause['column'] ) : '';
    1836         $value  = isset( $clause['value'] )  ? $clause['value'] : '';
    1837         if ( empty( $column ) || ! isset( $clause['value'] ) ) {
    1838             return $sql_chunks;
    1839         }
    1840 
    1841         if ( isset( $clause['compare'] ) ) {
    1842             $clause['compare'] = strtoupper( $clause['compare'] );
    1843         } else {
    1844             $clause['compare'] = isset( $clause['value'] ) && is_array( $clause['value'] ) ? 'IN' : '=';
    1845         }
    1846 
    1847         // default 'compare' to '=' if no valid operator is found
    1848         if ( ! in_array( $clause['compare'], array(
    1849             '=', '!=', '>', '>=', '<', '<=',
    1850             'LIKE', 'NOT LIKE',
    1851             'IN', 'NOT IN',
    1852             'BETWEEN', 'NOT BETWEEN',
    1853             'REGEXP', 'NOT REGEXP', 'RLIKE'
    1854         ) ) ) {
    1855             $clause['compare'] = '=';
    1856         }
    1857 
    1858         $compare = $clause['compare'];
    1859 
    1860         $alias = ! empty( $this->table_alias ) ? "{$this->table_alias}." : '';
    1861 
    1862         // Next, Build the WHERE clause.
    1863         $where = '';
    1864 
    1865         // value.
    1866         if ( isset( $clause['value'] ) ) {
    1867             if ( in_array( $compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ) ) ) {
    1868                 if ( ! is_array( $value ) ) {
    1869                     $value = preg_split( '/[,\s]+/', $value );
    1870                 }
    1871             }
    1872 
    1873             // tinyint
    1874             if ( ! empty( $column ) && true === in_array( $column, array( 'hide_sitewide', 'is_spam' ) ) ) {
    1875                 $sql_chunks['where'][] = $wpdb->prepare( "{$alias}{$column} = %d", $value );
    1876 
    1877             } else {
    1878                 switch ( $compare ) {
    1879                     // IN uses different syntax
    1880                     case 'IN' :
    1881                     case 'NOT IN' :
    1882                         $in_sql = BP_Activity_Activity::get_in_operator_sql( "{$alias}{$column}", $value );
    1883 
    1884                         // 'NOT IN' operator is as easy as a string replace!
    1885                         if ( 'NOT IN' === $compare ) {
    1886                             $in_sql = str_replace( 'IN', 'NOT IN', $in_sql );
    1887                         }
    1888 
    1889                         $sql_chunks['where'][] = $in_sql;
    1890                         break;
    1891 
    1892                     case 'BETWEEN' :
    1893                     case 'NOT BETWEEN' :
    1894                         $value = array_slice( $value, 0, 2 );
    1895                         $where = $wpdb->prepare( '%s AND %s', $value );
    1896                         break;
    1897 
    1898                     case 'LIKE' :
    1899                     case 'NOT LIKE' :
    1900                         $value = '%' . bp_esc_like( $value ) . '%';
    1901                         $where = $wpdb->prepare( '%s', $value );
    1902                         break;
    1903 
    1904                     default :
    1905                         $where = $wpdb->prepare( '%s', $value );
    1906                         break;
    1907 
    1908                 }
    1909             }
    1910 
    1911             if ( $where ) {
    1912                 $sql_chunks['where'][] = "{$alias}{$column} {$compare} {$where}";
    1913             }
    1914         }
    1915 
    1916         /*
    1917          * Multiple WHERE clauses should be joined in parentheses.
    1918          */
    1919         if ( 1 < count( $sql_chunks['where'] ) ) {
    1920             $sql_chunks['where'] = array( '( ' . implode( ' AND ', $sql_chunks['where'] ) . ' )' );
    1921         }
    1922 
    1923         return $sql_chunks;
    1924     }
    1925 
    1926     /**
    1927      * Determine whether a clause is first-order.
    1928      *
    1929      * @since BuddyPress (2.2.0)
    1930      * @access protected
    1931      *
    1932      * @param  array $query Clause to check.
    1933      * @return bool
    1934      */
    1935     protected function is_first_order_clause( $query ) {
    1936         return isset( $query['column'] ) || isset( $query['value'] );
    1937     }
    1938 
    1939     /**
    1940      * Validates a column name parameter.
    1941      *
    1942      * Column names are checked against a whitelist of known tables.
    1943      * See {@link BP_Activity_Query::db_tables}.
    1944      *
    1945      * @since BuddyPress (2.2.0)
    1946      * @access public
    1947      *
    1948      * @param string $column The user-supplied column name.
    1949      * @return string A validated column name value.
    1950      */
    1951     public function validate_column( $column = '' ) {
    1952         if ( in_array( $column, $this->db_columns ) ) {
    1953             return $column;
    1954         } else {
    1955             return '';
    1956         }
    1957     }
    1958 }
    1959 
    1960 /**
    1961  * Create a RSS feed using the activity component.
    1962  *
    1963  * You should only construct a new feed when you've validated that you're on
    1964  * the appropriate screen.
    1965  *
    1966  * See {@link bp_activity_action_sitewide_feed()} as an example.
    1967  *
    1968  * Accepted parameters:
    1969  *   id               - internal id for the feed; should be alphanumeric only
    1970  *                      (required)
    1971  *   title            - RSS feed title
    1972  *   link             - Relevant link for the RSS feed
    1973  *   description      - RSS feed description
    1974  *   ttl              - Time-to-live (see inline doc in constructor)
    1975  *   update_period    - Part of the syndication module (see inline doc in
    1976  *                      constructor for more info)
    1977  *   update_frequency - Part of the syndication module (see inline doc in
    1978  *                      constructor for more info)
    1979  *   max              - Number of feed items to display
    1980  *   activity_args    - Arguments passed to {@link bp_has_activities()}
    1981  *
    1982  * @since BuddyPress (1.8)
    1983  */
    1984 class BP_Activity_Feed {
    1985     /**
    1986      * Holds our custom class properties.
    1987      *
    1988      * These variables are stored in a protected array that is magically
    1989      * updated using PHP 5.2+ methods.
    1990      *
    1991      * @see BP_Feed::__construct() This is where $data is added
    1992      * @var array
    1993      */
    1994     protected $data;
    1995 
    1996     /**
    1997      * Magic method for checking the existence of a certain data variable.
    1998      *
    1999      * @param string $key
    2000      */
    2001     public function __isset( $key ) { return isset( $this->data[$key] ); }
    2002 
    2003     /**
    2004      * Magic method for getting a certain data variable.
    2005      *
    2006      * @param string $key
    2007      */
    2008     public function __get( $key ) { return isset( $this->data[$key] ) ? $this->data[$key] : null; }
    2009 
    2010     /**
    2011      * Constructor.
    2012      *
    2013      * @param array $args Optional
    2014      */
    2015     public function __construct( $args = array() ) {
    2016 
    2017         /**
    2018          * Filters if BuddyPress should consider feeds enabled. If disabled, it will return early.
    2019          *
    2020          * @since BuddyPress (1.8.0)
    2021          *
    2022          * @param bool true Default true aka feeds are enabled.
    2023          */
    2024         if ( false === (bool) apply_filters( 'bp_activity_enable_feeds', true ) ) {
    2025             global $wp_query;
    2026 
    2027             // set feed flag to false
    2028             $wp_query->is_feed = false;
    2029 
    2030             return false;
    2031         }
    2032 
    2033         // Setup data
    2034         $this->data = wp_parse_args( $args, array(
    2035             // Internal identifier for the RSS feed - should be alphanumeric only
    2036             'id'               => '',
    2037 
    2038             // RSS title - should be plain-text
    2039             'title'            => '',
    2040 
    2041             // relevant link for the RSS feed
    2042             'link'             => '',
    2043 
    2044             // RSS description - should be plain-text
    2045             'description'      => '',
    2046 
    2047             // Time-to-live - number of minutes to cache the data before an aggregator
    2048             // requests it again.  This is only acknowledged if the RSS client supports it
    2049             //
    2050             // See: http://www.rssboard.org/rss-profile#element-channel-ttl
    2051             //      http://www.kbcafe.com/rss/rssfeedstate.html#ttl
    2052             'ttl'              => '30',
    2053 
    2054             // Syndication module - similar to ttl, but not really supported by RSS
    2055             // clients
    2056             //
    2057             // See: http://web.resource.org/rss/1.0/modules/syndication/#description
    2058             //      http://www.kbcafe.com/rss/rssfeedstate.html#syndicationmodule
    2059             'update_period'    => 'hourly',
    2060             'update_frequency' => 2,
    2061 
    2062             // Number of items to display
    2063             'max'              => 50,
    2064 
    2065             // Activity arguments passed to bp_has_activities()
    2066             'activity_args'    => array()
    2067         ) );
    2068 
    2069         /**
    2070          * Fires before the feed is setup so plugins can modify.
    2071          *
    2072          * @since BuddyPress (1.8.0)
    2073          *
    2074          * @param BP_Activity_Feed Reference to current instance of activity feed.
    2075          */
    2076         do_action_ref_array( 'bp_activity_feed_prefetch', array( &$this ) );
    2077 
    2078         // Setup class properties
    2079         $this->setup_properties();
    2080 
    2081         // Check if id is valid
    2082         if ( empty( $this->id ) ) {
    2083             _doing_it_wrong( 'BP_Activity_Feed', __( "RSS feed 'id' must be defined", 'buddypress' ), 'BP 1.8' );
    2084             return false;
    2085         }
    2086 
    2087         /**
    2088          * Fires after the feed is setup so plugins can modify.
    2089          *
    2090          * @since BuddyPress (1.8.0)
    2091          *
    2092          * @param BP_Activity_Feed Reference to current instance of activity feed.
    2093          */
    2094         do_action_ref_array( 'bp_activity_feed_postfetch', array( &$this ) );
    2095 
    2096         // Setup feed hooks
    2097         $this->setup_hooks();
    2098 
    2099         // Output the feed
    2100         $this->output();
    2101 
    2102         // Kill the rest of the output
    2103         die();
    2104     }
    2105 
    2106     /** SETUP ****************************************************************/
    2107 
    2108     /**
    2109      * Setup and validate the class properties.
    2110      *
    2111      * @access protected
    2112      */
    2113     protected function setup_properties() {
    2114         $this->id               = sanitize_title( $this->id );
    2115         $this->title            = strip_tags( $this->title );
    2116         $this->link             = esc_url_raw( $this->link );
    2117         $this->description      = strip_tags( $this->description );
    2118         $this->ttl              = (int) $this->ttl;
    2119         $this->update_period    = strip_tags( $this->update_period );
    2120         $this->update_frequency = (int) $this->update_frequency;
    2121 
    2122         $this->activity_args    = wp_parse_args( $this->activity_args, array(
    2123             'max'              => $this->max,
    2124             'per_page'         => $this->max,
    2125             'display_comments' => 'stream'
    2126         ) );
    2127 
    2128     }
    2129 
    2130     /**
    2131      * Setup some hooks that are used in the feed.
    2132      *
    2133      * Currently, these hooks are used to maintain backwards compatibility with
    2134      * the RSS feeds previous to BP 1.8.
    2135      *
    2136      * @access protected
    2137      */
    2138     protected function setup_hooks() {
    2139         add_action( 'bp_activity_feed_rss_attributes',   array( $this, 'backpat_rss_attributes' ) );
    2140         add_action( 'bp_activity_feed_channel_elements', array( $this, 'backpat_channel_elements' ) );
    2141         add_action( 'bp_activity_feed_item_elements',    array( $this, 'backpat_item_elements' ) );
    2142     }
    2143 
    2144     /** BACKPAT HOOKS ********************************************************/
    2145 
    2146     /**
    2147      * Fire a hook to ensure backward compatibility for RSS attributes.
    2148      */
    2149     public function backpat_rss_attributes() {
    2150 
    2151         /**
    2152          * Fires inside backpat_rss_attributes method for backwards compatibility related to RSS attributes.
    2153          *
    2154          * This hook was originally separated out for individual components but has since been abstracted into the BP_Activity_Feed class.
    2155          *
    2156          * @since BuddyPress (1.0.0)
    2157          */
    2158         do_action( 'bp_activity_' . $this->id . '_feed' );
    2159     }
    2160 
    2161     /**
    2162      * Fire a hook to ensure backward compatibility for channel elements.
    2163      */
    2164     public function backpat_channel_elements() {
    2165 
    2166         /**
    2167          * Fires inside backpat_channel_elements method for backwards compatibility related to RSS channel elements.
    2168          *
    2169          * This hook was originally separated out for individual components but has since been abstracted into the BP_Activity_Feed class.
    2170          *
    2171          * @since BuddyPress (1.0.0)
    2172          */
    2173         do_action( 'bp_activity_' . $this->id . '_feed_head' );
    2174     }
    2175 
    2176     /**
    2177      * Fire a hook to ensure backward compatibility for item elements.
    2178      */
    2179     public function backpat_item_elements() {
    2180         switch ( $this->id ) {
    2181 
    2182             // sitewide and friends feeds use the 'personal' hook
    2183             case 'sitewide' :
    2184             case 'friends' :
    2185                 $id = 'personal';
    2186 
    2187                 break;
    2188 
    2189             default :
    2190                 $id = $this->id;
    2191 
    2192                 break;
    2193         }
    2194 
    2195         /**
    2196          * Fires inside backpat_item_elements method for backwards compatibility related to RSS item elements.
    2197          *
    2198          * This hook was originally separated out for individual components but has since been abstracted into the BP_Activity_Feed class.
    2199          *
    2200          * @since BuddyPress (1.0.0)
    2201          */
    2202         do_action( 'bp_activity_' . $id . '_feed_item' );
    2203     }
    2204 
    2205     /** HELPERS **************************************************************/
    2206 
    2207     /**
    2208      * Output the feed's item content.
    2209      *
    2210      * @access protected
    2211      */
    2212     protected function feed_content() {
    2213         bp_activity_content_body();
    2214 
    2215         switch ( $this->id ) {
    2216 
    2217             // also output parent activity item if we're on a specific feed
    2218             case 'favorites' :
    2219             case 'friends' :
    2220             case 'mentions' :
    2221             case 'personal' :
    2222 
    2223                 if ( 'activity_comment' == bp_get_activity_action_name() ) :
    2224             ?>
    2225                 <strong><?php _e( 'In reply to', 'buddypress' ) ?></strong> -
    2226                 <?php bp_activity_parent_content() ?>
    2227             <?php
    2228                 endif;
    2229 
    2230                 break;
    2231         }
    2232     }
    2233 
    2234     /**
    2235      * Sets various HTTP headers related to Content-Type and browser caching.
    2236      *
    2237      * Most of this class method is derived from {@link WP::send_headers()}.
    2238      *
    2239      * @since BuddyPress (1.9.0)
    2240      *
    2241      * @access protected
    2242      */
    2243     protected function http_headers() {
    2244         // set up some additional headers if not on a directory page
    2245         // this is done b/c BP uses pseudo-pages
    2246         if ( ! bp_is_directory() ) {
    2247             global $wp_query;
    2248 
    2249             $wp_query->is_404 = false;
    2250             status_header( 200 );
    2251         }
    2252 
    2253         // Set content-type
    2254         @header( 'Content-Type: text/xml; charset=' . get_option( 'blog_charset' ), true );
    2255         send_nosniff_header();
    2256 
    2257         // Cache-related variables
    2258         $last_modified      = mysql2date( 'D, d M Y H:i:s O', bp_activity_get_last_updated(), false );
    2259         $modified_timestamp = strtotime( $last_modified );
    2260         $etag               = md5( $last_modified );
    2261 
    2262         // Set cache-related headers
    2263         @header( 'Last-Modified: ' . $last_modified );
    2264         @header( 'Pragma: no-cache' );
    2265         @header( 'ETag: ' . '"' . $etag . '"' );
    2266 
    2267         // First commit of BuddyPress! (Easter egg)
    2268         @header( 'Expires: Tue, 25 Mar 2008 17:13:55 GMT');
    2269 
    2270         // Get ETag from supported user agents
    2271         if ( isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) ) {
    2272             $client_etag = wp_unslash( $_SERVER['HTTP_IF_NONE_MATCH'] );
    2273 
    2274             // Remove quotes from ETag
    2275             $client_etag = trim( $client_etag, '"' );
    2276 
    2277             // Strip suffixes from ETag if they exist (eg. "-gzip")
    2278             $etag_suffix_pos = strpos( $client_etag, '-' );
    2279             if ( ! empty( $etag_suffix_pos ) ) {
    2280                 $client_etag = substr( $client_etag, 0, $etag_suffix_pos );
    2281             }
    2282 
    2283         // No ETag found
    2284         } else {
    2285             $client_etag = false;
    2286         }
    2287 
    2288         // Get client last modified timestamp from supported user agents
    2289         $client_last_modified      = empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ? '' : trim( $_SERVER['HTTP_IF_MODIFIED_SINCE'] );
    2290         $client_modified_timestamp = $client_last_modified ? strtotime( $client_last_modified ) : 0;
    2291 
    2292         // Set 304 status if feed hasn't been updated since last fetch
    2293         if ( ( $client_last_modified && $client_etag ) ?
    2294                  ( ( $client_modified_timestamp >= $modified_timestamp ) && ( $client_etag == $etag ) ) :
    2295                  ( ( $client_modified_timestamp >= $modified_timestamp ) || ( $client_etag == $etag ) ) ) {
    2296             $status = 304;
    2297         } else {
    2298             $status = false;
    2299         }
    2300 
    2301         // If feed hasn't changed as reported by the user agent, set 304 status header
    2302         if ( ! empty( $status ) ) {
    2303             status_header( $status );
    2304 
    2305             // cached response, so stop now!
    2306             if ( $status == 304 ) {
    2307                 exit();
    2308             }
    2309         }
    2310     }
    2311 
    2312     /** OUTPUT ***************************************************************/
    2313 
    2314     /**
    2315      * Output the RSS feed.
    2316      *
    2317      * @access protected
    2318      */
    2319     protected function output() {
    2320         $this->http_headers();
    2321         echo '<?xml version="1.0" encoding="' . get_option( 'blog_charset' ) . '"?'.'>';
    2322     ?>
    2323 
    2324 <rss version="2.0"
    2325     xmlns:content="http://purl.org/rss/1.0/modules/content/"
    2326     xmlns:atom="http://www.w3.org/2005/Atom"
    2327     xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
    2328     xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
    2329     <?php
    2330 
    2331     /**
    2332      * Fires at the end of the opening RSS tag for feed output so plugins can add extra attributes.
    2333      *
    2334      * @since BuddyPress (1.8.0)
    2335      */
    2336     do_action( 'bp_activity_feed_rss_attributes' ); ?>
    2337 >
    2338 
    2339 <channel>
    2340     <title><?php echo $this->title; ?></title>
    2341     <link><?php echo $this->link; ?></link>
    2342     <atom:link href="<?php self_link(); ?>" rel="self" type="application/rss+xml" />
    2343     <description><?php echo $this->description ?></description>
    2344     <lastBuildDate><?php echo mysql2date( 'D, d M Y H:i:s O', bp_activity_get_last_updated(), false ); ?></lastBuildDate>
    2345     <generator>http://buddypress.org/?v=<?php bp_version(); ?></generator>
    2346     <language><?php bloginfo_rss( 'language' ); ?></language>
    2347     <ttl><?php echo $this->ttl; ?></ttl>
    2348     <sy:updatePeriod><?php echo $this->update_period; ?></sy:updatePeriod>
    2349     <sy:updateFrequency><?php echo $this->update_frequency; ?></sy:updateFrequency>
    2350     <?php
    2351 
    2352     /**
    2353      * Fires at the end of channel elements list in RSS feed so plugins can add extra channel elements.
    2354      *
    2355      * @since BuddyPress (1.8.0)
    2356      */
    2357     do_action( 'bp_activity_feed_channel_elements' ); ?>
    2358 
    2359     <?php if ( bp_has_activities( $this->activity_args ) ) : ?>
    2360         <?php while ( bp_activities() ) : bp_the_activity(); ?>
    2361             <item>
    2362                 <guid isPermaLink="false"><?php bp_activity_feed_item_guid(); ?></guid>
    2363                 <title><?php echo stripslashes( bp_get_activity_feed_item_title() ); ?></title>
    2364                 <link><?php bp_activity_thread_permalink() ?></link>
    2365                 <pubDate><?php echo mysql2date( 'D, d M Y H:i:s O', bp_get_activity_feed_item_date(), false ); ?></pubDate>
    2366 
    2367                 <?php if ( bp_get_activity_feed_item_description() ) : ?>
    2368                     <content:encoded><![CDATA[<?php $this->feed_content(); ?>]]></content:encoded>
    2369                 <?php endif; ?>
    2370 
    2371                 <?php if ( bp_activity_can_comment() ) : ?>
    2372                     <slash:comments><?php bp_activity_comment_count(); ?></slash:comments>
    2373                 <?php endif; ?>
    2374 
    2375                 <?php
    2376 
    2377                 /**
    2378                  * Fires at the end of the individual RSS Item list in RSS feed so plugins can add extra item elements.
    2379                  *
    2380                  * @since BuddyPress (1.8.0)
    2381                  */
    2382                 do_action( 'bp_activity_feed_item_elements' ); ?>
    2383             </item>
    2384         <?php endwhile; ?>
    2385 
    2386     <?php endif; ?>
    2387 </channel>
    2388 </rss><?php
    2389     }
    2390 }
     12require __DIR__ . '/classes/class-bp-activity-activity.php';
     13require __DIR__ . '/classes/class-bp-activity-feed.php';
     14require __DIR__ . '/classes/class-bp-activity-query.php';
  • trunk/src/bp-blogs/bp-blogs-classes.php

    r9471 r9485  
    1111defined( 'ABSPATH' ) || exit;
    1212
    13 /**
    14  * The main BuddyPress blog class.
    15  *
    16  * A BP_Blogs_Object represents a link between a specific WordPress blog on a
    17  * network and a specific user on that blog.
    18  *
    19  * @since BuddyPress (1.0.0)
    20  */
    21 class BP_Blogs_Blog {
    22     public $id;
    23     public $user_id;
    24     public $blog_id;
    25 
    26     /**
    27      * Constructor method.
    28      *
    29      * @param int $id Optional. The ID of the blog.
    30      */
    31     public function __construct( $id = null ) {
    32         if ( !empty( $id ) ) {
    33             $this->id = $id;
    34             $this->populate();
    35         }
    36     }
    37 
    38     /**
    39      * Populate the object with data about the specific activity item.
    40      */
    41     public function populate() {
    42         global $wpdb;
    43 
    44         $bp = buddypress();
    45 
    46         $blog = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$bp->blogs->table_name} WHERE id = %d", $this->id ) );
    47 
    48         $this->user_id = $blog->user_id;
    49         $this->blog_id = $blog->blog_id;
    50     }
    51 
    52     /**
    53      * Save the BP blog data to the database.
    54      *
    55      * @return bool True on success, false on failure.
    56      */
    57     public function save() {
    58         global $wpdb;
    59 
    60         $this->user_id = apply_filters( 'bp_blogs_blog_user_id_before_save', $this->user_id, $this->id );
    61         $this->blog_id = apply_filters( 'bp_blogs_blog_id_before_save', $this->blog_id, $this->id );
    62 
    63         /**
    64          * Fires before the current blog item gets saved.
    65          *
    66          * Please use this hook to filter the properties above. Each part will be passed in.
    67          *
    68          * @since BuddyPress (1.0.0)
    69          *
    70          * @param BP_Blogs_Blog Current instance of the blog item being saved. Passed by reference.
    71          */
    72         do_action_ref_array( 'bp_blogs_blog_before_save', array( &$this ) );
    73 
    74         // Don't try and save if there is no user ID or blog ID set.
    75         if ( !$this->user_id || !$this->blog_id )
    76             return false;
    77 
    78         // Don't save if this blog has already been recorded for the user.
    79         if ( !$this->id && $this->exists() )
    80             return false;
    81 
    82         $bp = buddypress();
    83 
    84         if ( $this->id ) {
    85             // Update
    86             $sql = $wpdb->prepare( "UPDATE {$bp->blogs->table_name} SET user_id = %d, blog_id = %d WHERE id = %d", $this->user_id, $this->blog_id, $this->id );
    87         } else {
    88             // Save
    89             $sql = $wpdb->prepare( "INSERT INTO {$bp->blogs->table_name} ( user_id, blog_id ) VALUES ( %d, %d )", $this->user_id, $this->blog_id );
    90         }
    91 
    92         if ( !$wpdb->query($sql) )
    93             return false;
    94 
    95         /**
    96          * Fires after the current blog item gets saved.
    97          *
    98          * Please use this hook to filter the properties above. Each part will be passed in.
    99          *
    100          * @since BuddyPress (1.0.0)
    101          *
    102          * @param BP_Blogs_Blog Current instance of the blog item being saved. Passed by reference.
    103          */
    104         do_action_ref_array( 'bp_blogs_blog_after_save', array( &$this ) );
    105 
    106         if ( $this->id )
    107             return $this->id;
    108         else
    109             return $wpdb->insert_id;
    110     }
    111 
    112     /**
    113      * Check whether an association between this user and this blog exists.
    114      *
    115      * @return int The number of associations between the user and blog
    116      *         saved in the blog component tables.
    117      */
    118     public function exists() {
    119         global $wpdb;
    120 
    121         $bp = buddypress();
    122 
    123         return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM {$bp->blogs->table_name} WHERE user_id = %d AND blog_id = %d", $this->user_id, $this->blog_id ) );
    124     }
    125 
    126     /** Static Methods ***************************************************/
    127 
    128     /**
    129      * Retrieve a set of blog-user associations.
    130      *
    131      * @param string $type The order in which results should be returned.
    132      *        'active', 'alphabetical', 'newest', or 'random'.
    133      * @param int|bool $limit Optional. The maximum records to return.
    134      *        Default: false.
    135      * @param int|bool $page Optional. The page of records to return.
    136      *        Default: false (unlimited results).
    137      * @param int $user_id Optional. ID of the user whose blogs are being
    138      *        retrieved. Default: 0.
    139      * @param string|bool $search_terms Optional. Search by text stored in
    140      *        blogmeta (such as the blog name). Default: false.
    141      * @param bool $update_meta_cache Whether to pre-fetch metadata for
    142      *        blogs. Default: true.
    143      * @param array $include_blog_ids Array of blog IDs to include.
    144      * @return array Multidimensional results array, structured as follows:
    145      *           'blogs' - Array of located blog objects
    146      *           'total' - A count of the total blogs matching the filter params
    147      */
    148     public static function get( $type, $limit = false, $page = false, $user_id = 0, $search_terms = false, $update_meta_cache = true, $include_blog_ids = false ) {
    149         global $wpdb;
    150 
    151         $bp = buddypress();
    152 
    153         if ( !is_user_logged_in() || ( !bp_current_user_can( 'bp_moderate' ) && ( $user_id != bp_loggedin_user_id() ) ) )
    154             $hidden_sql = "AND wb.public = 1";
    155         else
    156             $hidden_sql = '';
    157 
    158         $pag_sql = ( $limit && $page ) ? $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) ) : '';
    159 
    160         $user_sql = !empty( $user_id ) ? $wpdb->prepare( " AND b.user_id = %d", $user_id ) : '';
    161 
    162         switch ( $type ) {
    163             case 'active': default:
    164                 $order_sql = "ORDER BY bm.meta_value DESC";
    165                 break;
    166             case 'alphabetical':
    167                 $order_sql = "ORDER BY bm_name.meta_value ASC";
    168                 break;
    169             case 'newest':
    170                 $order_sql = "ORDER BY wb.registered DESC";
    171                 break;
    172             case 'random':
    173                 $order_sql = "ORDER BY RAND()";
    174                 break;
    175         }
    176 
    177         $include_sql = '';
    178         $include_blog_ids = array_filter( wp_parse_id_list( $include_blog_ids ) );
    179         if ( ! empty( $include_blog_ids ) ) {
    180             $blog_ids_sql = implode( ',', $include_blog_ids );
    181             $include_sql  = " AND b.blog_id IN ({$blog_ids_sql})";
    182         }
    183 
    184         if ( ! empty( $search_terms ) ) {
    185             $search_terms_like = '%' . bp_esc_like( $search_terms ) . '%';
    186             $search_terms_sql  = $wpdb->prepare( 'AND (bm_name.meta_value LIKE %s OR bm_description.meta_value LIKE %s)', $search_terms_like, $search_terms_like );
    187         } else {
    188             $search_terms_sql = '';
    189         }
    190 
    191         $paged_blogs = $wpdb->get_results( "
    192             SELECT b.blog_id, b.user_id as admin_user_id, u.user_email as admin_user_email, wb.domain, wb.path, bm.meta_value as last_activity, bm_name.meta_value as name
    193             FROM
    194               {$bp->blogs->table_name} b
    195               LEFT JOIN {$bp->blogs->table_name_blogmeta} bm ON (b.blog_id = bm.blog_id)
    196               LEFT JOIN {$bp->blogs->table_name_blogmeta} bm_name ON (b.blog_id = bm_name.blog_id)
    197               LEFT JOIN {$bp->blogs->table_name_blogmeta} bm_description ON (b.blog_id = bm_description.blog_id)
    198               LEFT JOIN {$wpdb->base_prefix}blogs wb ON (b.blog_id = wb.blog_id)
    199               LEFT JOIN {$wpdb->users} u ON (b.user_id = u.ID)
    200             WHERE
    201               wb.archived = '0' AND wb.spam = 0 AND wb.mature = 0 AND wb.deleted = 0 {$hidden_sql}
    202               AND bm.meta_key = 'last_activity' AND bm_name.meta_key = 'name' AND bm_description.meta_key = 'description'
    203               {$search_terms_sql} {$user_sql} {$include_sql}
    204             GROUP BY b.blog_id {$order_sql} {$pag_sql}
    205         " );
    206 
    207         $total_blogs = $wpdb->get_var( "
    208             SELECT COUNT(DISTINCT b.blog_id)
    209             FROM
    210               {$bp->blogs->table_name} b
    211               LEFT JOIN {$wpdb->base_prefix}blogs wb ON (b.blog_id = wb.blog_id)
    212               LEFT JOIN {$bp->blogs->table_name_blogmeta} bm_name ON (b.blog_id = bm_name.blog_id)
    213               LEFT JOIN {$bp->blogs->table_name_blogmeta} bm_description ON (b.blog_id = bm_description.blog_id)
    214             WHERE
    215               wb.archived = '0' AND wb.spam = 0 AND wb.mature = 0 AND wb.deleted = 0 {$hidden_sql}
    216               AND
    217               bm_name.meta_key = 'name' AND bm_description.meta_key = 'description'
    218               {$search_terms_sql} {$user_sql} {$include_sql}
    219         " );
    220 
    221         $blog_ids = array();
    222         foreach ( (array) $paged_blogs as $blog ) {
    223             $blog_ids[] = (int) $blog->blog_id;
    224         }
    225 
    226         $paged_blogs = BP_Blogs_Blog::get_blog_extras( $paged_blogs, $blog_ids, $type );
    227 
    228         if ( $update_meta_cache ) {
    229             bp_blogs_update_meta_cache( $blog_ids );
    230         }
    231 
    232         return array( 'blogs' => $paged_blogs, 'total' => $total_blogs );
    233     }
    234 
    235     /**
    236      * Delete the record of a given blog for all users.
    237      *
    238      * @param int $blog_id The blog being removed from all users.
    239      * @return int|bool Number of rows deleted on success, false on failure.
    240      */
    241     public static function delete_blog_for_all( $blog_id ) {
    242         global $wpdb;
    243 
    244         bp_blogs_delete_blogmeta( $blog_id );
    245        
    246         $bp = buddypress();
    247 
    248         return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->blogs->table_name} WHERE blog_id = %d", $blog_id ) );
    249     }
    250 
    251     /**
    252      * Delete the record of a given blog for a specific user.
    253      *
    254      * @param int $blog_id The blog being removed.
    255      * @param int $user_id Optional. The ID of the user from whom the blog
    256      *        is being removed. If absent, defaults to the logged-in user ID.
    257      * @return int|bool Number of rows deleted on success, false on failure.
    258      */
    259     public static function delete_blog_for_user( $blog_id, $user_id = null ) {
    260         global $wpdb;
    261 
    262         if ( !$user_id )
    263             $user_id = bp_loggedin_user_id();
    264 
    265         $bp = buddypress();
    266 
    267         return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->blogs->table_name} WHERE user_id = %d AND blog_id = %d", $user_id, $blog_id ) );
    268     }
    269 
    270     /**
    271      * Delete all of a user's blog associations in the BP tables.
    272      *
    273      * @param int $user_id Optional. The ID of the user whose blog
    274      *        associations are being deleted. If absent, defaults to
    275      *        logged-in user ID.
    276      * @return int|bool Number of rows deleted on success, false on failure.
    277      */
    278     public static function delete_blogs_for_user( $user_id = null ) {
    279         global $wpdb;
    280 
    281         if ( !$user_id )
    282             $user_id = bp_loggedin_user_id();
    283 
    284         $bp = buddypress();
    285 
    286         return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->blogs->table_name} WHERE user_id = %d", $user_id ) );
    287     }
    288 
    289     /**
    290      * Get all of a user's blogs, as tracked by BuddyPress.
    291      *
    292      * Note that this is different from the WordPress function
    293      * {@link get_blogs_of_user()}; the current method returns only those
    294      * blogs that have been recorded by BuddyPress, while the WP function
    295      * does a true query of a user's blog capabilities.
    296      *
    297      * @param int $user_id Optional. ID of the user whose blogs are being
    298      *        queried. Defaults to logged-in user.
    299      * @param bool $show_hidden Optional. Whether to include blogs that are
    300      *        not marked public. Defaults to true when viewing one's own
    301      *        profile.
    302      * @return array Multidimensional results array, structured as follows:
    303      *           'blogs' - Array of located blog objects
    304      *           'total' - A count of the total blogs for the user.
    305      */
    306     public static function get_blogs_for_user( $user_id = 0, $show_hidden = false ) {
    307         global $wpdb;
    308 
    309         $bp = buddypress();
    310 
    311         if ( !$user_id )
    312             $user_id = bp_displayed_user_id();
    313 
    314         // Show logged in users their hidden blogs.
    315         if ( !bp_is_my_profile() && !$show_hidden )
    316             $blogs = $wpdb->get_results( $wpdb->prepare( "SELECT DISTINCT b.blog_id, b.id, bm1.meta_value as name, wb.domain, wb.path FROM {$bp->blogs->table_name} b, {$wpdb->base_prefix}blogs wb, {$bp->blogs->table_name_blogmeta} bm1 WHERE b.blog_id = wb.blog_id AND b.blog_id = bm1.blog_id AND bm1.meta_key = 'name' AND wb.public = 1 AND wb.deleted = 0 AND wb.spam = 0 AND wb.mature = 0 AND wb.archived = '0' AND b.user_id = %d ORDER BY b.blog_id", $user_id ) );
    317         else
    318             $blogs = $wpdb->get_results( $wpdb->prepare( "SELECT DISTINCT b.blog_id, b.id, bm1.meta_value as name, wb.domain, wb.path FROM {$bp->blogs->table_name} b, {$wpdb->base_prefix}blogs wb, {$bp->blogs->table_name_blogmeta} bm1 WHERE b.blog_id = wb.blog_id AND b.blog_id = bm1.blog_id AND bm1.meta_key = 'name' AND wb.deleted = 0 AND wb.spam = 0 AND wb.mature = 0 AND wb.archived = '0' AND b.user_id = %d ORDER BY b.blog_id", $user_id ) );
    319 
    320         $total_blog_count = BP_Blogs_Blog::total_blog_count_for_user( $user_id );
    321 
    322         $user_blogs = array();
    323         foreach ( (array) $blogs as $blog ) {
    324             $user_blogs[$blog->blog_id] = new stdClass;
    325             $user_blogs[$blog->blog_id]->id = $blog->id;
    326             $user_blogs[$blog->blog_id]->blog_id = $blog->blog_id;
    327             $user_blogs[$blog->blog_id]->siteurl = ( is_ssl() ) ? 'https://' . $blog->domain . $blog->path : 'http://' . $blog->domain . $blog->path;
    328             $user_blogs[$blog->blog_id]->name = $blog->name;
    329         }
    330 
    331         return array( 'blogs' => $user_blogs, 'count' => $total_blog_count );
    332     }
    333 
    334     /**
    335      * Get IDs of all of a user's blogs, as tracked by BuddyPress.
    336      *
    337      * This method always includes hidden blogs.
    338      *
    339      * @param int $user_id Optional. ID of the user whose blogs are being
    340      *        queried. Defaults to logged-in user.
    341      * @return int The number of blogs associated with the user.
    342      */
    343     public static function get_blog_ids_for_user( $user_id = 0 ) {
    344         global $wpdb;
    345 
    346         $bp = buddypress();
    347 
    348         if ( !$user_id )
    349             $user_id = bp_displayed_user_id();
    350 
    351         return $wpdb->get_col( $wpdb->prepare( "SELECT blog_id FROM {$bp->blogs->table_name} WHERE user_id = %d", $user_id ) );
    352     }
    353 
    354     /**
    355      * Check whether a blog has been recorded by BuddyPress.
    356      *
    357      * @param int $blog_id ID of the blog being queried.
    358      * @return int|null The ID of the first located entry in the BP table
    359      *         on success, otherwise null.
    360      */
    361     public static function is_recorded( $blog_id ) {
    362         global $wpdb;
    363 
    364         $bp = buddypress();
    365 
    366         return $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$bp->blogs->table_name} WHERE blog_id = %d", $blog_id ) );
    367     }
    368 
    369     /**
    370      * Return a count of associated blogs for a given user.
    371      *
    372      * Includes hidden blogs when the logged-in user is the same as the
    373      * $user_id parameter, or when the logged-in user has the bp_moderate
    374      * cap.
    375      *
    376      * @param int $user_id Optional. ID of the user whose blogs are being
    377      *        queried. Defaults to logged-in user.
    378      * @return int Blog count for the user.
    379      */
    380     public static function total_blog_count_for_user( $user_id = null ) {
    381         global $wpdb;
    382 
    383         $bp = buddypress();
    384 
    385         if ( !$user_id )
    386             $user_id = bp_displayed_user_id();
    387 
    388         // If the user is logged in return the blog count including their hidden blogs.
    389         if ( ( is_user_logged_in() && $user_id == bp_loggedin_user_id() ) || bp_current_user_can( 'bp_moderate' ) ) {
    390             return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT b.blog_id) FROM {$bp->blogs->table_name} b LEFT JOIN {$wpdb->base_prefix}blogs wb ON b.blog_id = wb.blog_id WHERE wb.deleted = 0 AND wb.spam = 0 AND wb.mature = 0 AND wb.archived = '0' AND user_id = %d", $user_id ) );
    391         } else {
    392             return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT b.blog_id) FROM {$bp->blogs->table_name} b LEFT JOIN {$wpdb->base_prefix}blogs wb ON b.blog_id = wb.blog_id WHERE wb.public = 1 AND wb.deleted = 0 AND wb.spam = 0 AND wb.mature = 0 AND wb.archived = '0' AND user_id = %d", $user_id ) );
    393         }
    394     }
    395 
    396     /**
    397      * Return a list of blogs matching a search term.
    398      *
    399      * Matches against blog names and descriptions, as stored in the BP
    400      * blogmeta table.
    401      *
    402      * @param string $filter The search term.
    403      * @param int $limit Optional. The maximum number of items to return.
    404      *        Default: null (no limit).
    405      * @param int $page Optional. The page of results to return. Default:
    406      *        null (no limit).
    407      * @return array Multidimensional results array, structured as follows:
    408      *           'blogs' - Array of located blog objects
    409      *           'total' - A count of the total blogs matching the query.
    410      */
    411     public static function search_blogs( $filter, $limit = null, $page = null ) {
    412         global $wpdb;
    413 
    414         $search_terms_like = '%' . bp_esc_like( $filter ) . '%';
    415         $search_terms_sql  = $wpdb->prepare( 'bm.meta_value LIKE %s', $search_terms_like );
    416 
    417         $hidden_sql = '';
    418         if ( !bp_current_user_can( 'bp_moderate' ) )
    419             $hidden_sql = "AND wb.public = 1";
    420 
    421         $pag_sql = '';
    422         if ( $limit && $page ) {
    423             $pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
    424         }
    425 
    426         $bp = buddypress();
    427 
    428         $paged_blogs = $wpdb->get_results( "SELECT DISTINCT bm.blog_id FROM {$bp->blogs->table_name_blogmeta} bm LEFT JOIN {$wpdb->base_prefix}blogs wb ON bm.blog_id = wb.blog_id WHERE ( ( bm.meta_key = 'name' OR bm.meta_key = 'description' ) AND {$search_terms_sql} ) {$hidden_sql} AND wb.mature = 0 AND wb.spam = 0 AND wb.archived = '0' AND wb.deleted = 0 ORDER BY meta_value ASC{$pag_sql}" );
    429         $total_blogs = $wpdb->get_var( "SELECT COUNT(DISTINCT bm.blog_id) FROM {$bp->blogs->table_name_blogmeta} bm LEFT JOIN {$wpdb->base_prefix}blogs wb ON bm.blog_id = wb.blog_id WHERE ( ( bm.meta_key = 'name' OR bm.meta_key = 'description' ) AND {$search_terms_sql} ) {$hidden_sql} AND wb.mature = 0 AND wb.spam = 0 AND wb.archived = '0' AND wb.deleted = 0 ORDER BY meta_value ASC" );
    430 
    431         return array( 'blogs' => $paged_blogs, 'total' => $total_blogs );
    432     }
    433 
    434     /**
    435      * Retrieve a list of all blogs.
    436      *
    437      * Query will include hidden blogs if the logged-in user has the
    438      * 'bp_moderate' cap.
    439      *
    440      * @param int $limit Optional. The maximum number of items to return.
    441      *        Default: null (no limit).
    442      * @param int $page Optional. The page of results to return. Default:
    443      *        null (no limit).
    444      * @return array Multidimensional results array, structured as follows:
    445      *           'blogs' - Array of located blog objects
    446      *           'total' - A count of the total blogs.
    447      */
    448     public static function get_all( $limit = null, $page = null ) {
    449         global $wpdb;
    450 
    451         $bp = buddypress();
    452 
    453         $hidden_sql = !bp_current_user_can( 'bp_moderate' ) ? "AND wb.public = 1" : '';
    454         $pag_sql    = ( $limit && $page ) ? $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) ) : '';
    455 
    456         $paged_blogs = $wpdb->get_results( "SELECT DISTINCT b.blog_id FROM {$bp->blogs->table_name} b LEFT JOIN {$wpdb->base_prefix}blogs wb ON b.blog_id = wb.blog_id WHERE wb.mature = 0 AND wb.spam = 0 AND wb.archived = '0' AND wb.deleted = 0 {$hidden_sql} {$pag_sql}" );
    457         $total_blogs = $wpdb->get_var( "SELECT COUNT(DISTINCT b.blog_id) FROM {$bp->blogs->table_name} b LEFT JOIN {$wpdb->base_prefix}blogs wb ON b.blog_id = wb.blog_id WHERE wb.mature = 0 AND wb.spam = 0 AND wb.archived = '0' AND wb.deleted = 0 {$hidden_sql}" );
    458 
    459         return array( 'blogs' => $paged_blogs, 'total' => $total_blogs );
    460     }
    461 
    462     /**
    463      * Retrieve a list of blogs whose names start with a given letter.
    464      *
    465      * Query will include hidden blogs if the logged-in user has the
    466      * 'bp_moderate' cap.
    467      *
    468      * @param string $letter. The letter you're looking for.
    469      * @param int $limit Optional. The maximum number of items to return.
    470      *        Default: null (no limit).
    471      * @param int $page Optional. The page of results to return. Default:
    472      *        null (no limit).
    473      * @return array Multidimensional results array, structured as follows:
    474      *           'blogs' - Array of located blog objects.
    475      *           'total' - A count of the total blogs matching the query.
    476      */
    477     public static function get_by_letter( $letter, $limit = null, $page = null ) {
    478         global $wpdb;
    479 
    480         $bp = buddypress();
    481 
    482         $letter_like = '%' . bp_esc_like( $letter ) . '%';
    483         $letter_sql  = $wpdb->prepare( 'bm.meta_value LIKE %s', $letter_like );
    484 
    485         $hidden_sql = '';
    486         if ( !bp_current_user_can( 'bp_moderate' ) )
    487             $hidden_sql = "AND wb.public = 1";
    488 
    489         $pag_sql = '';
    490         if ( $limit && $page )
    491             $pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
    492 
    493         $paged_blogs = $wpdb->get_results( "SELECT DISTINCT bm.blog_id FROM {$bp->blogs->table_name_blogmeta} bm LEFT JOIN {$wpdb->base_prefix}blogs wb ON bm.blog_id = wb.blog_id WHERE bm.meta_key = 'name' AND {$letter_sql} {$hidden_sql} AND wb.mature = 0 AND wb.spam = 0 AND wb.archived = '0' AND wb.deleted = 0 ORDER BY bm.meta_value ASC{$pag_sql}" );
    494         $total_blogs = $wpdb->get_var( "SELECT COUNT(DISTINCT bm.blog_id) FROM {$bp->blogs->table_name_blogmeta} bm LEFT JOIN {$wpdb->base_prefix}blogs wb ON bm.blog_id = wb.blog_id WHERE bm.meta_key = 'name' AND {$letter_sql} {$hidden_sql} AND wb.mature = 0 AND wb.spam = 0 AND wb.archived = '0' AND wb.deleted = 0 ORDER BY bm.meta_value ASC" );
    495 
    496         return array( 'blogs' => $paged_blogs, 'total' => $total_blogs );
    497     }
    498 
    499     /**
    500      * Fetch blog data not caught in the main query and append it to results array.
    501      *
    502      * Gets the following information, which is either unavailable at the
    503      * time of the original query, or is more efficient to look up in one
    504      * fell swoop:
    505      *   - The latest post for each blog, include Featured Image data
    506      *   - The blog description
    507      *
    508      * @param array $paged_blogs Array of results from the original query.
    509      * @param array $blog_ids Array of IDs returned from the original query.
    510      * @param string|bool $type Not currently used. Default: false.
    511      * @return array $paged_blogs The located blogs array, with the extras added.
    512      */
    513     public static function get_blog_extras( &$paged_blogs, &$blog_ids, $type = false ) {
    514         global $wpdb;
    515 
    516         $bp = buddypress();
    517 
    518         if ( empty( $blog_ids ) )
    519             return $paged_blogs;
    520 
    521         $blog_ids = implode( ',', wp_parse_id_list( $blog_ids ) );
    522 
    523         for ( $i = 0, $count = count( $paged_blogs ); $i < $count; ++$i ) {
    524             $blog_prefix = $wpdb->get_blog_prefix( $paged_blogs[$i]->blog_id );
    525             $paged_blogs[$i]->latest_post = $wpdb->get_row( "SELECT ID, post_content, post_title, post_excerpt, guid FROM {$blog_prefix}posts WHERE post_status = 'publish' AND post_type = 'post' AND id != 1 ORDER BY id DESC LIMIT 1" );
    526             $images = array();
    527 
    528             // Add URLs to any Featured Image this post might have
    529             if ( ! empty( $paged_blogs[$i]->latest_post ) && has_post_thumbnail( $paged_blogs[$i]->latest_post->ID ) ) {
    530 
    531                 // Grab 4 sizes of the image. Thumbnail.
    532                 $image = wp_get_attachment_image_src( get_post_thumbnail_id( $paged_blogs[$i]->latest_post->ID ), 'thumbnail', false );
    533                 if ( ! empty( $image ) )
    534                     $images['thumbnail'] = $image[0];
    535 
    536                 // Medium
    537                 $image = wp_get_attachment_image_src( get_post_thumbnail_id( $paged_blogs[$i]->latest_post->ID ), 'medium', false );
    538                 if ( ! empty( $image ) )
    539                     $images['medium'] = $image[0];
    540 
    541                 // Large
    542                 $image = wp_get_attachment_image_src( get_post_thumbnail_id( $paged_blogs[$i]->latest_post->ID ), 'large', false );
    543                 if ( ! empty( $image ) )
    544                     $images['large'] = $image[0];
    545 
    546                 // Post thumbnail
    547                 $image = wp_get_attachment_image_src( get_post_thumbnail_id( $paged_blogs[$i]->latest_post->ID ), 'post-thumbnail', false );
    548                 if ( ! empty( $image ) )
    549                     $images['post-thumbnail'] = $image[0];
    550 
    551                 // Add the images to the latest_post object
    552                 $paged_blogs[$i]->latest_post->images = $images;
    553             }
    554         }
    555 
    556         /* Fetch the blog description for each blog (as it may be empty we can't fetch it in the main query). */
    557         $blog_descs = $wpdb->get_results( "SELECT blog_id, meta_value as description FROM {$bp->blogs->table_name_blogmeta} WHERE meta_key = 'description' AND blog_id IN ( {$blog_ids} )" );
    558 
    559         for ( $i = 0, $count = count( $paged_blogs ); $i < $count; ++$i ) {
    560             foreach ( (array) $blog_descs as $desc ) {
    561                 if ( $desc->blog_id == $paged_blogs[$i]->blog_id )
    562                     $paged_blogs[$i]->description = $desc->description;
    563             }
    564         }
    565 
    566         return $paged_blogs;
    567     }
    568 
    569     /**
    570      * Check whether a given blog is hidden.
    571      *
    572      * Checks the 'public' column in the wp_blogs table.
    573      *
    574      * @param int $blog_id The ID of the blog being checked.
    575      * @return bool True if hidden (public = 0), false otherwise.
    576      */
    577     public static function is_hidden( $blog_id ) {
    578         global $wpdb;
    579 
    580         if ( !(int) $wpdb->get_var( $wpdb->prepare( "SELECT public FROM {$wpdb->base_prefix}blogs WHERE blog_id = %d", $blog_id ) ) ) {
    581             return true;
    582         }
    583 
    584         return false;
    585     }
    586 
    587     /**
    588      * Get ID of user-blog link.
    589      *
    590      * @param int $user_id ID of user.
    591      * @param int $blog_id ID of blog.
    592      * @return int|bool ID of user-blog link, or false if not found.
    593      */
    594     public static function get_user_blog( $user_id, $blog_id ) {
    595         global $wpdb;
    596 
    597         $bp = buddypress();
    598 
    599         $user_blog = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$bp->blogs->table_name} WHERE user_id = %d AND blog_id = %d", $user_id, $blog_id ) );
    600 
    601         if ( empty( $user_blog ) ) {
    602             $user_blog = false;
    603         } else {
    604             $user_blog = intval( $user_blog );
    605         }
    606 
    607         return $user_blog;
    608     }
    609 }
     13require __DIR__ . '/classes/class-bp-blogs-blog.php';
  • trunk/src/bp-core/bp-core-classes.php

    r9471 r9485  
    1010defined( 'ABSPATH' ) || exit;
    1111
    12 /**
    13  * BuddyPress User Query class.
    14  *
    15  * Used for querying users in a BuddyPress context, in situations where WP_User_Query won't do the trick:
    16  * Member directories, the Friends component, etc.
    17  *
    18  * @since BuddyPress (1.7.0)
    19  *
    20  * @param array $query {
    21  *     Query arguments. All items are optional.
    22  *     @type string            $type            Determines sort order. Select from 'newest', 'active', 'online',
    23  *                                              'random', 'popular', 'alphabetical'. Default: 'newest'.
    24  *     @type int               $per_page Number of results to return. Default: 0 (no limit).
    25  *     @type int               $page            Page offset (together with $per_page). Default: 1.
    26  *     @type int               $user_id         ID of a user. If present, and if the friends component is activated,
    27  *                                              results will be limited to the friends of that user. Default: 0.
    28  *     @type string|bool       $search_terms    Terms to search by. Search happens across xprofile fields. Requires
    29  *                                              XProfile component. Default: false.
    30  *     @type string            $search_wildcard When searching with $search_terms, set where wildcards around the term
    31  *                                              should be positioned. Accepts 'both', 'left', 'right'. Default: 'both'.
    32  *     @type array|string|bool $include         An array or comma-separated list of user IDs to which query should
    33  *                                              be limited. Default: false.
    34  *     @type array|string|bool $exclude         An array or comma-separated list of user IDs that will be excluded from
    35  *                                              query results. Default: false.
    36  *     @type array|string|bool $user_ids        An array or comma-separated list of IDs corresponding to the users
    37  *                                              that should be returned. When this parameter is passed, it will
    38  *                                              override all others; BP User objects will be constructed using these
    39  *                                              IDs only. Default: false.
    40  *     @type array|string      $member_type     Array or comma-separated list of member types to limit results to.
    41  *     @type string|bool       $meta_key        Limit results to users that have usermeta associated with this meta_key.
    42  *                                              Usually used with $meta_value. Default: false.
    43  *     @type string|bool       $meta_value      When used with $meta_key, limits results to users whose usermeta value
    44  *                                              associated with $meta_key matches $meta_value. Default: false.
    45  *     @type array             $xprofile_query  Filter results by xprofile data. Requires the xprofile component. See
    46  *                                              {@see BP_XProfile_Query} for details.
    47  *     @type bool              $populate_extras True if you want to fetch extra metadata
    48  *                                              about returned users, such as total group and friend counts.
    49  *     @type string            $count_total     Determines how BP_User_Query will do a count of total users matching
    50  *                                              the other filter criteria. Default value is 'count_query', which does
    51  *                                              a separate SELECT COUNT query to determine the total.
    52  *                                              'sql_count_found_rows' uses SQL_COUNT_FOUND_ROWS and
    53  *                                              SELECT FOUND_ROWS(). Pass an empty string to skip the total user
    54  *                                              count query.
    55  * }
    56  */
    57 class BP_User_Query {
    58 
    59     /** Variables *************************************************************/
    60 
    61     /**
    62      * Unaltered params as passed to the constructor.
    63      *
    64      * @since BuddyPress (1.8.0)
    65      * @var array
    66      */
    67     public $query_vars_raw = array();
    68 
    69     /**
    70      * Array of variables to query with.
    71      *
    72      * @since BuddyPress (1.7.0)
    73      * @var array
    74      */
    75     public $query_vars = array();
    76 
    77     /**
    78      * List of found users and their respective data.
    79      *
    80      * @access public To allow components to manipulate them.
    81      * @since BuddyPress (1.7.0)
    82      * @var array
    83      */
    84     public $results = array();
    85 
    86     /**
    87      * Total number of found users for the current query.
    88      *
    89      * @access public To allow components to manipulate it.
    90      * @since BuddyPress (1.7.0)
    91      * @var int
    92      */
    93     public $total_users = 0;
    94 
    95     /**
    96      * List of found user IDs.
    97      *
    98      * @access public To allow components to manipulate it.
    99      * @since BuddyPress (1.7.0)
    100      * @var array
    101      */
    102     public $user_ids = array();
    103 
    104     /**
    105      * SQL clauses for the user ID query.
    106      *
    107      * @access public To allow components to manipulate it.
    108      * @since BuddyPress (1.7.0)
    109      * @var array
    110      */
    111     public $uid_clauses = array();
    112 
    113     /**
    114      * SQL table where the user ID is being fetched from.
    115      *
    116      * @since BuddyPress (2.2.0)
    117      * @access public
    118      * @var string
    119      */
    120     public $uid_table = '';
    121 
    122     /**
    123      * SQL database column name to order by.
    124      *
    125      * @since BuddyPress (1.7.0)
    126      * @var string
    127      */
    128     public $uid_name = '';
    129 
    130     /**
    131      * Standard response when the query should not return any rows.
    132      *
    133      * @access protected
    134      * @since BuddyPress (1.7.0)
    135      * @var string
    136      */
    137     protected $no_results = array( 'join' => '', 'where' => '0 = 1' );
    138 
    139 
    140     /** Methods ***************************************************************/
    141 
    142     /**
    143      * Constructor.
    144      *
    145      * @since BuddyPress (1.7.0)
    146      *
    147      * @param string|array $query See {@link BP_User_Query}.
    148      */
    149     public function __construct( $query = null ) {
    150 
    151         // Store the raw query vars for later access
    152         $this->query_vars_raw = $query;
    153 
    154         // Allow extending classes to register action/filter hooks
    155         $this->setup_hooks();
    156 
    157         if ( ! empty( $this->query_vars_raw ) ) {
    158             $this->query_vars = wp_parse_args( $this->query_vars_raw, array(
    159                 'type'            => 'newest',
    160                 'per_page'        => 0,
    161                 'page'            => 1,
    162                 'user_id'         => 0,
    163                 'search_terms'    => false,
    164                 'search_wildcard' => 'both',
    165                 'include'         => false,
    166                 'exclude'         => false,
    167                 'user_ids'        => false,
    168                 'member_type'     => '',
    169                 'meta_key'        => false,
    170                 'meta_value'      => false,
    171                 'xprofile_query'  => false,
    172                 'populate_extras' => true,
    173                 'count_total'     => 'count_query'
    174             ) );
    175 
    176             // Plugins can use this filter to modify query args
    177             // before the query is constructed
    178             do_action_ref_array( 'bp_pre_user_query_construct', array( &$this ) );
    179 
    180             // Get user ids
    181             // If the user_ids param is present, we skip the query
    182             if ( false !== $this->query_vars['user_ids'] ) {
    183                 $this->user_ids = wp_parse_id_list( $this->query_vars['user_ids'] );
    184             } else {
    185                 $this->prepare_user_ids_query();
    186                 $this->do_user_ids_query();
    187             }
    188         }
    189 
    190         // Bail if no user IDs were found
    191         if ( empty( $this->user_ids ) ) {
    192             return;
    193         }
    194 
    195         // Fetch additional data. First, using WP_User_Query
    196         $this->do_wp_user_query();
    197 
    198         // Get BuddyPress specific user data
    199         $this->populate_extras();
    200     }
    201 
    202     /**
    203      * Allow extending classes to set up action/filter hooks.
    204      *
    205      * When extending BP_User_Query, you may need to use some of its
    206      * internal hooks to modify the output. It's not convenient to call
    207      * add_action() or add_filter() in your class constructor, because
    208      * BP_User_Query::__construct() contains a fair amount of logic that
    209      * you may not want to override in your class. Define this method in
    210      * your own class if you need a place where your extending class can
    211      * add its hooks early in the query-building process. See
    212      * {@link BP_Group_Member_Query::setup_hooks()} for an example.
    213      *
    214      * @since BuddyPress (1.8.0)
    215      */
    216     public function setup_hooks() {}
    217 
    218     /**
    219      * Prepare the query for user_ids.
    220      *
    221      * @since BuddyPress (1.7.0)
    222      */
    223     public function prepare_user_ids_query() {
    224         global $wpdb;
    225 
    226         $bp = buddypress();
    227 
    228         // Default query variables used here
    229         $type         = '';
    230         $per_page     = 0;
    231         $page         = 1;
    232         $user_id      = 0;
    233         $include      = false;
    234         $search_terms = false;
    235         $exclude      = false;
    236         $meta_key     = false;
    237         $meta_value   = false;
    238 
    239         extract( $this->query_vars );
    240 
    241         // Setup the main SQL query container
    242         $sql = array(
    243             'select'  => '',
    244             'where'   => array(),
    245             'orderby' => '',
    246             'order'   => '',
    247             'limit'   => ''
    248         );
    249 
    250         /** TYPE **************************************************************/
    251 
    252         // Determines the sort order, which means it also determines where the
    253         // user IDs are drawn from (the SELECT and WHERE statements)
    254         switch ( $type ) {
    255 
    256             // 'online' query happens against the last_activity usermeta key
    257             // Filter 'bp_user_query_online_interval' to modify the
    258             // number of minutes used as an interval
    259             case 'online' :
    260                 $this->uid_name = 'user_id';
    261                 $this->uid_table = $bp->members->table_name_last_activity;
    262                 $sql['select']  = "SELECT u.{$this->uid_name} as id FROM {$this->uid_table} u";
    263                 $sql['where'][] = $wpdb->prepare( "u.component = %s AND u.type = 'last_activity'", buddypress()->members->id );
    264                 $sql['where'][] = $wpdb->prepare( "u.date_recorded >= DATE_SUB( UTC_TIMESTAMP(), INTERVAL %d MINUTE )", apply_filters( 'bp_user_query_online_interval', 15 ) );
    265                 $sql['orderby'] = "ORDER BY u.date_recorded";
    266                 $sql['order']   = "DESC";
    267 
    268                 break;
    269 
    270             // 'active', 'newest', and 'random' queries
    271             // all happen against the last_activity usermeta key
    272             case 'active' :
    273             case 'newest' :
    274             case 'random' :
    275                 $this->uid_name = 'user_id';
    276                 $this->uid_table = $bp->members->table_name_last_activity;
    277                 $sql['select']  = "SELECT u.{$this->uid_name} as id FROM {$this->uid_table} u";
    278                 $sql['where'][] = $wpdb->prepare( "u.component = %s AND u.type = 'last_activity'", buddypress()->members->id );
    279 
    280                 if ( 'newest' == $type ) {
    281                     $sql['orderby'] = "ORDER BY u.user_id";
    282                     $sql['order'] = "DESC";
    283                 } elseif ( 'random' == $type ) {
    284                     $sql['orderby'] = "ORDER BY rand()";
    285                 } else {
    286                     $sql['orderby'] = "ORDER BY u.date_recorded";
    287                     $sql['order'] = "DESC";
    288                 }
    289 
    290                 break;
    291 
    292             // 'popular' sorts by the 'total_friend_count' usermeta
    293             case 'popular' :
    294                 $this->uid_name = 'user_id';
    295                 $this->uid_table = $wpdb->usermeta;
    296                 $sql['select']  = "SELECT u.{$this->uid_name} as id FROM {$this->uid_table} u";
    297                 $sql['where'][] = $wpdb->prepare( "u.meta_key = %s", bp_get_user_meta_key( 'total_friend_count' ) );
    298                 $sql['orderby'] = "ORDER BY CONVERT(u.meta_value, SIGNED)";
    299                 $sql['order']   = "DESC";
    300 
    301                 break;
    302 
    303             // 'alphabetical' sorts depend on the xprofile setup
    304             case 'alphabetical' :
    305 
    306                 // We prefer to do alphabetical sorts against the display_name field
    307                 // of wp_users, because the table is smaller and better indexed. We
    308                 // can do so if xprofile sync is enabled, or if xprofile is inactive.
    309                 //
    310                 // @todo remove need for bp_is_active() check
    311                 if ( ! bp_disable_profile_sync() || ! bp_is_active( 'xprofile' ) ) {
    312                     $this->uid_name = 'ID';
    313                     $this->uid_table = $wpdb->users;
    314                     $sql['select']  = "SELECT u.{$this->uid_name} as id FROM {$this->uid_table} u";
    315                     $sql['orderby'] = "ORDER BY u.display_name";
    316                     $sql['order']   = "ASC";
    317 
    318                 // When profile sync is disabled, alphabetical sorts must happen against
    319                 // the xprofile table
    320                 } else {
    321                     $this->uid_name = 'user_id';
    322                     $this->uid_table = $bp->profile->table_name_data;
    323                     $sql['select']  = "SELECT u.{$this->uid_name} as id FROM {$this->uid_table} u";
    324                     $sql['where'][] = $wpdb->prepare( "u.field_id = %d", bp_xprofile_fullname_field_id() );
    325                     $sql['orderby'] = "ORDER BY u.value";
    326                     $sql['order']   = "ASC";
    327                 }
    328 
    329                 // Alphabetical queries ignore last_activity, while BP uses last_activity
    330                 // to infer spam/deleted/non-activated users. To ensure that these users
    331                 // are filtered out, we add an appropriate sub-query.
    332                 $sql['where'][] = "u.{$this->uid_name} IN ( SELECT ID FROM {$wpdb->users} WHERE " . bp_core_get_status_sql( '' ) . " )";
    333 
    334                 break;
    335 
    336             // Any other 'type' falls through
    337             default :
    338                 $this->uid_name = 'ID';
    339                 $this->uid_table = $wpdb->users;
    340                 $sql['select']  = "SELECT u.{$this->uid_name} as id FROM {$this->uid_table} u";
    341 
    342                 // In this case, we assume that a plugin is
    343                 // handling order, so we leave those clauses
    344                 // blank
    345 
    346                 break;
    347         }
    348 
    349         /** WHERE *************************************************************/
    350 
    351         // 'include' - User ids to include in the results
    352         $include     = false !== $include ? wp_parse_id_list( $include ) : array();
    353         $include_ids = $this->get_include_ids( $include );
    354         if ( ! empty( $include_ids ) ) {
    355             $include_ids    = implode( ',', wp_parse_id_list( $include_ids ) );
    356             $sql['where'][] = "u.{$this->uid_name} IN ({$include_ids})";
    357         }
    358 
    359         // 'exclude' - User ids to exclude from the results
    360         if ( false !== $exclude ) {
    361             $exclude_ids    = implode( ',', wp_parse_id_list( $exclude ) );
    362             $sql['where'][] = "u.{$this->uid_name} NOT IN ({$exclude_ids})";
    363         }
    364 
    365         // 'user_id' - When a user id is passed, limit to the friends of the user
    366         // @todo remove need for bp_is_active() check
    367         if ( ! empty( $user_id ) && bp_is_active( 'friends' ) ) {
    368             $friend_ids = friends_get_friend_user_ids( $user_id );
    369             $friend_ids = implode( ',', wp_parse_id_list( $friend_ids ) );
    370 
    371             if ( ! empty( $friend_ids ) ) {
    372                 $sql['where'][] = "u.{$this->uid_name} IN ({$friend_ids})";
    373 
    374             // If the user has no friends, the query should always
    375             // return no users
    376             } else {
    377                 $sql['where'][] = $this->no_results['where'];
    378             }
    379         }
    380 
    381         /** Search Terms ******************************************************/
    382 
    383         // 'search_terms' searches user_login and user_nicename
    384         // xprofile field matches happen in bp_xprofile_bp_user_query_search()
    385         if ( false !== $search_terms ) {
    386             $search_terms = bp_esc_like( wp_kses_normalize_entities( $search_terms ) );
    387 
    388             if ( $search_wildcard === 'left' ) {
    389                 $search_terms_nospace = '%' . $search_terms;
    390                 $search_terms_space   = '%' . $search_terms . ' %';
    391             } elseif ( $search_wildcard === 'right' ) {
    392                 $search_terms_nospace =        $search_terms . '%';
    393                 $search_terms_space   = '% ' . $search_terms . '%';
    394             } else {
    395                 $search_terms_nospace = '%' . $search_terms . '%';
    396                 $search_terms_space   = '%' . $search_terms . '%';
    397             }
    398 
    399             $sql['where']['search'] = $wpdb->prepare(
    400                 "u.{$this->uid_name} IN ( SELECT ID FROM {$wpdb->users} WHERE ( user_login LIKE %s OR user_login LIKE %s OR user_nicename LIKE %s OR user_nicename LIKE %s ) )",
    401                 $search_terms_nospace,
    402                 $search_terms_space,
    403                 $search_terms_nospace,
    404                 $search_terms_space
    405             );
    406         }
    407 
    408         // Member type.
    409         if ( ! empty( $member_type ) ) {
    410             $member_types = array();
    411 
    412             if ( ! is_array( $member_type ) ) {
    413                 $member_type = preg_split( '/[,\s+]/', $member_type );
    414             }
    415 
    416             foreach ( $member_type as $mt ) {
    417                 if ( ! bp_get_member_type_object( $mt ) ) {
    418                     continue;
    419                 }
    420 
    421                 $member_types[] = $mt;
    422             }
    423 
    424             if ( ! empty( $member_types ) ) {
    425                 $member_type_tq = new WP_Tax_Query( array(
    426                     array(
    427                         'taxonomy' => 'bp_member_type',
    428                         'field'    => 'name',
    429                         'operator' => 'IN',
    430                         'terms'    => $member_types,
    431                     ),
    432                 ) );
    433 
    434                 // Switch to the root blog, where member type taxonomies live.
    435                 switch_to_blog( bp_get_root_blog_id() );
    436 
    437                 $member_type_sql_clauses = $member_type_tq->get_sql( 'u', $this->uid_name );
    438                 restore_current_blog();
    439 
    440 
    441 
    442                 // Grab the first term_relationships clause and convert to a subquery.
    443                 if ( preg_match( '/' . $wpdb->term_relationships . '\.term_taxonomy_id IN \([0-9, ]+\)/', $member_type_sql_clauses['where'], $matches ) ) {
    444                     $sql['where']['member_type'] = "u.{$this->uid_name} IN ( SELECT object_id FROM $wpdb->term_relationships WHERE {$matches[0]} )";
    445                 }
    446             }
    447         }
    448 
    449         // 'meta_key', 'meta_value' allow usermeta search
    450         // To avoid global joins, do a separate query
    451         if ( false !== $meta_key ) {
    452             $meta_sql = $wpdb->prepare( "SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s", $meta_key );
    453 
    454             if ( false !== $meta_value ) {
    455                 $meta_sql .= $wpdb->prepare( " AND meta_value = %s", $meta_value );
    456             }
    457 
    458             $found_user_ids = $wpdb->get_col( $meta_sql );
    459 
    460             if ( ! empty( $found_user_ids ) ) {
    461                 $sql['where'][] = "u.{$this->uid_name} IN (" . implode( ',', wp_parse_id_list( $found_user_ids ) ) . ")";
    462             } else {
    463                 $sql['where'][] = '1 = 0';
    464             }
    465         }
    466 
    467         // 'per_page', 'page' - handles LIMIT
    468         if ( !empty( $per_page ) && !empty( $page ) ) {
    469             $sql['limit'] = $wpdb->prepare( "LIMIT %d, %d", intval( ( $page - 1 ) * $per_page ), intval( $per_page ) );
    470         } else {
    471             $sql['limit'] = '';
    472         }
    473 
    474         // Allow custom filters
    475         $sql = apply_filters_ref_array( 'bp_user_query_uid_clauses', array( $sql, &$this ) );
    476 
    477         // Assemble the query chunks
    478         $this->uid_clauses['select']  = $sql['select'];
    479         $this->uid_clauses['where']   = ! empty( $sql['where'] ) ? 'WHERE ' . implode( ' AND ', $sql['where'] ) : '';
    480         $this->uid_clauses['orderby'] = $sql['orderby'];
    481         $this->uid_clauses['order']   = $sql['order'];
    482         $this->uid_clauses['limit']   = $sql['limit'];
    483 
    484         do_action_ref_array( 'bp_pre_user_query', array( &$this ) );
    485     }
    486 
    487     /**
    488      * Query for IDs of users that match the query parameters.
    489      *
    490      * Perform a database query to specifically get only user IDs, using
    491      * existing query variables set previously in the constructor.
    492      *
    493      * Also used to quickly perform user total counts.
    494      *
    495      * @since BuddyPress (1.7.0)
    496      */
    497     public function do_user_ids_query() {
    498         global $wpdb;
    499 
    500         // If counting using SQL_CALC_FOUND_ROWS, set it up here
    501         if ( 'sql_calc_found_rows' == $this->query_vars['count_total'] ) {
    502             $this->uid_clauses['select'] = str_replace( 'SELECT', 'SELECT SQL_CALC_FOUND_ROWS', $this->uid_clauses['select'] );
    503         }
    504 
    505         // Get the specific user ids
    506         $this->user_ids = $wpdb->get_col( "{$this->uid_clauses['select']} {$this->uid_clauses['where']} {$this->uid_clauses['orderby']} {$this->uid_clauses['order']} {$this->uid_clauses['limit']}" );
    507 
    508         // Get the total user count
    509         if ( 'sql_calc_found_rows' == $this->query_vars['count_total'] ) {
    510             $this->total_users = $wpdb->get_var( apply_filters( 'bp_found_user_query', "SELECT FOUND_ROWS()", $this ) );
    511         } elseif ( 'count_query' == $this->query_vars['count_total'] ) {
    512             $count_select      = preg_replace( '/^SELECT.*?FROM (\S+) u/', "SELECT COUNT(u.{$this->uid_name}) FROM $1 u", $this->uid_clauses['select'] );
    513             $this->total_users = $wpdb->get_var( apply_filters( 'bp_found_user_query', "{$count_select} {$this->uid_clauses['where']}", $this ) );
    514         }
    515     }
    516 
    517     /**
    518      * Use WP_User_Query() to pull data for the user IDs retrieved in the main query.
    519      *
    520      * @since BuddyPress (1.7.0)
    521      */
    522     public function do_wp_user_query() {
    523         $fields = array( 'ID', 'user_login', 'user_pass', 'user_nicename', 'user_email', 'user_url', 'user_registered', 'user_activation_key', 'user_status', 'display_name' );
    524 
    525         if ( is_multisite() ) {
    526             $fields[] = 'spam';
    527             $fields[] = 'deleted';
    528         }
    529 
    530         $wp_user_query = new WP_User_Query( apply_filters( 'bp_wp_user_query_args', array(
    531 
    532             // Relevant
    533             'fields'      => $fields,
    534             'include'     => $this->user_ids,
    535 
    536             // Overrides
    537             'blog_id'     => 0,    // BP does not require blog roles
    538             'count_total' => false // We already have a count
    539 
    540         ), $this ) );
    541 
    542         // WP_User_Query doesn't cache the data it pulls from wp_users,
    543         // and it does not give us a way to save queries by fetching
    544         // only uncached users. However, BP does cache this data, so
    545         // we set it here.
    546         foreach ( $wp_user_query->results as $u ) {
    547             wp_cache_set( 'bp_core_userdata_' . $u->ID, $u, 'bp' );
    548         }
    549 
    550         // We calculate total_users using a standalone query, except
    551         // when a whitelist of user_ids is passed to the constructor.
    552         // This clause covers the latter situation, and ensures that
    553         // pagination works when querying by $user_ids.
    554         if ( empty( $this->total_users ) ) {
    555             $this->total_users = count( $wp_user_query->results );
    556         }
    557 
    558         // Reindex for easier matching
    559         $r = array();
    560         foreach ( $wp_user_query->results as $u ) {
    561             $r[ $u->ID ] = $u;
    562         }
    563 
    564         // Match up to the user ids from the main query
    565         foreach ( $this->user_ids as $uid ) {
    566             if ( isset( $r[ $uid ] ) ) {
    567                 $this->results[ $uid ] = $r[ $uid ];
    568 
    569                 // The BP template functions expect an 'id'
    570                 // (as opposed to 'ID') property
    571                 $this->results[ $uid ]->id = $uid;
    572             }
    573         }
    574     }
    575 
    576     /**
    577      * Fetch the IDs of users to put in the IN clause of the main query.
    578      *
    579      * By default, returns the value passed to it
    580      * ($this->query_vars['include']). Having this abstracted into a
    581      * standalone method means that extending classes can override the
    582      * logic, parsing together their own user_id limits with the 'include'
    583      * ids passed to the class constructor. See {@link BP_Group_Member_Query}
    584      * for an example.
    585      *
    586      * @since BuddyPress (1.8.0)
    587      *
    588      * @param array Sanitized array of user IDs, as passed to the 'include'
    589      *        parameter of the class constructor.
    590      * @return array The list of users to which the main query should be
    591      *         limited.
    592      */
    593     public function get_include_ids( $include = array() ) {
    594         return $include;
    595     }
    596 
    597     /**
    598      * Perform a database query to populate any extra metadata we might need.
    599      *
    600      * Different components will hook into the 'bp_user_query_populate_extras'
    601      * action to loop in the things they want.
    602      *
    603      * @since BuddyPress (1.7.0)
    604      *
    605      * @global WPDB $wpdb Global WordPress database access object.
    606      */
    607     public function populate_extras() {
    608         global $wpdb;
    609 
    610         // Bail if no users
    611         if ( empty( $this->user_ids ) || empty( $this->results ) ) {
    612             return;
    613         }
    614 
    615         // Bail if the populate_extras flag is set to false
    616         // In the case of the 'popular' sort type, we force
    617         // populate_extras to true, because we need the friend counts
    618         if ( 'popular' == $this->query_vars['type'] ) {
    619             $this->query_vars['populate_extras'] = 1;
    620         }
    621 
    622         if ( ! (bool) $this->query_vars['populate_extras'] ) {
    623             return;
    624         }
    625 
    626         // Turn user ID's into a query-usable, comma separated value
    627         $user_ids_sql = implode( ',', wp_parse_id_list( $this->user_ids ) );
    628 
    629         /**
    630          * Use this action to independently populate your own custom extras.
    631          *
    632          * Note that anything you add here should query using $user_ids_sql, to
    633          * avoid running multiple queries per user in the loop.
    634          *
    635          * Two BuddyPress components currently do this:
    636          * - XProfile: To override display names
    637          * - Friends:  To set whether or not a user is the current users friend
    638          *
    639          * @see bp_xprofile_filter_user_query_populate_extras()
    640          * @see bp_friends_filter_user_query_populate_extras()
    641          */
    642         do_action_ref_array( 'bp_user_query_populate_extras', array( $this, $user_ids_sql ) );
    643 
    644         // Fetch last_active data from the activity table
    645         $last_activities = BP_Core_User::get_last_activity( $this->user_ids );
    646 
    647         // Set a last_activity value for each user, even if it's empty
    648         foreach ( $this->results as $user_id => $user ) {
    649             $user_last_activity = isset( $last_activities[ $user_id ] ) ? $last_activities[ $user_id ]['date_recorded'] : '';
    650             $this->results[ $user_id ]->last_activity = $user_last_activity;
    651         }
    652 
    653         // Fetch usermeta data
    654         // We want the three following pieces of info from usermeta:
    655         // - friend count
    656         // - latest update
    657         $total_friend_count_key = bp_get_user_meta_key( 'total_friend_count' );
    658         $bp_latest_update_key   = bp_get_user_meta_key( 'bp_latest_update'   );
    659 
    660         // total_friend_count must be set for each user, even if its
    661         // value is 0
    662         foreach ( $this->results as $uindex => $user ) {
    663             $this->results[$uindex]->total_friend_count = 0;
    664         }
    665 
    666         // Create, prepare, and run the separate usermeta query
    667         $user_metas = $wpdb->get_results( $wpdb->prepare( "SELECT user_id, meta_key, meta_value FROM {$wpdb->usermeta} WHERE meta_key IN (%s,%s) AND user_id IN ({$user_ids_sql})", $total_friend_count_key, $bp_latest_update_key ) );
    668 
    669         // The $members_template global expects the index key to be different
    670         // from the meta_key in some cases, so we rejig things here.
    671         foreach ( $user_metas as $user_meta ) {
    672             switch ( $user_meta->meta_key ) {
    673                 case $total_friend_count_key :
    674                     $key = 'total_friend_count';
    675                     break;
    676 
    677                 case $bp_latest_update_key :
    678                     $key = 'latest_update';
    679                     break;
    680             }
    681 
    682             if ( isset( $this->results[ $user_meta->user_id ] ) ) {
    683                 $this->results[ $user_meta->user_id ]->{$key} = $user_meta->meta_value;
    684             }
    685         }
    686 
    687         // When meta_key or meta_value have been passed to the query,
    688         // fetch the resulting values for use in the template functions
    689         if ( ! empty( $this->query_vars['meta_key'] ) ) {
    690             $meta_sql = array(
    691                 'select' => "SELECT user_id, meta_key, meta_value",
    692                 'from'   => "FROM $wpdb->usermeta",
    693                 'where'  => $wpdb->prepare( "WHERE meta_key = %s", $this->query_vars['meta_key'] )
    694             );
    695 
    696             if ( false !== $this->query_vars['meta_value'] ) {
    697                 $meta_sql['where'] .= $wpdb->prepare( " AND meta_value = %s", $this->query_vars['meta_value'] );
    698             }
    699 
    700             $metas = $wpdb->get_results( "{$meta_sql['select']} {$meta_sql['from']} {$meta_sql['where']}" );
    701 
    702             if ( ! empty( $metas ) ) {
    703                 foreach ( $metas as $meta ) {
    704                     if ( isset( $this->results[ $meta->user_id ] ) ) {
    705                         $this->results[ $meta->user_id ]->meta_key = $meta->meta_key;
    706 
    707                         if ( ! empty( $meta->meta_value ) ) {
    708                             $this->results[ $meta->user_id ]->meta_value = $meta->meta_value;
    709                         }
    710                     }
    711                 }
    712             }
    713         }
    714     }
    715 }
    716 
    717 /**
    718  * Fetch data about a BuddyPress user.
    719  *
    720  * BP_Core_User class can be used by any component. It will fetch useful
    721  * details for any user when provided with a user_id.
    722  *
    723  * Example:
    724  *    $user = new BP_Core_User( $user_id );
    725  *    $user_avatar = $user->avatar;
    726  *    $user_email = $user->email;
    727  *    $user_status = $user->status;
    728  *    etc.
    729  */
    730 class BP_Core_User {
    731 
    732     /**
    733      * ID of the user which the object relates to.
    734      *
    735      * @var integer
    736      */
    737     public $id;
    738 
    739     /**
    740      * The URL to the full size of the avatar for the user.
    741      *
    742      * @var string
    743      */
    744     public $avatar;
    745 
    746     /**
    747      * The URL to the thumb size of the avatar for the user.
    748      *
    749      * @var string
    750      */
    751     public $avatar_thumb;
    752 
    753     /**
    754      * The URL to the mini size of the avatar for the user.
    755      *
    756      * @var string
    757      */
    758     public $avatar_mini;
    759 
    760     /**
    761      * The full name of the user
    762      *
    763      * @var string
    764      */
    765     public $fullname;
    766 
    767     /**
    768      * The email for the user.
    769      *
    770      * @var string
    771      */
    772     public $email;
    773 
    774     /**
    775      * The absolute url for the user's profile.
    776      *
    777      * @var string
    778      */
    779     public $user_url;
    780 
    781     /**
    782      * The HTML for the user link, with the link text being the user's full name.
    783      *
    784      * @var string
    785      */
    786     public $user_link;
    787 
    788     /**
    789      * Contains a formatted string when the last time the user was active.
    790      *
    791      * Example: "active 2 hours and 50 minutes ago"
    792      *
    793      * @var string
    794      */
    795     public $last_active;
    796 
    797     /* Extras */
    798 
    799     /**
    800      * The total number of "Friends" the user has on site.
    801      *
    802      * @var integer
    803      */
    804     public $total_friends;
    805 
    806     /**
    807      * The total number of blog posts posted by the user
    808      *
    809      * @var integer
    810      * @deprecated No longer used
    811      */
    812     public $total_blogs;
    813 
    814     /**
    815      * The total number of groups the user is a part of.
    816      *
    817      * Example: "1 group", "2 groups"
    818      *
    819      * @var string
    820      */
    821     public $total_groups;
    822 
    823     /**
    824      * Profile information for the specific user.
    825      *
    826      * @since BuddyPress (1.2.0)
    827      * @var array
    828      */
    829     public $profile_data;
    830 
    831     /** Public Methods *******************************************************/
    832 
    833     /**
    834      * Class constructor.
    835      *
    836      * @param integer $user_id The ID for the user being queried.
    837      * @param bool $populate_extras Whether to fetch extra information
    838      *        such as group/friendship counts or not. Default: false.
    839      */
    840     public function __construct( $user_id, $populate_extras = false ) {
    841         if ( !empty( $user_id ) ) {
    842             $this->id = $user_id;
    843             $this->populate();
    844 
    845             if ( !empty( $populate_extras ) ) {
    846                 $this->populate_extras();
    847             }
    848         }
    849     }
    850 
    851     /**
    852      * Populate the instantiated class with data based on the User ID provided.
    853      *
    854      * @uses bp_core_get_userurl() Returns the URL with no HTML markup for
    855      *       a user based on their user id.
    856      * @uses bp_core_get_userlink() Returns a HTML formatted link for a
    857      *       user with the user's full name as the link text.
    858      * @uses bp_core_get_user_email() Returns the email address for the
    859      *       user based on user ID.
    860      * @uses bp_get_user_meta() BP function returns the value of passed
    861      *       usermeta name from usermeta table.
    862      * @uses bp_core_fetch_avatar() Returns HTML formatted avatar for a user
    863      * @uses bp_profile_last_updated_date() Returns the last updated date
    864      *       for a user.
    865      */
    866     public function populate() {
    867 
    868         if ( bp_is_active( 'xprofile' ) )
    869             $this->profile_data = $this->get_profile_data();
    870 
    871         if ( !empty( $this->profile_data ) ) {
    872             $full_name_field_name = bp_xprofile_fullname_field_name();
    873 
    874             $this->user_url  = bp_core_get_user_domain( $this->id, $this->profile_data['user_nicename'], $this->profile_data['user_login'] );
    875             $this->fullname  = esc_attr( $this->profile_data[$full_name_field_name]['field_data'] );
    876             $this->user_link = "<a href='{$this->user_url}' title='{$this->fullname}'>{$this->fullname}</a>";
    877             $this->email     = esc_attr( $this->profile_data['user_email'] );
    878         } else {
    879             $this->user_url  = bp_core_get_user_domain( $this->id );
    880             $this->user_link = bp_core_get_userlink( $this->id );
    881             $this->fullname  = esc_attr( bp_core_get_user_displayname( $this->id ) );
    882             $this->email     = esc_attr( bp_core_get_user_email( $this->id ) );
    883         }
    884 
    885         // Cache a few things that are fetched often
    886         wp_cache_set( 'bp_user_fullname_' . $this->id, $this->fullname, 'bp' );
    887         wp_cache_set( 'bp_user_email_' . $this->id, $this->email, 'bp' );
    888         wp_cache_set( 'bp_user_url_' . $this->id, $this->user_url, 'bp' );
    889 
    890         $this->avatar       = bp_core_fetch_avatar( array( 'item_id' => $this->id, 'type' => 'full', 'alt' => sprintf( __( 'Profile photo of %s', 'buddypress' ), $this->fullname ) ) );
    891         $this->avatar_thumb = bp_core_fetch_avatar( array( 'item_id' => $this->id, 'type' => 'thumb', 'alt' => sprintf( __( 'Profile photo of %s', 'buddypress' ), $this->fullname ) ) );
    892         $this->avatar_mini  = bp_core_fetch_avatar( array( 'item_id' => $this->id, 'type' => 'thumb', 'alt' => sprintf( __( 'Profile photo of %s', 'buddypress' ), $this->fullname ), 'width' => 30, 'height' => 30 ) );
    893         $this->last_active  = bp_core_get_last_activity( bp_get_user_last_activity( $this->id ), __( 'active %s', 'buddypress' ) );
    894     }
    895 
    896     /**
    897      * Populates extra fields such as group and friendship counts.
    898      */
    899     public function populate_extras() {
    900 
    901         if ( bp_is_active( 'friends' ) ) {
    902             $this->total_friends = BP_Friends_Friendship::total_friend_count( $this->id );
    903         }
    904 
    905         if ( bp_is_active( 'groups' ) ) {
    906             $this->total_groups = BP_Groups_Member::total_group_count( $this->id );
    907             $this->total_groups = sprintf( _n( '%d group', '%d groups', $this->total_groups, 'buddypress' ), $this->total_groups );
    908         }
    909     }
    910 
    911     /**
    912      * Fetch xprofile data for the current user.
    913      *
    914      * @see BP_XProfile_ProfileData::get_all_for_user() for description of
    915      *      return value.
    916      *
    917      * @return array See {@link BP_XProfile_Profile_Data::get_all_for_user()}.
    918      */
    919     public function get_profile_data() {
    920         return BP_XProfile_ProfileData::get_all_for_user( $this->id );
    921     }
    922 
    923     /** Static Methods ********************************************************/
    924 
    925     /**
    926      * Get a list of users that match the query parameters.
    927      *
    928      * Since BuddyPress 1.7, use {@link BP_User_Query} instead.
    929      *
    930      * @deprecated 1.7.0 Use {@link BP_User_Query}.
    931      *
    932      * @see BP_User_Query for a description of parameters, most of which
    933      *      are used there in the same way.
    934      *
    935      * @param string $type See {@link BP_User_Query}.
    936      * @param int $limit See {@link BP_User_Query}. Default: 0.
    937      * @param int $page See {@link BP_User_Query}. Default: 1.
    938      * @param int $user_id See {@link BP_User_Query}. Default: 0.
    939      * @param mixed $include See {@link BP_User_Query}. Default: false.
    940      * @param string|bool $search_terms See {@link BP_User_Query}.
    941      *        Default: false.
    942      * @param bool $populate_extras See {@link BP_User_Query}.
    943      *        Default: true.
    944      * @param mixed $exclude See {@link BP_User_Query}. Default: false.
    945      * @param string|bool $meta_key See {@link BP_User_Query}.
    946      *        Default: false.
    947      * @param string|bool $meta_value See {@link BP_User_Query}.
    948      *        Default: false.
    949      * @return array {
    950      *     @type int $total_users Total number of users matched by query
    951      *           params.
    952      *     @type array $paged_users The current page of users matched by
    953      *           query params.
    954      * }
    955      */
    956     public static function get_users( $type, $limit = 0, $page = 1, $user_id = 0, $include = false, $search_terms = false, $populate_extras = true, $exclude = false, $meta_key = false, $meta_value = false ) {
    957         global $wpdb;
    958 
    959         _deprecated_function( __METHOD__, '1.7', 'BP_User_Query' );
    960 
    961         $bp = buddypress();
    962 
    963         $sql = array();
    964 
    965         $sql['select_main'] = "SELECT DISTINCT u.ID as id, u.user_registered, u.user_nicename, u.user_login, u.display_name, u.user_email";
    966 
    967         if ( 'active' == $type || 'online' == $type || 'newest' == $type  ) {
    968             $sql['select_active'] = ", um.meta_value as last_activity";
    969         }
    970 
    971         if ( 'popular' == $type ) {
    972             $sql['select_popular'] = ", um.meta_value as total_friend_count";
    973         }
    974 
    975         if ( 'alphabetical' == $type ) {
    976             $sql['select_alpha'] = ", pd.value as fullname";
    977         }
    978 
    979         if ( $meta_key ) {
    980             $sql['select_meta'] = ", umm.meta_key";
    981 
    982             if ( $meta_value ) {
    983                 $sql['select_meta'] .= ", umm.meta_value";
    984             }
    985         }
    986 
    987         $sql['from'] = "FROM {$wpdb->users} u LEFT JOIN {$wpdb->usermeta} um ON um.user_id = u.ID";
    988 
    989         // We search against xprofile fields, so we must join the table
    990         if ( $search_terms && bp_is_active( 'xprofile' ) ) {
    991             $sql['join_profiledata_search'] = "LEFT JOIN {$bp->profile->table_name_data} spd ON u.ID = spd.user_id";
    992         }
    993 
    994         // Alphabetical sorting is done by the xprofile Full Name field
    995         if ( 'alphabetical' == $type ) {
    996             $sql['join_profiledata_alpha'] = "LEFT JOIN {$bp->profile->table_name_data} pd ON u.ID = pd.user_id";
    997         }
    998 
    999         if ( $meta_key ) {
    1000             $sql['join_meta'] = "LEFT JOIN {$wpdb->usermeta} umm ON umm.user_id = u.ID";
    1001         }
    1002 
    1003         $sql['where'] = 'WHERE ' . bp_core_get_status_sql( 'u.' );
    1004 
    1005         if ( 'active' == $type || 'online' == $type || 'newest' == $type ) {
    1006             $sql['where_active'] = $wpdb->prepare( "AND um.meta_key = %s", bp_get_user_meta_key( 'last_activity' ) );
    1007         }
    1008 
    1009         if ( 'popular' == $type ) {
    1010             $sql['where_popular'] = $wpdb->prepare( "AND um.meta_key = %s", bp_get_user_meta_key( 'total_friend_count' ) );
    1011         }
    1012 
    1013         if ( 'online' == $type ) {
    1014             $sql['where_online'] = "AND DATE_ADD( um.meta_value, INTERVAL 5 MINUTE ) >= UTC_TIMESTAMP()";
    1015         }
    1016 
    1017         if ( 'alphabetical' == $type ) {
    1018             $sql['where_alpha'] = "AND pd.field_id = 1";
    1019         }
    1020 
    1021         if ( !empty( $exclude ) ) {
    1022             $exclude              = implode( ',', wp_parse_id_list( $exclude ) );
    1023             $sql['where_exclude'] = "AND u.ID NOT IN ({$exclude})";
    1024         }
    1025 
    1026         // Passing an $include value of 0 or '0' will necessarily result in an empty set
    1027         // returned. The default value of false will hit the 'else' clause.
    1028         if ( 0 === $include || '0' === $include ) {
    1029             $sql['where_users'] = "AND 0 = 1";
    1030         } else {
    1031             if ( !empty( $include ) ) {
    1032                 $include = implode( ',',  wp_parse_id_list( $include ) );
    1033                 $sql['where_users'] = "AND u.ID IN ({$include})";
    1034             } elseif ( !empty( $user_id ) && bp_is_active( 'friends' ) ) {
    1035                 $friend_ids = friends_get_friend_user_ids( $user_id );
    1036 
    1037                 if ( !empty( $friend_ids ) ) {
    1038                     $friend_ids = implode( ',', wp_parse_id_list( $friend_ids ) );
    1039                     $sql['where_friends'] = "AND u.ID IN ({$friend_ids})";
    1040 
    1041                 // User has no friends, return false since there will be no users to fetch.
    1042                 } else {
    1043                     return false;
    1044                 }
    1045             }
    1046         }
    1047 
    1048         if ( !empty( $search_terms ) && bp_is_active( 'xprofile' ) ) {
    1049             $search_terms_like        = '%' . bp_esc_like( $search_terms ) . '%';
    1050             $sql['where_searchterms'] = $wpdb->prepare( "AND spd.value LIKE %s", $search_terms_like );
    1051         }
    1052 
    1053         if ( !empty( $meta_key ) ) {
    1054             $sql['where_meta'] = $wpdb->prepare( " AND umm.meta_key = %s", $meta_key );
    1055 
    1056             // If a meta value is provided, match it
    1057             if ( $meta_value ) {
    1058                 $sql['where_meta'] .= $wpdb->prepare( " AND umm.meta_value = %s", $meta_value );
    1059             }
    1060         }
    1061 
    1062         switch ( $type ) {
    1063             case 'active': case 'online': default:
    1064                 $sql[] = "ORDER BY um.meta_value DESC";
    1065                 break;
    1066             case 'newest':
    1067                 $sql[] = "ORDER BY u.ID DESC";
    1068                 break;
    1069             case 'alphabetical':
    1070                 $sql[] = "ORDER BY pd.value ASC";
    1071                 break;
    1072             case 'random':
    1073                 $sql[] = "ORDER BY rand()";
    1074                 break;
    1075             case 'popular':
    1076                 $sql[] = "ORDER BY CONVERT(um.meta_value, SIGNED) DESC";
    1077                 break;
    1078         }
    1079 
    1080         if ( !empty( $limit ) && !empty( $page ) ) {
    1081             $sql['pagination'] = $wpdb->prepare( "LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
    1082         }
    1083 
    1084         // Get paginated results
    1085         $paged_users_sql = apply_filters( 'bp_core_get_paged_users_sql', join( ' ', (array) $sql ), $sql );
    1086         $paged_users     = $wpdb->get_results( $paged_users_sql );
    1087 
    1088         // Re-jig the SQL so we can get the total user count
    1089         unset( $sql['select_main'] );
    1090 
    1091         if ( !empty( $sql['select_active'] ) ) {
    1092             unset( $sql['select_active'] );
    1093         }
    1094 
    1095         if ( !empty( $sql['select_popular'] ) ) {
    1096             unset( $sql['select_popular'] );
    1097         }
    1098 
    1099         if ( !empty( $sql['select_alpha'] ) ) {
    1100             unset( $sql['select_alpha'] );
    1101         }
    1102 
    1103         if ( !empty( $sql['pagination'] ) ) {
    1104             unset( $sql['pagination'] );
    1105         }
    1106 
    1107         array_unshift( $sql, "SELECT COUNT(u.ID)" );
    1108 
    1109         // Get total user results
    1110         $total_users_sql = apply_filters( 'bp_core_get_total_users_sql', join( ' ', (array) $sql ), $sql );
    1111         $total_users     = $wpdb->get_var( $total_users_sql );
    1112 
    1113         /***
    1114          * Lets fetch some other useful data in a separate queries, this will be faster than querying the data for every user in a list.
    1115          * We can't add these to the main query above since only users who have this information will be returned (since the much of the data is in usermeta and won't support any type of directional join)
    1116          */
    1117         if ( !empty( $populate_extras ) ) {
    1118             $user_ids = array();
    1119 
    1120             foreach ( (array) $paged_users as $user ) {
    1121                 $user_ids[] = $user->id;
    1122             }
    1123 
    1124             // Add additional data to the returned results
    1125             $paged_users = BP_Core_User::get_user_extras( $paged_users, $user_ids, $type );
    1126         }
    1127 
    1128         return array( 'users' => $paged_users, 'total' => $total_users );
    1129     }
    1130 
    1131 
    1132     /**
    1133      * Fetch the details for all users whose usernames start with the given letter.
    1134      *
    1135      * @global wpdb $wpdb WordPress database object.
    1136      *
    1137      * @param string $letter The letter the users names are to start with.
    1138      * @param int $limit The number of users we wish to retrive.
    1139      * @param int $page The page number we are currently on, used in
    1140      *        conjunction with $limit to get the start position for the
    1141      *        limit.
    1142      * @param bool $populate_extras Populate extra user fields?
    1143      * @param string $exclude Comma-separated IDs of users whose results
    1144      *        aren't to be fetched.
    1145      * @return mixed False on error, otherwise associative array of results.
    1146      */
    1147     public static function get_users_by_letter( $letter, $limit = null, $page = 1, $populate_extras = true, $exclude = '' ) {
    1148         global $wpdb;
    1149 
    1150         $pag_sql = '';
    1151         if ( $limit && $page ) {
    1152             $pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
    1153         }
    1154 
    1155         // Multibyte compliance
    1156         if ( function_exists( 'mb_strlen' ) ) {
    1157             if ( mb_strlen( $letter, 'UTF-8' ) > 1 || is_numeric( $letter ) || !$letter ) {
    1158                 return false;
    1159             }
    1160         } else {
    1161             if ( strlen( $letter ) > 1 || is_numeric( $letter ) || !$letter ) {
    1162                 return false;
    1163             }
    1164         }
    1165 
    1166         $bp = buddypress();
    1167 
    1168         $letter_like = bp_esc_like( $letter ) . '%';
    1169         $status_sql  = bp_core_get_status_sql( 'u.' );
    1170 
    1171         if ( !empty( $exclude ) ) {
    1172             $exclude     = implode( ',', wp_parse_id_list( $exclude ) );
    1173             $exclude_sql = " AND u.id NOT IN ({$exclude})";
    1174         } else {
    1175             $exclude_sql = '';
    1176         }
    1177 
    1178         $total_users_sql = apply_filters( 'bp_core_users_by_letter_count_sql', $wpdb->prepare( "SELECT COUNT(DISTINCT u.ID) FROM {$wpdb->users} u LEFT JOIN {$bp->profile->table_name_data} pd ON u.ID = pd.user_id LEFT JOIN {$bp->profile->table_name_fields} pf ON pd.field_id = pf.id WHERE {$status_sql} AND pf.name = %s {$exclude_sql} AND pd.value LIKE %s ORDER BY pd.value ASC", bp_xprofile_fullname_field_name(), $letter_like ) );
    1179         $paged_users_sql = apply_filters( 'bp_core_users_by_letter_sql',       $wpdb->prepare( "SELECT DISTINCT u.ID as id, u.user_registered, u.user_nicename, u.user_login, u.user_email FROM {$wpdb->users} u LEFT JOIN {$bp->profile->table_name_data} pd ON u.ID = pd.user_id LEFT JOIN {$bp->profile->table_name_fields} pf ON pd.field_id = pf.id WHERE {$status_sql} AND pf.name = %s {$exclude_sql} AND pd.value LIKE %s ORDER BY pd.value ASC{$pag_sql}", bp_xprofile_fullname_field_name(), $letter_like ) );
    1180 
    1181         $total_users = $wpdb->get_var( $total_users_sql );
    1182         $paged_users = $wpdb->get_results( $paged_users_sql );
    1183 
    1184         /***
    1185          * Lets fetch some other useful data in a separate queries, this will be
    1186          * faster than querying the data for every user in a list. We can't add
    1187          * these to the main query above since only users who have this
    1188          * information will be returned (since the much of the data is in
    1189          * usermeta and won't support any type of directional join)
    1190          */
    1191         $user_ids = array();
    1192         foreach ( (array) $paged_users as $user )
    1193             $user_ids[] = (int) $user->id;
    1194 
    1195         // Add additional data to the returned results
    1196         if ( $populate_extras ) {
    1197             $paged_users = BP_Core_User::get_user_extras( $paged_users, $user_ids );
    1198         }
    1199 
    1200         return array( 'users' => $paged_users, 'total' => $total_users );
    1201     }
    1202 
    1203     /**
    1204      * Get details of specific users from the database.
    1205      *
    1206      * Use {@link BP_User_Query} with the 'user_ids' param instead.
    1207      *
    1208      * @global wpdb $wpdb WordPress database object.
    1209      * @param array $user_ids The user IDs of the users who we wish to
    1210      *        fetch information on.
    1211      * @param int $limit The limit of results we want.
    1212      * @param int $page The page we are on for pagination.
    1213      * @param bool $populate_extras Populate extra user fields?
    1214      * @return array Associative array.
    1215      */
    1216     public static function get_specific_users( $user_ids, $limit = null, $page = 1, $populate_extras = true ) {
    1217         global $wpdb;
    1218 
    1219         $pag_sql = '';
    1220         if ( $limit && $page )
    1221             $pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
    1222 
    1223         $user_ids   = implode( ',', wp_parse_id_list( $user_ids ) );
    1224         $status_sql = bp_core_get_status_sql();
    1225 
    1226         $total_users_sql = apply_filters( 'bp_core_get_specific_users_count_sql', "SELECT COUNT(ID) FROM {$wpdb->users} WHERE {$status_sql} AND ID IN ({$user_ids})" );
    1227         $paged_users_sql = apply_filters( 'bp_core_get_specific_users_count_sql', "SELECT ID as id, user_registered, user_nicename, user_login, user_email FROM {$wpdb->users} WHERE {$status_sql} AND ID IN ({$user_ids}) {$pag_sql}" );
    1228 
    1229         $total_users = $wpdb->get_var( $total_users_sql );
    1230         $paged_users = $wpdb->get_results( $paged_users_sql );
    1231 
    1232         /***
    1233          * Lets fetch some other useful data in a separate queries, this will be
    1234          * faster than querying the data for every user in a list. We can't add
    1235          * these to the main query above since only users who have this
    1236          * information will be returned (since the much of the data is in
    1237          * usermeta and won't support any type of directional join)
    1238          */
    1239 
    1240         // Add additional data to the returned results
    1241         if ( !empty( $populate_extras ) ) {
    1242             $paged_users = BP_Core_User::get_user_extras( $paged_users, $user_ids );
    1243         }
    1244 
    1245         return array( 'users' => $paged_users, 'total' => $total_users );
    1246     }
    1247 
    1248     /**
    1249      * Find users who match on the value of an xprofile data.
    1250      *
    1251      * @global wpdb $wpdb WordPress database object.
    1252      *
    1253      * @param string $search_terms The terms to search the profile table
    1254      *        value column for.
    1255      * @param integer $limit The limit of results we want.
    1256      * @param integer $page The page we are on for pagination.
    1257      * @param boolean $populate_extras Populate extra user fields?
    1258      * @return array Associative array.
    1259      */
    1260     public static function search_users( $search_terms, $limit = null, $page = 1, $populate_extras = true ) {
    1261         global $wpdb;
    1262 
    1263         $bp = buddypress();
    1264 
    1265         $user_ids = array();
    1266         $pag_sql  = $limit && $page ? $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * intval( $limit ) ), intval( $limit ) ) : '';
    1267 
    1268         $search_terms_like = '%' . bp_esc_like( $search_terms ) . '%';
    1269         $status_sql        = bp_core_get_status_sql( 'u.' );
    1270 
    1271         $total_users_sql = apply_filters( 'bp_core_search_users_count_sql', $wpdb->prepare( "SELECT COUNT(DISTINCT u.ID) as id FROM {$wpdb->users} u LEFT JOIN {$bp->profile->table_name_data} pd ON u.ID = pd.user_id WHERE {$status_sql} AND pd.value LIKE %s ORDER BY pd.value ASC", $search_terms_like ), $search_terms );
    1272         $paged_users_sql = apply_filters( 'bp_core_search_users_sql',       $wpdb->prepare( "SELECT DISTINCT u.ID as id, u.user_registered, u.user_nicename, u.user_login, u.user_email FROM {$wpdb->users} u LEFT JOIN {$bp->profile->table_name_data} pd ON u.ID = pd.user_id WHERE {$status_sql} AND pd.value LIKE %s ORDER BY pd.value ASC{$pag_sql}", $search_terms_like ), $search_terms, $pag_sql );
    1273 
    1274         $total_users = $wpdb->get_var( $total_users_sql );
    1275         $paged_users = $wpdb->get_results( $paged_users_sql );
    1276 
    1277         /***
    1278          * Lets fetch some other useful data in a separate queries, this will be faster than querying the data for every user in a list.
    1279          * We can't add these to the main query above since only users who have this information will be returned (since the much of the data is in usermeta and won't support any type of directional join)
    1280          */
    1281         foreach ( (array) $paged_users as $user )
    1282             $user_ids[] = $user->id;
    1283 
    1284         // Add additional data to the returned results
    1285         if ( $populate_extras )
    1286             $paged_users = BP_Core_User::get_user_extras( $paged_users, $user_ids );
    1287 
    1288         return array( 'users' => $paged_users, 'total' => $total_users );
    1289     }
    1290 
    1291     /**
    1292      * Fetch extra user information, such as friend count and last profile update message.
    1293      *
    1294      * Accepts multiple user IDs to fetch data for.
    1295      *
    1296      * @global wpdb $wpdb WordPress database object.
    1297      *
    1298      * @param array $paged_users An array of stdClass containing the users.
    1299      * @param string $user_ids The user ids to select information about.
    1300      * @param string $type The type of fields we wish to get.
    1301      * @return mixed False on error, otherwise associative array of results.
    1302      */
    1303     public static function get_user_extras( &$paged_users, &$user_ids, $type = false ) {
    1304         global $wpdb;
    1305 
    1306         $bp = buddypress();
    1307 
    1308         if ( empty( $user_ids ) )
    1309             return $paged_users;
    1310 
    1311         // Sanitize user IDs
    1312         $user_ids = implode( ',', wp_parse_id_list( $user_ids ) );
    1313 
    1314         // Fetch the user's full name
    1315         if ( bp_is_active( 'xprofile' ) && 'alphabetical' != $type ) {
    1316             $names = $wpdb->get_results( $wpdb->prepare( "SELECT pd.user_id as id, pd.value as fullname FROM {$bp->profile->table_name_fields} pf, {$bp->profile->table_name_data} pd WHERE pf.id = pd.field_id AND pf.name = %s AND pd.user_id IN ( {$user_ids} )", bp_xprofile_fullname_field_name() ) );
    1317             for ( $i = 0, $count = count( $paged_users ); $i < $count; ++$i ) {
    1318                 foreach ( (array) $names as $name ) {
    1319                     if ( $name->id == $paged_users[$i]->id )
    1320                         $paged_users[$i]->fullname = $name->fullname;
    1321                 }
    1322             }
    1323         }
    1324 
    1325         // Fetch the user's total friend count
    1326         if ( 'popular' != $type ) {
    1327             $friend_count = $wpdb->get_results( $wpdb->prepare( "SELECT user_id as id, meta_value as total_friend_count FROM {$wpdb->usermeta} WHERE meta_key = %s AND user_id IN ( {$user_ids} )", bp_get_user_meta_key( 'total_friend_count' ) ) );
    1328             for ( $i = 0, $count = count( $paged_users ); $i < $count; ++$i ) {
    1329                 foreach ( (array) $friend_count as $fcount ) {
    1330                     if ( $fcount->id == $paged_users[$i]->id )
    1331                         $paged_users[$i]->total_friend_count = (int) $fcount->total_friend_count;
    1332                 }
    1333             }
    1334         }
    1335 
    1336         // Fetch whether or not the user is a friend
    1337         if ( bp_is_active( 'friends' ) ) {
    1338             $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() ) );
    1339             for ( $i = 0, $count = count( $paged_users ); $i < $count; ++$i ) {
    1340                 foreach ( (array) $friend_status as $status ) {
    1341                     if ( $status->initiator_user_id == $paged_users[$i]->id || $status->friend_user_id == $paged_users[$i]->id )
    1342                         $paged_users[$i]->is_friend = $status->is_confirmed;
    1343                 }
    1344             }
    1345         }
    1346 
    1347         if ( 'active' != $type ) {
    1348             $user_activity = $wpdb->get_results( $wpdb->prepare( "SELECT user_id as id, meta_value as last_activity FROM {$wpdb->usermeta} WHERE meta_key = %s AND user_id IN ( {$user_ids} )", bp_get_user_meta_key( 'last_activity' ) ) );
    1349             for ( $i = 0, $count = count( $paged_users ); $i < $count; ++$i ) {
    1350                 foreach ( (array) $user_activity as $activity ) {
    1351                     if ( $activity->id == $paged_users[$i]->id )
    1352                         $paged_users[$i]->last_activity = $activity->last_activity;
    1353                 }
    1354             }
    1355         }
    1356 
    1357         // Fetch the user's last_activity
    1358         if ( 'active' != $type ) {
    1359             $user_activity = $wpdb->get_results( $wpdb->prepare( "SELECT user_id as id, meta_value as last_activity FROM {$wpdb->usermeta} WHERE meta_key = %s AND user_id IN ( {$user_ids} )", bp_get_user_meta_key( 'last_activity' ) ) );
    1360             for ( $i = 0, $count = count( $paged_users ); $i < $count; ++$i ) {
    1361                 foreach ( (array) $user_activity as $activity ) {
    1362                     if ( $activity->id == $paged_users[$i]->id )
    1363                         $paged_users[$i]->last_activity = $activity->last_activity;
    1364                 }
    1365             }
    1366         }
    1367 
    1368         // Fetch the user's latest update
    1369         $user_update = $wpdb->get_results( $wpdb->prepare( "SELECT user_id as id, meta_value as latest_update FROM {$wpdb->usermeta} WHERE meta_key = %s AND user_id IN ( {$user_ids} )", bp_get_user_meta_key( 'bp_latest_update' ) ) );
    1370         for ( $i = 0, $count = count( $paged_users ); $i < $count; ++$i ) {
    1371             foreach ( (array) $user_update as $update ) {
    1372                 if ( $update->id == $paged_users[$i]->id )
    1373                     $paged_users[$i]->latest_update = $update->latest_update;
    1374             }
    1375         }
    1376 
    1377         return $paged_users;
    1378     }
    1379 
    1380     /**
    1381      * Get WordPress user details for a specified user.
    1382      *
    1383      * @global wpdb $wpdb WordPress database object.
    1384      *
    1385      * @param integer $user_id User ID.
    1386      * @return array Associative array.
    1387      */
    1388     public static function get_core_userdata( $user_id ) {
    1389         global $wpdb;
    1390 
    1391         if ( !$user = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->users} WHERE ID = %d LIMIT 1", $user_id ) ) )
    1392             return false;
    1393 
    1394         return $user;
    1395     }
    1396 
    1397     /**
    1398      * Get last activity data for a user or set of users.
    1399      *
    1400      * @param int|array User IDs or multiple user IDs.
    1401      * @return array
    1402      */
    1403     public static function get_last_activity( $user_id ) {
    1404         global $wpdb;
    1405 
    1406         // Sanitize and remove empty values
    1407         $user_ids = array_filter( wp_parse_id_list( $user_id ) );
    1408 
    1409         if ( empty( $user_ids ) ) {
    1410             return false;
    1411         }
    1412 
    1413         $uncached_user_ids = bp_get_non_cached_ids( $user_ids, 'bp_last_activity' );
    1414         if ( ! empty( $uncached_user_ids ) ) {
    1415             $bp = buddypress();
    1416 
    1417             $user_ids_sql = implode( ',', $uncached_user_ids );
    1418             $user_count   = count( $uncached_user_ids );
    1419 
    1420             $last_activities = $wpdb->get_results( $wpdb->prepare( "SELECT id, user_id, date_recorded FROM {$bp->members->table_name_last_activity} WHERE component = %s AND type = 'last_activity' AND user_id IN ({$user_ids_sql}) LIMIT {$user_count}", $bp->members->id ) );
    1421 
    1422             foreach ( $last_activities as $last_activity ) {
    1423                 wp_cache_set( $last_activity->user_id, array(
    1424                     'user_id'       => $last_activity->user_id,
    1425                     'date_recorded' => $last_activity->date_recorded,
    1426                     'activity_id'   => $last_activity->id,
    1427                 ), 'bp_last_activity' );
    1428             }
    1429         }
    1430 
    1431         // Fetch all user data from the cache
    1432         $retval = array();
    1433         foreach ( $user_ids as $user_id ) {
    1434             $retval[ $user_id ] = wp_cache_get( $user_id, 'bp_last_activity' );
    1435         }
    1436 
    1437         return $retval;
    1438     }
    1439 
    1440     /**
    1441      * Set a user's last_activity value.
    1442      *
    1443      * Will create a new entry if it does not exist. Otherwise updates the
    1444      * existing entry.
    1445      *
    1446      * @since 2.0
    1447      *
    1448      * @param int $user_id ID of the user whose last_activity you are updating.
    1449      * @param string $time MySQL-formatted time string.
    1450      * @return bool True on success, false on failure.
    1451      */
    1452     public static function update_last_activity( $user_id, $time ) {
    1453         global $wpdb;
    1454 
    1455         $table_name = buddypress()->members->table_name_last_activity;
    1456 
    1457         $activity = self::get_last_activity( $user_id );
    1458 
    1459         if ( ! empty( $activity[ $user_id ] ) ) {
    1460             $updated = $wpdb->update(
    1461                 $table_name,
    1462 
    1463                 // Data to update
    1464                 array(
    1465                     'date_recorded' => $time,
    1466                 ),
    1467 
    1468                 // WHERE
    1469                 array(
    1470                     'id' => $activity[ $user_id ]['activity_id'],
    1471                 ),
    1472 
    1473                 // Data sanitization format
    1474                 array(
    1475                     '%s',
    1476                 ),
    1477 
    1478                 // WHERE sanitization format
    1479                 array(
    1480                     '%d',
    1481                 )
    1482             );
    1483 
    1484             // add new date to existing activity entry for caching
    1485             $activity[ $user_id ]['date_recorded'] = $time;
    1486 
    1487         } else {
    1488             $updated = $wpdb->insert(
    1489                 $table_name,
    1490 
    1491                 // Data
    1492                 array(
    1493                     'user_id'       => $user_id,
    1494                     'component'     => buddypress()->members->id,
    1495                     'type'          => 'last_activity',
    1496                     'action'        => '',
    1497                     'content'       => '',
    1498                     'primary_link'  => '',
    1499                     'item_id'       => 0,
    1500                     'date_recorded' => $time,
    1501                 ),
    1502 
    1503                 // Data sanitization format
    1504                 array(
    1505                     '%d',
    1506                     '%s',
    1507                     '%s',
    1508                     '%s',
    1509                     '%s',
    1510                     '%s',
    1511                     '%d',
    1512                     '%s',
    1513                 )
    1514             );
    1515 
    1516             // set up activity array for caching
    1517             // view the foreach loop in the get_last_activity() method for format
    1518             $activity = array();
    1519             $activity[ $user_id ] = array(
    1520                 'user_id'       => $user_id,
    1521                 'date_recorded' => $time,
    1522                 'activity_id'   => $wpdb->insert_id,
    1523             );
    1524         }
    1525 
    1526         // set cache
    1527         wp_cache_set( $user_id, $activity[ $user_id ], 'bp_last_activity' );
    1528 
    1529         return $updated;
    1530     }
    1531 
    1532     /**
    1533      * Delete a user's last_activity value.
    1534      *
    1535      * @since 2.0
    1536      *
    1537      * @param int $user_id
    1538      * @return bool True on success, false on failure or if no last_activity
    1539      *         is found for the user.
    1540      */
    1541     public static function delete_last_activity( $user_id ) {
    1542         global $wpdb;
    1543 
    1544         $existing = self::get_last_activity( $user_id );
    1545 
    1546         if ( empty( $existing ) ) {
    1547             return false;
    1548         }
    1549 
    1550         $deleted = $wpdb->delete(
    1551             buddypress()->members->table_name_last_activity,
    1552 
    1553             // WHERE
    1554             array(
    1555                 'id' => $existing[ $user_id ]['activity_id'],
    1556             ),
    1557 
    1558             // WHERE sanitization format
    1559             array(
    1560                 '%s',
    1561             )
    1562         );
    1563 
    1564         wp_cache_delete( $user_id, 'bp_last_activity' );
    1565 
    1566         return $deleted;
    1567     }
    1568 }
    1569 
    1570 if ( class_exists( 'WP_Date_Query' ) ) :
    1571 /**
    1572  * BuddyPress date query class.
    1573  *
    1574  * Extends the {@link WP_Date_Query} class for use with BuddyPress.
    1575  *
    1576  * @since BuddyPress (2.1.0)
    1577  *
    1578  * @param array $date_query {
    1579  *     Date query arguments.  See first parameter of {@link WP_Date_Query::__construct()}.
    1580  * }
    1581  * @param string $column The DB column to query against.
    1582  */
    1583 class BP_Date_Query extends WP_Date_Query {
    1584     /**
    1585      * The column to query against. Can be changed via the query arguments.
    1586      *
    1587      * @var string
    1588      */
    1589     public $column;
    1590 
    1591     /**
    1592      * Constructor.
    1593      *
    1594      * @see WP_Date_Query::__construct()
    1595      */
    1596     public function __construct( $date_query, $column = '' ) {
    1597         if ( ! empty( $column ) ) {
    1598             $this->column = $column;
    1599             add_filter( 'date_query_valid_columns', array( $this, 'register_date_column' ) );
    1600         }
    1601 
    1602         parent::__construct( $date_query, $column );
    1603     }
    1604 
    1605     /**
    1606      * Destructor.
    1607      */
    1608     public function __destruct() {
    1609         remove_filter( 'date_query_valid_columns', array( $this, 'register_date_column' ) );
    1610     }
    1611 
    1612     /**
    1613      * Registers our date column with WP Date Query to pass validation.
    1614      *
    1615      * @param array $retval Current DB columns
    1616      * @return array
    1617      */
    1618     public function register_date_column( $retval = array() ) {
    1619         $retval[] = $this->column;
    1620         return $retval;
    1621     }
    1622 }
    1623 endif;
    1624 
    1625 /**
    1626  * BP_Core_Notification is deprecated.
    1627  *
    1628  * Use BP_Notifications_Notification instead.
    1629  *
    1630  * @package BuddyPress Core
    1631  * @deprecated since BuddyPress (1.9)
    1632  */
    1633 class BP_Core_Notification {
    1634 
    1635     /**
    1636      * The notification id
    1637      *
    1638      * @var integer
    1639      */
    1640     public $id;
    1641 
    1642     /**
    1643      * The ID to which the notification relates to within the component.
    1644      *
    1645      * @var integer
    1646      */
    1647     public $item_id;
    1648 
    1649     /**
    1650      * The secondary ID to which the notification relates to within the component.
    1651      *
    1652      * @var integer
    1653      */
    1654     public $secondary_item_id = null;
    1655 
    1656     /**
    1657      * The user ID for who the notification is for.
    1658      *
    1659      * @var integer
    1660      */
    1661     public $user_id;
    1662 
    1663     /**
    1664      * The name of the component that the notification is for.
    1665      *
    1666      * @var string
    1667      */
    1668     public $component_name;
    1669 
    1670     /**
    1671      * The action within the component which the notification is related to.
    1672      *
    1673      * @var string
    1674      */
    1675     public $component_action;
    1676 
    1677     /**
    1678      * The date the notification was created.
    1679      *
    1680      * @var string
    1681      */
    1682     public $date_notified;
    1683 
    1684     /**
    1685      * Is the notification new or has it already been read.
    1686      *
    1687      * @var boolean
    1688      */
    1689     public $is_new;
    1690 
    1691     /** Public Methods ********************************************************/
    1692 
    1693     /**
    1694      * Constructor
    1695      *
    1696      * @param integer $id
    1697      */
    1698     public function __construct( $id = 0 ) {
    1699         if ( !empty( $id ) ) {
    1700             $this->id = $id;
    1701             $this->populate();
    1702         }
    1703     }
    1704 
    1705     /**
    1706      * Update or insert notification details into the database.
    1707      *
    1708      * @global wpdb $wpdb WordPress database object
    1709      * @return bool Success or failure
    1710      */
    1711     public function save() {
    1712         global $wpdb;
    1713 
    1714         $bp = buddypress();
    1715 
    1716         // Update
    1717         if ( !empty( $this->id ) ) {
    1718             $sql = $wpdb->prepare( "UPDATE {$bp->core->table_name_notifications} SET item_id = %d, secondary_item_id = %d, user_id = %d, component_name = %s, component_action = %d, date_notified = %s, is_new = %d ) WHERE id = %d", $this->item_id, $this->secondary_item_id, $this->user_id, $this->component_name, $this->component_action, $this->date_notified, $this->is_new, $this->id );
    1719 
    1720         // Save
    1721         } else {
    1722             $sql = $wpdb->prepare( "INSERT INTO {$bp->core->table_name_notifications} ( item_id, secondary_item_id, user_id, component_name, component_action, date_notified, is_new ) VALUES ( %d, %d, %d, %s, %s, %s, %d )", $this->item_id, $this->secondary_item_id, $this->user_id, $this->component_name, $this->component_action, $this->date_notified, $this->is_new );
    1723         }
    1724 
    1725         if ( !$result = $wpdb->query( $sql ) )
    1726             return false;
    1727 
    1728         $this->id = $wpdb->insert_id;
    1729 
    1730         return true;
    1731     }
    1732 
    1733     /** Private Methods *******************************************************/
    1734 
    1735     /**
    1736      * Fetches the notification data from the database.
    1737      *
    1738      * @global wpdb $wpdb WordPress database object
    1739      */
    1740     public function populate() {
    1741         global $wpdb;
    1742 
    1743         $bp = buddypress();
    1744 
    1745         if ( $notification = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$bp->core->table_name_notifications} WHERE id = %d", $this->id ) ) ) {
    1746             $this->item_id = $notification->item_id;
    1747             $this->secondary_item_id = $notification->secondary_item_id;
    1748             $this->user_id           = $notification->user_id;
    1749             $this->component_name    = $notification->component_name;
    1750             $this->component_action  = $notification->component_action;
    1751             $this->date_notified     = $notification->date_notified;
    1752             $this->is_new            = $notification->is_new;
    1753         }
    1754     }
    1755 
    1756     /** Static Methods ********************************************************/
    1757 
    1758     public static function check_access( $user_id, $notification_id ) {
    1759         global $wpdb;
    1760 
    1761         $bp = buddypress();
    1762 
    1763         return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM {$bp->core->table_name_notifications} WHERE id = %d AND user_id = %d", $notification_id, $user_id ) );
    1764     }
    1765 
    1766     /**
    1767      * Fetches all the notifications in the database for a specific user.
    1768      *
    1769      * @global wpdb $wpdb WordPress database object
    1770      * @param integer $user_id User ID
    1771      * @param string $status 'is_new' or 'all'
    1772      * @return array Associative array
    1773      * @static
    1774      */
    1775     public static function get_all_for_user( $user_id, $status = 'is_new' ) {
    1776         global $wpdb;
    1777 
    1778         $bp = buddypress();
    1779 
    1780         $is_new = ( 'is_new' === $status )
    1781             ? ' AND is_new = 1 '
    1782             : '';
    1783 
    1784         return $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$bp->core->table_name_notifications} WHERE user_id = %d {$is_new}", $user_id ) );
    1785     }
    1786 
    1787     /**
    1788      * Delete all the notifications for a user based on the component name and action.
    1789      *
    1790      * @global wpdb $wpdb WordPress database object
    1791      * @param integer $user_id
    1792      * @param string $component_name
    1793      * @param string $component_action
    1794      * @static
    1795      */
    1796     public static function delete_for_user_by_type( $user_id, $component_name, $component_action ) {
    1797         global $wpdb;
    1798 
    1799         $bp = buddypress();
    1800 
    1801         return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->core->table_name_notifications} WHERE user_id = %d AND component_name = %s AND component_action = %s", $user_id, $component_name, $component_action ) );
    1802     }
    1803 
    1804     /**
    1805      * Delete all the notifications that have a specific item id, component name and action.
    1806      *
    1807      * @global wpdb $wpdb WordPress database object
    1808      * @param integer $user_id The ID of the user who the notifications are for.
    1809      * @param integer $item_id The item ID of the notifications we wish to delete.
    1810      * @param string $component_name The name of the component that the notifications we wish to delete.
    1811      * @param string $component_action The action of the component that the notifications we wish to delete.
    1812      * @param integer $secondary_item_id (optional) The secondary item id of the notifications that we wish to use to delete.
    1813      * @static
    1814      */
    1815     public static function delete_for_user_by_item_id( $user_id, $item_id, $component_name, $component_action, $secondary_item_id = false ) {
    1816         global $wpdb;
    1817 
    1818         $bp = buddypress();
    1819 
    1820         $secondary_item_sql = !empty( $secondary_item_id )
    1821             ? $wpdb->prepare( " AND secondary_item_id = %d", $secondary_item_id )
    1822             : '';
    1823 
    1824         return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->core->table_name_notifications} WHERE user_id = %d AND item_id = %d AND component_name = %s AND component_action = %s{$secondary_item_sql}", $user_id, $item_id, $component_name, $component_action ) );
    1825     }
    1826 
    1827     /**
    1828      * Deletes all the notifications sent by a specific user, by component and action.
    1829      *
    1830      * @global wpdb $wpdb WordPress database object
    1831      * @param integer $user_id The ID of the user whose sent notifications we wish to delete.
    1832      * @param string $component_name The name of the component the notification was sent from.
    1833      * @param string $component_action The action of the component the notification was sent from.
    1834      * @static
    1835      */
    1836     public static function delete_from_user_by_type( $user_id, $component_name, $component_action ) {
    1837         global $wpdb;
    1838 
    1839         $bp = buddypress();
    1840 
    1841         return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->core->table_name_notifications} WHERE item_id = %d AND component_name = %s AND component_action = %s", $user_id, $component_name, $component_action ) );
    1842     }
    1843 
    1844     /**
    1845      * Deletes all the notifications for all users by item id, and optional secondary item id, and component name and action.
    1846      *
    1847      * @global wpdb $wpdb WordPress database object
    1848      * @param string $item_id The item id that they notifications are to be for.
    1849      * @param string $component_name The component that the notifications are to be from.
    1850      * @param string $component_action The action that the notifications are to be from.
    1851      * @param string $secondary_item_id Optional secondary item id that the notifications are to have.
    1852      * @static
    1853      */
    1854     public static function delete_all_by_type( $item_id, $component_name, $component_action, $secondary_item_id ) {
    1855         global $wpdb;
    1856 
    1857         if ( $component_action )
    1858             $component_action_sql = $wpdb->prepare( "AND component_action = %s", $component_action );
    1859         else
    1860             $component_action_sql = '';
    1861 
    1862         if ( $secondary_item_id )
    1863             $secondary_item_sql = $wpdb->prepare( "AND secondary_item_id = %d", $secondary_item_id );
    1864         else
    1865             $secondary_item_sql = '';
    1866 
    1867         $bp = buddypress();
    1868 
    1869         return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->core->table_name_notifications} WHERE item_id = %d AND component_name = %s {$component_action_sql} {$secondary_item_sql}", $item_id, $component_name ) );
    1870     }
    1871 }
    1872 
    1873 /**
    1874  * API to create BuddyPress buttons.
    1875  *
    1876  * @since BuddyPress (1.2.6)
    1877  *
    1878  * @param array $args {
    1879  *     Array of arguments.
    1880  *     @type string $id String describing the button type.
    1881  *     @type string $component The name of the component the button belongs to.
    1882  *           Default: 'core'.
    1883  *     @type bool $must_be_logged_in Optional. Does the user need to be logged
    1884  *           in to see this button? Default: true.
    1885  *     @type bool $block_self Optional. True if the button should be hidden
    1886  *           when a user is viewing his own profile. Default: true.
    1887  *     @type string|bool $wrapper Optional. HTML element type that should wrap
    1888  *           the button: 'div', 'span', 'p', or 'li'. False for no wrapper at
    1889  *           all. Default: 'div'.
    1890  *     @type string $wrapper_id Optional. DOM ID of the button wrapper element.
    1891  *           Default: ''.
    1892  *     @type string $wrapper_class Optional. DOM class of the button wrapper
    1893  *           element. Default: ''.
    1894  *     @type string $link_href Optional. Destination link of the button.
    1895  *           Default: ''.
    1896  *     @type string $link_class Optional. DOM class of the button. Default: ''.
    1897  *     @type string $link_id Optional. DOM ID of the button. Default: ''.
    1898  *     @type string $link_rel Optional. DOM 'rel' attribute of the button.
    1899  *           Default: ''.
    1900  *     @type string $link_title Optional. Title attribute of the button.
    1901  *           Default: ''.
    1902  *     @type string $link_text Optional. Text to appear on the button.
    1903  *           Default: ''.
    1904  * }
    1905  */
    1906 class BP_Button {
    1907 
    1908     /** Button properties *****************************************************/
    1909 
    1910     /**
    1911      * The button ID.
    1912      *
    1913      * @var string
    1914      */
    1915     public $id = '';
    1916 
    1917     /**
    1918      * The name of the component that the button belongs to.
    1919      *
    1920      * @var string
    1921      */
    1922     public $component = 'core';
    1923 
    1924     /**
    1925      * Does the user need to be logged in to see this button?
    1926      *
    1927      * @var bool
    1928      */
    1929     public $must_be_logged_in = true;
    1930 
    1931     /**
    1932      * Whether the button should be hidden when viewing your own profile.
    1933      *
    1934      * @var bool
    1935      */
    1936     public $block_self = true;
    1937 
    1938     /** Wrapper ***************************************************************/
    1939 
    1940     /**
    1941      * The type of DOM element to use for a wrapper.
    1942      *
    1943      * @var string|bool 'div', 'span', 'p', 'li', or false for no wrapper.
    1944      */
    1945     public $wrapper = 'div';
    1946 
    1947     /**
    1948      * The DOM class of the button wrapper.
    1949      *
    1950      * @var string
    1951      */
    1952     public $wrapper_class = '';
    1953 
    1954     /**
    1955      * The DOM ID of the button wrapper.
    1956      *
    1957      * @var string
    1958      */
    1959     public $wrapper_id = '';
    1960 
    1961     /** Button ****************************************************************/
    1962 
    1963     /**
    1964      * The destination link of the button.
    1965      *
    1966      * @var string
    1967      */
    1968     public $link_href = '';
    1969 
    1970     /**
    1971      * The DOM class of the button link.
    1972      *
    1973      * @var string
    1974      */
    1975     public $link_class = '';
    1976 
    1977     /**
    1978      * The DOM ID of the button link.
    1979      *
    1980      * @var string
    1981      */
    1982     public $link_id = '';
    1983 
    1984     /**
    1985      * The DOM rel value of the button link.
    1986      *
    1987      * @var string
    1988      */
    1989     public $link_rel = '';
    1990 
    1991     /**
    1992      * Title of the button link.
    1993      *
    1994      * @var string
    1995      */
    1996     public $link_title = '';
    1997 
    1998     /**
    1999      * The contents of the button link.
    2000      *
    2001      * @var string
    2002      */
    2003     public $link_text = '';
    2004 
    2005     /** HTML result ***********************************************************/
    2006 
    2007     public $contents = '';
    2008 
    2009     /** Methods ***************************************************************/
    2010 
    2011     /**
    2012      * Builds the button based on class parameters.
    2013      *
    2014      * @since BuddyPress (1.2.6)
    2015      *
    2016      * @param array $args See {@BP_Button}.
    2017      * @return bool|null Returns false when the button is not allowed for
    2018      *         the current context.
    2019      */
    2020     public function __construct( $args = '' ) {
    2021 
    2022         $r = wp_parse_args( $args, get_class_vars( __CLASS__ ) );
    2023 
    2024         // Required button properties
    2025         $this->id                = $r['id'];
    2026         $this->component         = $r['component'];
    2027         $this->must_be_logged_in = (bool) $r['must_be_logged_in'];
    2028         $this->block_self        = (bool) $r['block_self'];
    2029         $this->wrapper           = $r['wrapper'];
    2030 
    2031         // $id and $component are required
    2032         if ( empty( $r['id'] ) || empty( $r['component'] ) )
    2033             return false;
    2034 
    2035         // No button if component is not active
    2036         if ( ! bp_is_active( $this->component ) )
    2037             return false;
    2038 
    2039         // No button for guests if must be logged in
    2040         if ( true == $this->must_be_logged_in && ! is_user_logged_in() )
    2041             return false;
    2042 
    2043         // block_self
    2044         if ( true == $this->block_self ) {
    2045             // No button if you are the current user in a members loop
    2046             // This condition takes precedence, because members loops
    2047             // can be found on user profiles
    2048             if ( bp_get_member_user_id() ) {
    2049                 if ( is_user_logged_in() && bp_loggedin_user_id() == bp_get_member_user_id() ) {
    2050                     return false;
    2051                 }
    2052 
    2053             // No button if viewing your own profile (and not in
    2054             // a members loop)
    2055             } elseif ( bp_is_my_profile() ) {
    2056                 return false;
    2057             }
    2058         }
    2059 
    2060         // Wrapper properties
    2061         if ( false !== $this->wrapper ) {
    2062 
    2063             // Wrapper ID
    2064             if ( !empty( $r['wrapper_id'] ) ) {
    2065                 $this->wrapper_id    = ' id="' . $r['wrapper_id'] . '"';
    2066             }
    2067 
    2068             // Wrapper class
    2069             if ( !empty( $r['wrapper_class'] ) ) {
    2070                 $this->wrapper_class = ' class="generic-button ' . $r['wrapper_class'] . '"';
    2071             } else {
    2072                 $this->wrapper_class = ' class="generic-button"';
    2073             }
    2074 
    2075             // Set before and after
    2076             $before = '<' . $r['wrapper'] . $this->wrapper_class . $this->wrapper_id . '>';
    2077             $after  = '</' . $r['wrapper'] . '>';
    2078 
    2079         // No wrapper
    2080         } else {
    2081             $before = $after = '';
    2082         }
    2083 
    2084         // Link properties
    2085         if ( !empty( $r['link_id']    ) ) $this->link_id    = ' id="' .    $r['link_id']    . '"';
    2086         if ( !empty( $r['link_href']  ) ) $this->link_href  = ' href="' .  $r['link_href']  . '"';
    2087         if ( !empty( $r['link_title'] ) ) $this->link_title = ' title="' . $r['link_title'] . '"';
    2088         if ( !empty( $r['link_rel']   ) ) $this->link_rel   = ' rel="' .   $r['link_rel']   . '"';
    2089         if ( !empty( $r['link_class'] ) ) $this->link_class = ' class="' . $r['link_class'] . '"';
    2090         if ( !empty( $r['link_text']  ) ) $this->link_text  =              $r['link_text'];
    2091 
    2092         // Build the button
    2093         $this->contents = $before . '<a'. $this->link_href . $this->link_title . $this->link_id . $this->link_rel . $this->link_class . '>' . $this->link_text . '</a>' . $after;
    2094 
    2095         // Allow button to be manipulated externally
    2096         $this->contents = apply_filters( 'bp_button_' . $this->component . '_' . $this->id, $this->contents, $this, $before, $after );
    2097     }
    2098 
    2099     /**
    2100      * Return the markup for the generated button.
    2101      *
    2102      * @since BuddyPress (1.2.6)
    2103      *
    2104      * @return string Button markup.
    2105      */
    2106     public function contents() {
    2107         return $this->contents;
    2108     }
    2109 
    2110     /**
    2111      * Output the markup of button.
    2112      *
    2113      * @since BuddyPress (1.2.6)
    2114      */
    2115     public function display() {
    2116         if ( !empty( $this->contents ) )
    2117             echo $this->contents;
    2118     }
    2119 }
    2120 
    2121 /**
    2122  * Enable oEmbeds in BuddyPress contexts.
    2123  *
    2124  * Extends WP_Embed class for use with BuddyPress.
    2125  *
    2126  * @since BuddyPress (1.5.0)
    2127  *
    2128  * @see WP_Embed
    2129  */
    2130 class BP_Embed extends WP_Embed {
    2131 
    2132     /**
    2133      * Constructor
    2134      *
    2135      * @global WP_Embed $wp_embed
    2136      */
    2137     public function __construct() {
    2138         global $wp_embed;
    2139 
    2140         // Make sure we populate the WP_Embed handlers array.
    2141         // These are providers that use a regex callback on the URL in question.
    2142         // Do not confuse with oEmbed providers, which require an external ping.
    2143         // Used in WP_Embed::shortcode()
    2144         $this->handlers = $wp_embed->handlers;
    2145 
    2146         if ( bp_use_embed_in_activity() ) {
    2147             add_filter( 'bp_get_activity_content_body', array( &$this, 'autoembed' ), 8 );
    2148             add_filter( 'bp_get_activity_content_body', array( &$this, 'run_shortcode' ), 7 );
    2149         }
    2150 
    2151         if ( bp_use_embed_in_activity_replies() ) {
    2152             add_filter( 'bp_get_activity_content', array( &$this, 'autoembed' ), 8 );
    2153             add_filter( 'bp_get_activity_content', array( &$this, 'run_shortcode' ), 7 );
    2154         }
    2155 
    2156         if ( bp_use_embed_in_forum_posts() ) {
    2157             add_filter( 'bp_get_the_topic_post_content', array( &$this, 'autoembed' ), 8 );
    2158             add_filter( 'bp_get_the_topic_post_content', array( &$this, 'run_shortcode' ), 7 );
    2159         }
    2160 
    2161         if ( bp_use_embed_in_private_messages() ) {
    2162             add_filter( 'bp_get_the_thread_message_content', array( &$this, 'autoembed' ), 8 );
    2163             add_filter( 'bp_get_the_thread_message_content', array( &$this, 'run_shortcode' ), 7 );
    2164         }
    2165 
    2166         do_action_ref_array( 'bp_core_setup_oembed', array( &$this ) );
    2167     }
    2168 
    2169     /**
    2170      * The {@link do_shortcode()} callback function.
    2171      *
    2172      * Attempts to convert a URL into embed HTML. Starts by checking the
    2173      * URL against the regex of the registered embed handlers. Next, checks
    2174      * the URL against the regex of registered {@link WP_oEmbed} providers
    2175      * if oEmbed discovery is false. If none of the regex matches and it's
    2176      * enabled, then the URL will be passed to {@link BP_Embed::parse_oembed()}
    2177      * for oEmbed parsing.
    2178      *
    2179      * @uses wp_parse_args()
    2180      * @uses wp_embed_defaults()
    2181      * @uses current_user_can()
    2182      * @uses _wp_oembed_get_object()
    2183      * @uses WP_Embed::maybe_make_link()
    2184      *
    2185      * @param array $attr Shortcode attributes.
    2186      * @param string $url The URL attempting to be embeded.
    2187      * @return string The embed HTML on success, otherwise the original URL.
    2188      */
    2189     public function shortcode( $attr, $url = '' ) {
    2190         if ( empty( $url ) )
    2191             return '';
    2192 
    2193         $rawattr = $attr;
    2194         $attr = wp_parse_args( $attr, wp_embed_defaults() );
    2195 
    2196         // kses converts & into &amp; and we need to undo this
    2197         // See http://core.trac.wordpress.org/ticket/11311
    2198         $url = str_replace( '&amp;', '&', $url );
    2199 
    2200         // Look for known internal handlers
    2201         ksort( $this->handlers );
    2202         foreach ( $this->handlers as $priority => $handlers ) {
    2203             foreach ( $handlers as $hid => $handler ) {
    2204                 if ( preg_match( $handler['regex'], $url, $matches ) && is_callable( $handler['callback'] ) ) {
    2205                     if ( false !== $return = call_user_func( $handler['callback'], $matches, $attr, $url, $rawattr ) ) {
    2206                         return apply_filters( 'embed_handler_html', $return, $url, $attr );
    2207                     }
    2208                 }
    2209             }
    2210         }
    2211 
    2212         // Get object ID
    2213         $id = apply_filters( 'embed_post_id', 0 );
    2214 
    2215         // Is oEmbed discovery on?
    2216         $attr['discover'] = ( apply_filters( 'bp_embed_oembed_discover', false ) && current_user_can( 'unfiltered_html' ) );
    2217 
    2218         // Set up a new WP oEmbed object to check URL with registered oEmbed providers
    2219         require_once( ABSPATH . WPINC . '/class-oembed.php' );
    2220         $oembed_obj = _wp_oembed_get_object();
    2221 
    2222         // If oEmbed discovery is true, skip oEmbed provider check
    2223         $is_oembed_link = false;
    2224         if ( !$attr['discover'] ) {
    2225             foreach ( (array) $oembed_obj->providers as $provider_matchmask => $provider ) {
    2226                 $regex = ( $is_regex = $provider[1] ) ? $provider_matchmask : '#' . str_replace( '___wildcard___', '(.+)', preg_quote( str_replace( '*', '___wildcard___', $provider_matchmask ), '#' ) ) . '#i';
    2227 
    2228                 if ( preg_match( $regex, $url ) )
    2229                     $is_oembed_link = true;
    2230             }
    2231 
    2232             // If url doesn't match a WP oEmbed provider, stop parsing
    2233             if ( !$is_oembed_link )
    2234                 return $this->maybe_make_link( $url );
    2235         }
    2236 
    2237         return $this->parse_oembed( $id, $url, $attr, $rawattr );
    2238     }
    2239 
    2240     /**
    2241      * Base function so BP components/plugins can parse links to be embedded.
    2242      *
    2243      * View an example to add support in {@link bp_activity_embed()}.
    2244      *
    2245      * @uses apply_filters() Filters cache.
    2246      * @uses do_action() To save cache.
    2247      * @uses wp_oembed_get() Connects to oEmbed provider and returns HTML
    2248      *       on success.
    2249      * @uses WP_Embed::maybe_make_link() Process URL for hyperlinking on
    2250      *       oEmbed failure.
    2251      *
    2252      * @param int $id ID to do the caching for.
    2253      * @param string $url The URL attempting to be embedded.
    2254      * @param array $attr Shortcode attributes from {@link WP_Embed::shortcode()}.
    2255      * @param array $rawattr Untouched shortcode attributes from
    2256      *        {@link WP_Embed::shortcode()}.
    2257      * @return string The embed HTML on success, otherwise the original URL.
    2258      */
    2259     public function parse_oembed( $id, $url, $attr, $rawattr ) {
    2260         $id = intval( $id );
    2261 
    2262         if ( $id ) {
    2263             // Setup the cachekey
    2264             $cachekey = '_oembed_' . md5( $url . serialize( $attr ) );
    2265 
    2266             // Let components / plugins grab their cache
    2267             $cache = '';
    2268             $cache = apply_filters( 'bp_embed_get_cache', $cache, $id, $cachekey, $url, $attr, $rawattr );
    2269 
    2270             // Grab cache and return it if available
    2271             if ( !empty( $cache ) ) {
    2272                 return apply_filters( 'bp_embed_oembed_html', $cache, $url, $attr, $rawattr );
    2273 
    2274             // If no cache, ping the oEmbed provider and cache the result
    2275             } else {
    2276                 $html = wp_oembed_get( $url, $attr );
    2277                 $cache = ( $html ) ? $html : $url;
    2278 
    2279                 // Let components / plugins save their cache
    2280                 do_action( 'bp_embed_update_cache', $cache, $cachekey, $id );
    2281 
    2282                 // If there was a result, return it
    2283                 if ( $html )
    2284                     return apply_filters( 'bp_embed_oembed_html', $html, $url, $attr, $rawattr );
    2285             }
    2286         }
    2287 
    2288         // Still unknown
    2289         return $this->maybe_make_link( $url );
    2290     }
    2291 }
    2292 
    2293 /**
    2294  * Create HTML list of BP nav items.
    2295  *
    2296  * @since BuddyPress (1.7.0)
    2297  */
    2298 class BP_Walker_Nav_Menu extends Walker_Nav_Menu {
    2299 
    2300     /**
    2301      * Description of fields indexes for building markup.
    2302      *
    2303      * @since BuddyPress (1.7.0)
    2304      * @var array
    2305      */
    2306     var $db_fields = array( 'id' => 'css_id', 'parent' => 'parent' );
    2307 
    2308     /**
    2309      * Tree type.
    2310      *
    2311      * @since BuddyPress (1.7.0)
    2312      * @var string
    2313      */
    2314     var $tree_type = array();
    2315 
    2316     /**
    2317      * Display array of elements hierarchically.
    2318      *
    2319      * This method is almost identical to the version in {@link Walker::walk()}.
    2320      * The only change is on one line which has been commented. An IF was
    2321      * comparing 0 to a non-empty string which was preventing child elements
    2322      * being grouped under their parent menu element.
    2323      *
    2324      * This caused a problem for BuddyPress because our primary/secondary
    2325      * navigations don't have a unique numerical ID that describes a
    2326      * hierarchy (we use a slug). Obviously, WordPress Menus use Posts, and
    2327      * those have ID/post_parent.
    2328      *
    2329      * @since BuddyPress (1.7.0)
    2330      *
    2331      * @see Walker::walk()
    2332      *
    2333      * @param array $elements See {@link Walker::walk()}.
    2334      * @param int $max_depth See {@link Walker::walk()}.
    2335      * @return string See {@link Walker::walk()}.
    2336      */
    2337     public function walk( $elements, $max_depth ) {
    2338         $func_args = func_get_args();
    2339 
    2340         $args   = array_slice( $func_args, 2 );
    2341         $output = '';
    2342 
    2343         if ( $max_depth < -1 ) // invalid parameter
    2344             return $output;
    2345 
    2346         if ( empty( $elements ) ) // nothing to walk
    2347             return $output;
    2348 
    2349         $parent_field = $this->db_fields['parent'];
    2350 
    2351         // flat display
    2352         if ( -1 == $max_depth ) {
    2353 
    2354             $empty_array = array();
    2355             foreach ( $elements as $e )
    2356                 $this->display_element( $e, $empty_array, 1, 0, $args, $output );
    2357 
    2358             return $output;
    2359         }
    2360 
    2361         /*
    2362          * need to display in hierarchical order
    2363          * separate elements into two buckets: top level and children elements
    2364          * children_elements is two dimensional array, eg.
    2365          * children_elements[10][] contains all sub-elements whose parent is 10.
    2366          */
    2367         $top_level_elements = array();
    2368         $children_elements  = array();
    2369 
    2370         foreach ( $elements as $e ) {
    2371             // BuddyPress: changed '==' to '==='. This is the only change from version in Walker::walk().
    2372             if ( 0 === $e->$parent_field )
    2373                 $top_level_elements[] = $e;
    2374             else
    2375                 $children_elements[$e->$parent_field][] = $e;
    2376         }
    2377 
    2378         /*
    2379          * when none of the elements is top level
    2380          * assume the first one must be root of the sub elements
    2381          */
    2382         if ( empty( $top_level_elements ) ) {
    2383 
    2384             $first              = array_slice( $elements, 0, 1 );
    2385             $root               = $first[0];
    2386             $top_level_elements = array();
    2387             $children_elements  = array();
    2388 
    2389             foreach ( $elements as $e ) {
    2390                 if ( $root->$parent_field == $e->$parent_field )
    2391                     $top_level_elements[] = $e;
    2392                 else
    2393                     $children_elements[$e->$parent_field][] = $e;
    2394             }
    2395         }
    2396 
    2397         foreach ( $top_level_elements as $e )
    2398             $this->display_element( $e, $children_elements, $max_depth, 0, $args, $output );
    2399 
    2400         /*
    2401          * if we are displaying all levels, and remaining children_elements is not empty,
    2402          * then we got orphans, which should be displayed regardless
    2403          */
    2404         if ( ( $max_depth == 0 ) && count( $children_elements ) > 0 ) {
    2405             $empty_array = array();
    2406 
    2407             foreach ( $children_elements as $orphans )
    2408                 foreach ( $orphans as $op )
    2409                     $this->display_element( $op, $empty_array, 1, 0, $args, $output );
    2410          }
    2411 
    2412          return $output;
    2413     }
    2414 
    2415     /**
    2416      * Display the current <li> that we are on.
    2417      *
    2418      * @see Walker::start_el() for complete description of parameters .
    2419      *
    2420      * @since BuddyPress (1.7.0)
    2421      *
    2422      * @param string $output Passed by reference. Used to append
    2423      *        additional content.
    2424      * @param object $item Menu item data object.
    2425      * @param int $depth Depth of menu item. Used for padding. Optional,
    2426      *        defaults to 0.
    2427      * @param array $args Optional. See {@link Walker::start_el()}.
    2428      * @param int $current_page Menu item ID. Optional.
    2429      */
    2430     public function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
    2431         // If we're someway down the tree, indent the HTML with the appropriate number of tabs
    2432         $indent = $depth ? str_repeat( "\t", $depth ) : '';
    2433 
    2434         // Add HTML classes
    2435         $class_names = join( ' ', apply_filters( 'bp_nav_menu_css_class', array_filter( $item->class ), $item, $args ) );
    2436         $class_names = ! empty( $class_names ) ? ' class="' . esc_attr( $class_names ) . '"' : '';
    2437 
    2438         // Add HTML ID
    2439         $id = sanitize_html_class( $item->css_id . '-personal-li' );  // Backpat with BP pre-1.7
    2440         $id = apply_filters( 'bp_nav_menu_item_id', $id, $item, $args );
    2441         $id = ! empty( $id ) ? ' id="' . esc_attr( $id ) . '"' : '';
    2442 
    2443         // Opening tag; closing tag is handled in Walker_Nav_Menu::end_el().
    2444         $output .= $indent . '<li' . $id . $class_names . '>';
    2445 
    2446         // Add href attribute
    2447         $attributes = ! empty( $item->link ) ? ' href="' . esc_attr( esc_url( $item->link ) ) . '"' : '';
    2448 
    2449         // Construct the link
    2450         $item_output = $args->before;
    2451         $item_output .= '<a' . $attributes . '>';
    2452         $item_output .= $args->link_before . apply_filters( 'the_title', $item->name, 0 ) . $args->link_after;
    2453         $item_output .= '</a>';
    2454         $item_output .= $args->after;
    2455 
    2456         // $output is byref
    2457         $output .= apply_filters( 'bp_walker_nav_menu_start_el', $item_output, $item, $depth, $args );
    2458     }
    2459 }
    2460 
    2461 /**
    2462  * Create a set of BuddyPress-specific links for use in the Menus admin UI.
    2463  *
    2464  * Borrowed heavily from {@link Walker_Nav_Menu_Checklist}, but modified so as not
    2465  * to require an actual post type or taxonomy, and to force certain CSS classes
    2466  *
    2467  * @since BuddyPress (1.9.0)
    2468  */
    2469 class BP_Walker_Nav_Menu_Checklist extends Walker_Nav_Menu {
    2470 
    2471     /**
    2472      * Constructor.
    2473      *
    2474      * @see Walker_Nav_Menu::__construct() for a description of parameters.
    2475      *
    2476      * @param array $fields See {@link Walker_Nav_Menu::__construct()}.
    2477      */
    2478     public function __construct( $fields = false ) {
    2479         if ( $fields ) {
    2480             $this->db_fields = $fields;
    2481         }
    2482     }
    2483 
    2484     /**
    2485      * Create the markup to start a tree level.
    2486      *
    2487      * @see Walker_Nav_Menu::start_lvl() for description of parameters.
    2488      *
    2489      * @param string $output See {@Walker_Nav_Menu::start_lvl()}.
    2490      * @param int $depth See {@Walker_Nav_Menu::start_lvl()}.
    2491      * @param array $args See {@Walker_Nav_Menu::start_lvl()}.
    2492      */
    2493     public function start_lvl( &$output, $depth = 0, $args = array() ) {
    2494         $indent = str_repeat( "\t", $depth );
    2495         $output .= "\n$indent<ul class='children'>\n";
    2496     }
    2497 
    2498     /**
    2499      * Create the markup to end a tree level.
    2500      *
    2501      * @see Walker_Nav_Menu::end_lvl() for description of parameters.
    2502      *
    2503      * @param string $output See {@Walker_Nav_Menu::end_lvl()}.
    2504      * @param int $depth See {@Walker_Nav_Menu::end_lvl()}.
    2505      * @param array $args See {@Walker_Nav_Menu::end_lvl()}.
    2506      */
    2507     public function end_lvl( &$output, $depth = 0, $args = array() ) {
    2508         $indent = str_repeat( "\t", $depth );
    2509         $output .= "\n$indent</ul>";
    2510     }
    2511 
    2512     /**
    2513      * Create the markup to start an element.
    2514      *
    2515      * @see Walker::start_el() for description of parameters.
    2516      *
    2517      * @param string $output Passed by reference. Used to append additional
    2518      *        content.
    2519      * @param object $item Menu item data object.
    2520      * @param int $depth Depth of menu item. Used for padding.
    2521      * @param object $args See {@Walker::start_el()}.
    2522      * @param int $id See {@Walker::start_el()}.
    2523      */
    2524     function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
    2525         global $_nav_menu_placeholder;
    2526 
    2527         $_nav_menu_placeholder = ( 0 > $_nav_menu_placeholder ) ? intval($_nav_menu_placeholder) - 1 : -1;
    2528         $possible_object_id = isset( $item->post_type ) && 'nav_menu_item' == $item->post_type ? $item->object_id : $_nav_menu_placeholder;
    2529         $possible_db_id = ( ! empty( $item->ID ) ) && ( 0 < $possible_object_id ) ? (int) $item->ID : 0;
    2530 
    2531         $indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';
    2532 
    2533         $output .= $indent . '<li>';
    2534         $output .= '<label class="menu-item-title">';
    2535         $output .= '<input type="checkbox" class="menu-item-checkbox';
    2536 
    2537         if ( property_exists( $item, 'label' ) ) {
    2538             $title = $item->label;
    2539         }
    2540 
    2541         $output .= '" name="menu-item[' . $possible_object_id . '][menu-item-object-id]" value="'. esc_attr( $item->object_id ) .'" /> ';
    2542         $output .= isset( $title ) ? esc_html( $title ) : esc_html( $item->title );
    2543         $output .= '</label>';
    2544 
    2545         if ( empty( $item->url ) ) {
    2546             $item->url = $item->guid;
    2547         }
    2548 
    2549         if ( ! in_array( array( 'bp-menu', 'bp-'. $item->post_excerpt .'-nav' ), $item->classes ) ) {
    2550             $item->classes[] = 'bp-menu';
    2551             $item->classes[] = 'bp-'. $item->post_excerpt .'-nav';
    2552         }
    2553 
    2554         // Menu item hidden fields
    2555         $output .= '<input type="hidden" class="menu-item-db-id" name="menu-item[' . $possible_object_id . '][menu-item-db-id]" value="' . $possible_db_id . '" />';
    2556         $output .= '<input type="hidden" class="menu-item-object" name="menu-item[' . $possible_object_id . '][menu-item-object]" value="'. esc_attr( $item->object ) .'" />';
    2557         $output .= '<input type="hidden" class="menu-item-parent-id" name="menu-item[' . $possible_object_id . '][menu-item-parent-id]" value="'. esc_attr( $item->menu_item_parent ) .'" />';
    2558         $output .= '<input type="hidden" class="menu-item-type" name="menu-item[' . $possible_object_id . '][menu-item-type]" value="custom" />';
    2559         $output .= '<input type="hidden" class="menu-item-title" name="menu-item[' . $possible_object_id . '][menu-item-title]" value="'. esc_attr( $item->title ) .'" />';
    2560         $output .= '<input type="hidden" class="menu-item-url" name="menu-item[' . $possible_object_id . '][menu-item-url]" value="'. esc_attr( $item->url ) .'" />';
    2561         $output .= '<input type="hidden" class="menu-item-target" name="menu-item[' . $possible_object_id . '][menu-item-target]" value="'. esc_attr( $item->target ) .'" />';
    2562         $output .= '<input type="hidden" class="menu-item-attr_title" name="menu-item[' . $possible_object_id . '][menu-item-attr_title]" value="'. esc_attr( $item->attr_title ) .'" />';
    2563         $output .= '<input type="hidden" class="menu-item-classes" name="menu-item[' . $possible_object_id . '][menu-item-classes]" value="'. esc_attr( implode( ' ', $item->classes ) ) .'" />';
    2564         $output .= '<input type="hidden" class="menu-item-xfn" name="menu-item[' . $possible_object_id . '][menu-item-xfn]" value="'. esc_attr( $item->xfn ) .'" />';
    2565     }
    2566 }
    2567 
    2568 /**
    2569  * Base class for the BuddyPress Suggestions API.
    2570  *
    2571  * Originally built to power BuddyPress' at-mentions suggestions, it's flexible enough to be used
    2572  * for similar kinds of future core requirements, or those desired by third-party developers.
    2573  *
    2574  * To implement a new suggestions service, create a new class that extends this one, and update
    2575  * the list of default services in {@link bp_core_get_suggestions()}. If you're building a plugin,
    2576  * it's recommend that you use the `bp_suggestions_services` filter to do this. :)
    2577  *
    2578  * While the implementation of the query logic is left to you, it should be as quick and efficient
    2579  * as possible. When implementing the abstract methods in this class, pay close attention to the
    2580  * recommendations provided in the phpDoc blocks, particularly the expected return types.
    2581  *
    2582  * @since BuddyPress (2.1.0)
    2583  */
    2584 abstract class BP_Suggestions {
    2585 
    2586     /**
    2587      * Default arguments common to all suggestions services.
    2588      *
    2589      * If your custom service requires further defaults, add them here.
    2590      *
    2591      * @since BuddyPress (2.1.0)
    2592      * @var array
    2593      */
    2594     protected $default_args = array(
    2595         'limit' => 16,
    2596         'term'  => '',
    2597         'type'  => '',
    2598     );
    2599 
    2600     /**
    2601      * Holds the arguments for the query (about to made to the suggestions service).
    2602      *
    2603      * This includes `$default_args`, as well as the user-supplied values.
    2604      *
    2605      * @since BuddyPress (2.1.0)
    2606      * @var array
    2607      */
    2608     protected $args = array(
    2609     );
    2610 
    2611 
    2612     /**
    2613      * Constructor.
    2614      *
    2615      * @param array $args Optional. If set, used as the parameters for the suggestions service query.
    2616      * @since BuddyPress (2.1.0)
    2617      */
    2618     public function __construct( array $args = array() ) {
    2619         if ( ! empty( $args ) ) {
    2620             $this->set_query( $args );
    2621         }
    2622     }
    2623 
    2624     /**
    2625      * Set the parameters for the suggestions service query.
    2626      *
    2627      * @param array $args {
    2628      *     @type int $limit Maximum number of results to display. Optional, default: 16.
    2629      *     @type string $type The name of the suggestion service to use for the request. Mandatory.
    2630      *     @type string $term The suggestion service will try to find results that contain this string.
    2631      *           Mandatory.
    2632      * }
    2633      * @since BuddyPress (2.1.0)
    2634      */
    2635     public function set_query( array $args = array() ) {
    2636         $this->args = wp_parse_args( $args, $this->default_args );
    2637     }
    2638 
    2639     /**
    2640      * Validate and sanitise the parameters for the suggestion service query.
    2641      *
    2642      * Be sure to call this class' version of this method when implementing it in your own service.
    2643      * If validation fails, you must return a WP_Error object.
    2644      *
    2645      * @return true|WP_Error If validation fails, return a WP_Error object. On success, return true (bool).
    2646      * @since BuddyPress (2.1.0)
    2647      */
    2648     public function validate() {
    2649         $this->args['limit'] = absint( $this->args['limit'] );
    2650         $this->args['term']  = trim( sanitize_text_field( $this->args['term'] ) );
    2651         $this->args          = apply_filters( 'bp_suggestions_args', $this->args, $this );
    2652 
    2653 
    2654         // Check for invalid or missing mandatory parameters.
    2655         if ( ! $this->args['limit'] || ! $this->args['term'] ) {
    2656             return new WP_Error( 'missing_parameter' );
    2657         }
    2658 
    2659         // Check for blocked users (e.g. deleted accounts, or spammers).
    2660         if ( is_user_logged_in() && ! bp_is_user_active( get_current_user_id() ) ) {
    2661             return new WP_Error( 'invalid_user' );
    2662         }
    2663 
    2664         return apply_filters( 'bp_suggestions_validate_args', true, $this );
    2665     }
    2666 
    2667     /**
    2668      * Find and return a list of suggestions that match the query.
    2669      *
    2670      * The return type is important. If no matches are found, an empty array must be returned.
    2671      * Matches must be returned as objects in an array.
    2672      *
    2673      * The object format for each match must be: { 'ID': string, 'image': string, 'name': string }
    2674      * For example: { 'ID': 'admin', 'image': 'http://example.com/logo.png', 'name': 'Name Surname' }
    2675      *
    2676      * @return array|WP_Error Array of results. If there were problems, returns a WP_Error object.
    2677      * @since BuddyPress (2.1.0)
    2678      */
    2679     abstract public function get_suggestions();
    2680 }
    2681 
    2682 /**
    2683  * Adds support for user at-mentions to the Suggestions API.
    2684  *
    2685  * This class is in the Core component because it's required by a class in the Groups component,
    2686  * and Groups is loaded before Members (alphabetical order).
    2687  *
    2688  * @since BuddyPress (2.1.0)
    2689  */
    2690 class BP_Members_Suggestions extends BP_Suggestions {
    2691 
    2692     /**
    2693      * Default arguments for this suggestions service.
    2694      *
    2695      * @since BuddyPress (2.1.0)
    2696      * @var array $args {
    2697      *     @type int $limit Maximum number of results to display. Default: 16.
    2698      *     @type bool $only_friends If true, only match the current user's friends. Default: false.
    2699      *     @type string $term The suggestion service will try to find results that contain this string.
    2700      *           Mandatory.
    2701      * }
    2702      */
    2703     protected $default_args = array(
    2704         'limit'        => 10,
    2705         'only_friends' => false,
    2706         'term'         => '',
    2707         'type'         => '',
    2708     );
    2709 
    2710 
    2711     /**
    2712      * Validate and sanitise the parameters for the suggestion service query.
    2713      *
    2714      * @return true|WP_Error If validation fails, return a WP_Error object. On success, return true (bool).
    2715      * @since BuddyPress (2.1.0)
    2716      */
    2717     public function validate() {
    2718         $this->args['only_friends'] = (bool) $this->args['only_friends'];
    2719         $this->args                 = apply_filters( 'bp_members_suggestions_args', $this->args, $this );
    2720 
    2721         // Check for invalid or missing mandatory parameters.
    2722         if ( $this->args['only_friends'] && ( ! bp_is_active( 'friends' ) || ! is_user_logged_in() ) ) {
    2723             return new WP_Error( 'missing_requirement' );
    2724         }
    2725 
    2726         return apply_filters( 'bp_members_suggestions_validate_args', parent::validate(), $this );
    2727     }
    2728 
    2729     /**
    2730      * Find and return a list of username suggestions that match the query.
    2731      *
    2732      * @return array|WP_Error Array of results. If there were problems, returns a WP_Error object.
    2733      * @since BuddyPress (2.1.0)
    2734      */
    2735     public function get_suggestions() {
    2736         $user_query = array(
    2737             'count_total'     => '',  // Prevents total count
    2738             'populate_extras' => false,
    2739             'type'            => 'alphabetical',
    2740 
    2741             'page'            => 1,
    2742             'per_page'        => $this->args['limit'],
    2743             'search_terms'    => $this->args['term'],
    2744             'search_wildcard' => 'right',
    2745         );
    2746 
    2747         // Only return matches of friends of this user.
    2748         if ( $this->args['only_friends'] && is_user_logged_in() ) {
    2749             $user_query['user_id'] = get_current_user_id();
    2750         }
    2751 
    2752         $user_query = apply_filters( 'bp_members_suggestions_query_args', $user_query, $this );
    2753         if ( is_wp_error( $user_query ) ) {
    2754             return $user_query;
    2755         }
    2756 
    2757 
    2758         $user_query = new BP_User_Query( $user_query );
    2759         $results    = array();
    2760 
    2761         foreach ( $user_query->results as $user ) {
    2762             $result        = new stdClass();
    2763             $result->ID    = $user->user_nicename;
    2764             $result->image = bp_core_fetch_avatar( array( 'html' => false, 'item_id' => $user->ID ) );
    2765             $result->name  = bp_core_get_user_displayname( $user->ID );
    2766 
    2767             $results[] = $result;
    2768         }
    2769 
    2770         return apply_filters( 'bp_members_suggestions_get_suggestions', $results, $this );
    2771     }
    2772 }
    2773 
    2774 /**
    2775  * Base class for creating query classes that generate SQL fragments for filtering results based on recursive query params.
    2776  *
    2777  * @since BuddyPress (2.2.0)
    2778  */
    2779 abstract class BP_Recursive_Query {
    2780 
    2781     /**
    2782      * Query arguments passed to the constructor.
    2783      *
    2784      * @since BuddyPress (2.2.0)
    2785      * @access public
    2786      * @var array
    2787      */
    2788     public $queries = array();
    2789 
    2790     /**
    2791      * Generate SQL clauses to be appended to a main query.
    2792      *
    2793      * Extending classes should call this method from within a publicly
    2794      * accessible get_sql() method, and manipulate the SQL as necessary.
    2795      * For example, {@link BP_XProfile_Query::get_sql()} is merely a wrapper for
    2796      * get_sql_clauses(), while {@link BP_Activity_Query::get_sql()} discards
    2797      * the empty 'join' clause, and only passes the 'where' clause.
    2798      *
    2799      * @since BuddyPress (2.2.0)
    2800      * @access protected
    2801      *
    2802      * @param  string $primary_table
    2803      * @param  string $primary_id_column
    2804      * @return array
    2805      */
    2806     protected function get_sql_clauses() {
    2807         $sql = $this->get_sql_for_query( $this->queries );
    2808 
    2809         if ( ! empty( $sql['where'] ) ) {
    2810             $sql['where'] = ' AND ' . "\n" . $sql['where'] . "\n";
    2811         }
    2812 
    2813         return $sql;
    2814     }
    2815 
    2816     /**
    2817      * Generate SQL clauses for a single query array.
    2818      *
    2819      * If nested subqueries are found, this method recurses the tree to
    2820      * produce the properly nested SQL.
    2821      *
    2822      * Subclasses generally do not need to call this method. It is invoked
    2823      * automatically from get_sql_clauses().
    2824      *
    2825      * @since BuddyPress (2.2.0)
    2826      * @access protected
    2827      *
    2828      * @param  array $query Query to parse.
    2829      * @param  int   $depth Optional. Number of tree levels deep we
    2830      *                      currently are. Used to calculate indentation.
    2831      * @return array
    2832      */
    2833     protected function get_sql_for_query( $query, $depth = 0 ) {
    2834         $sql_chunks = array(
    2835             'join'  => array(),
    2836             'where' => array(),
    2837         );
    2838 
    2839         $sql = array(
    2840             'join'  => '',
    2841             'where' => '',
    2842         );
    2843 
    2844         $indent = '';
    2845         for ( $i = 0; $i < $depth; $i++ ) {
    2846             $indent .= "\t";
    2847         }
    2848 
    2849         foreach ( $query as $key => $clause ) {
    2850             if ( 'relation' === $key ) {
    2851                 $relation = $query['relation'];
    2852             } elseif ( is_array( $clause ) ) {
    2853                 // This is a first-order clause
    2854                 if ( $this->is_first_order_clause( $clause ) ) {
    2855                     $clause_sql = $this->get_sql_for_clause( $clause, $query );
    2856 
    2857                     $where_count = count( $clause_sql['where'] );
    2858                     if ( ! $where_count ) {
    2859                         $sql_chunks['where'][] = '';
    2860                     } elseif ( 1 === $where_count ) {
    2861                         $sql_chunks['where'][] = $clause_sql['where'][0];
    2862                     } else {
    2863                         $sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )';
    2864                     }
    2865 
    2866                     $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] );
    2867                 // This is a subquery
    2868                 } else {
    2869                     $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 );
    2870 
    2871                     $sql_chunks['where'][] = $clause_sql['where'];
    2872                     $sql_chunks['join'][]  = $clause_sql['join'];
    2873                 }
    2874             }
    2875         }
    2876 
    2877         // Filter empties
    2878         $sql_chunks['join']  = array_filter( $sql_chunks['join'] );
    2879         $sql_chunks['where'] = array_filter( $sql_chunks['where'] );
    2880 
    2881         if ( empty( $relation ) ) {
    2882             $relation = 'AND';
    2883         }
    2884 
    2885         if ( ! empty( $sql_chunks['join'] ) ) {
    2886             $sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) );
    2887         }
    2888 
    2889         if ( ! empty( $sql_chunks['where'] ) ) {
    2890             $sql['where'] = '( ' . "\n\t" . $indent . implode( ' ' . "\n\t" . $indent . $relation . ' ' . "\n\t" . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')' . "\n";
    2891         }
    2892 
    2893         return $sql;
    2894     }
    2895 
    2896     /**
    2897      * Recursive-friendly query sanitizer.
    2898      *
    2899      * Ensures that each query-level clause has a 'relation' key, and that
    2900      * each first-order clause contains all the necessary keys from
    2901      * $defaults.
    2902      *
    2903      * Extend this method if your class uses different sanitizing logic.
    2904      *
    2905      * @since BuddyPress (2.2.0)
    2906      * @access public
    2907      *
    2908      * @param  array $queries Array of query clauses.
    2909      * @return array Sanitized array of query clauses.
    2910      */
    2911     protected function sanitize_query( $queries ) {
    2912         $clean_queries = array();
    2913 
    2914         if ( ! is_array( $queries ) ) {
    2915             return $clean_queries;
    2916         }
    2917 
    2918         foreach ( $queries as $key => $query ) {
    2919             if ( 'relation' === $key ) {
    2920                 $relation = $query;
    2921 
    2922             } elseif ( ! is_array( $query ) ) {
    2923                 continue;
    2924 
    2925             // First-order clause.
    2926             } elseif ( $this->is_first_order_clause( $query ) ) {
    2927                 if ( isset( $query['value'] ) && array() === $query['value'] ) {
    2928                     unset( $query['value'] );
    2929                 }
    2930 
    2931                 $clean_queries[] = $query;
    2932 
    2933             // Otherwise, it's a nested query, so we recurse.
    2934             } else {
    2935                 $cleaned_query = $this->sanitize_query( $query );
    2936 
    2937                 if ( ! empty( $cleaned_query ) ) {
    2938                     $clean_queries[] = $cleaned_query;
    2939                 }
    2940             }
    2941         }
    2942 
    2943         if ( empty( $clean_queries ) ) {
    2944             return $clean_queries;
    2945         }
    2946 
    2947         // Sanitize the 'relation' key provided in the query.
    2948         if ( isset( $relation ) && 'OR' === strtoupper( $relation ) ) {
    2949             $clean_queries['relation'] = 'OR';
    2950 
    2951         /*
    2952          * If there is only a single clause, call the relation 'OR'.
    2953          * This value will not actually be used to join clauses, but it
    2954          * simplifies the logic around combining key-only queries.
    2955          */
    2956         } elseif ( 1 === count( $clean_queries ) ) {
    2957             $clean_queries['relation'] = 'OR';
    2958 
    2959         // Default to AND.
    2960         } else {
    2961             $clean_queries['relation'] = 'AND';
    2962         }
    2963 
    2964         return $clean_queries;
    2965     }
    2966 
    2967     /**
    2968      * Generate JOIN and WHERE clauses for a first-order clause.
    2969      *
    2970      * Must be overridden in a subclass.
    2971      *
    2972      * @since BuddyPress (2.2.0)
    2973      * @access protected
    2974      *
    2975      * @param  array $clause       Array of arguments belonging to the clause.
    2976      * @param  array $parent_query Parent query to which the clause belongs.
    2977      * @return array {
    2978      *     @type array $join  Array of subclauses for the JOIN statement.
    2979      *     @type array $where Array of subclauses for the WHERE statement.
    2980      * }
    2981      */
    2982     abstract protected function get_sql_for_clause( $clause, $parent_query );
    2983 
    2984     /**
    2985      * Determine whether a clause is first-order.
    2986      *
    2987      * Must be overridden in a subclass.
    2988      *
    2989      * @since BuddyPress (2.2.0)
    2990      * @access protected
    2991      *
    2992      * @param  array $q Clause to check.
    2993      * @return bool
    2994      */
    2995     abstract protected function is_first_order_clause( $query );
    2996 }
     12require __DIR__ . '/classes/class-bp-user-query.php';
     13require __DIR__ . '/classes/class-bp-core-user.php';
     14require __DIR__ . '/classes/class-bp-date-query.php';
     15require __DIR__ . '/classes/class-bp-core-notification.php';
     16require __DIR__ . '/classes/class-bp-button.php';
     17require __DIR__ . '/classes/class-bp-embed.php';
     18require __DIR__ . '/classes/class-bp-walker-nav-menu.php';
     19require __DIR__ . '/classes/class-bp-walker-nav-menu-checklist.php';
     20require __DIR__ . '/classes/class-bp-suggestions.php';
     21require __DIR__ . '/classes/class-bp-members-suggestions.php';
     22require __DIR__ . '/classes/class-bp-recursive-query.php';
  • trunk/src/bp-friends/bp-friends-classes.php

    r9471 r9485  
    1010defined( 'ABSPATH' ) || exit;
    1111
    12 /**
    13  * BuddyPress Friendship object.
    14  */
    15 class BP_Friends_Friendship {
    16 
    17     /**
    18      * ID of the friendship.
    19      *
    20      * @access public
    21      * @var int
    22      */
    23     public $id;
    24 
    25     /**
    26      * User ID of the friendship initiator.
    27      *
    28      * @access public
    29      * @var int
    30      */
    31     public $initiator_user_id;
    32 
    33     /**
    34      * User ID of the 'friend' - the one invited to the friendship.
    35      *
    36      * @access public
    37      * @var int
    38      */
    39     public $friend_user_id;
    40 
    41     /**
    42      * Has the friendship been confirmed/accepted?
    43      *
    44      * @access public
    45      * @var int
    46      */
    47     public $is_confirmed;
    48 
    49     /**
    50      * Is this a "limited" friendship?
    51      *
    52      * Not currently used by BuddyPress.
    53      *
    54      * @access public
    55      * @var int
    56      */
    57     public $is_limited;
    58 
    59     /**
    60      * Date the friendship was created.
    61      *
    62      * @access public
    63      * @var string
    64      */
    65     public $date_created;
    66 
    67     /**
    68      * Is this a request?
    69      *
    70      * Not currently used in BuddyPress.
    71      *
    72      * @access public
    73      * @var unknown
    74      */
    75     public $is_request;
    76 
    77     /**
    78      * Should additional friend details be queried?
    79      *
    80      * @access public
    81      * @var bool
    82      */
    83     public $populate_friend_details;
    84 
    85     /**
    86      * Details about the friend.
    87      *
    88      * @access public
    89      * @var BP_Core_User
    90      */
    91     public $friend;
    92 
    93     /**
    94      * Constructor method.
    95      *
    96      * @param int $id Optional. The ID of an existing friendship.
    97      * @param bool $is_request Deprecated.
    98      * @param bool $populate_friend_details True if friend details should
    99      *        be queried.
    100      */
    101     public function __construct( $id = null, $is_request = false, $populate_friend_details = true ) {
    102         $this->is_request = $is_request;
    103 
    104         if ( !empty( $id ) ) {
    105             $this->id                      = $id;
    106             $this->populate_friend_details = $populate_friend_details;
    107             $this->populate( $this->id );
    108         }
    109     }
    110 
    111     /**
    112      * Set up data about the current friendship.
    113      */
    114     public function populate() {
    115         global $wpdb;
    116 
    117         $bp = buddypress();
    118 
    119         if ( $friendship = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$bp->friends->table_name} WHERE id = %d", $this->id ) ) ) {
    120             $this->initiator_user_id = $friendship->initiator_user_id;
    121             $this->friend_user_id    = $friendship->friend_user_id;
    122             $this->is_confirmed      = $friendship->is_confirmed;
    123             $this->is_limited        = $friendship->is_limited;
    124             $this->date_created      = $friendship->date_created;
    125         }
    126 
    127         if ( !empty( $this->populate_friend_details ) ) {
    128             if ( $this->friend_user_id == bp_displayed_user_id() ) {
    129                 $this->friend = new BP_Core_User( $this->initiator_user_id );
    130             } else {
    131                 $this->friend = new BP_Core_User( $this->friend_user_id );
    132             }
    133         }
    134     }
    135 
    136     /**
    137      * Save the current friendship to the database.
    138      *
    139      * @return bool True on success, false on failure.
    140      */
    141     public function save() {
    142         global $wpdb;
    143 
    144         $bp = buddypress();
    145 
    146         $this->initiator_user_id = apply_filters( 'friends_friendship_initiator_user_id_before_save', $this->initiator_user_id, $this->id );
    147         $this->friend_user_id    = apply_filters( 'friends_friendship_friend_user_id_before_save',    $this->friend_user_id,    $this->id );
    148         $this->is_confirmed      = apply_filters( 'friends_friendship_is_confirmed_before_save',      $this->is_confirmed,      $this->id );
    149         $this->is_limited        = apply_filters( 'friends_friendship_is_limited_before_save',        $this->is_limited,        $this->id );
    150         $this->date_created      = apply_filters( 'friends_friendship_date_created_before_save',      $this->date_created,      $this->id );
    151 
    152         /**
    153          * Fires before processing and saving the current friendship request.
    154          *
    155          * @since BuddyPress (1.0.0)
    156          *
    157          * @param Object $value Current friendship request object.
    158          */
    159         do_action_ref_array( 'friends_friendship_before_save', array( &$this ) );
    160 
    161         // Update
    162         if (!empty( $this->id ) ) {
    163             $result = $wpdb->query( $wpdb->prepare( "UPDATE {$bp->friends->table_name} SET initiator_user_id = %d, friend_user_id = %d, is_confirmed = %d, is_limited = %d, date_created = %s ) WHERE id = %d", $this->initiator_user_id, $this->friend_user_id, $this->is_confirmed, $this->is_limited, $this->date_created, $this->id ) );
    164 
    165         // Save
    166         } else {
    167             $result = $wpdb->query( $wpdb->prepare( "INSERT INTO {$bp->friends->table_name} ( initiator_user_id, friend_user_id, is_confirmed, is_limited, date_created ) VALUES ( %d, %d, %d, %d, %s )", $this->initiator_user_id, $this->friend_user_id, $this->is_confirmed, $this->is_limited, $this->date_created ) );
    168             $this->id = $wpdb->insert_id;
    169         }
    170 
    171         /**
    172          * Fires after processing and saving the current friendship request.
    173          *
    174          * @since BuddyPress (1.0.0)
    175          *
    176          * @param Object $value Current friendship request object.
    177          */
    178         do_action( 'friends_friendship_after_save', array( &$this ) );
    179 
    180         return $result;
    181     }
    182 
    183     public function delete() {
    184         global $wpdb;
    185 
    186         $bp = buddypress();
    187 
    188         return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->friends->table_name} WHERE id = %d", $this->id ) );
    189     }
    190 
    191     /** Static Methods ********************************************************/
    192 
    193     /**
    194      * Get the IDs of a given user's friends.
    195      *
    196      * @param int $user_id ID of the user whose friends are being retrieved.
    197      * @param bool $friend_requests_only Optional. Whether to fetch
    198      *        unaccepted requests only. Default: false.
    199      * @param bool $assoc_arr Optional. True to receive an array of arrays
    200      *        keyed as 'user_id' => $user_id; false to get a one-dimensional
    201      *        array of user IDs. Default: false.
    202      */
    203     public static function get_friend_user_ids( $user_id, $friend_requests_only = false, $assoc_arr = false ) {
    204         global $wpdb;
    205 
    206         if ( !empty( $friend_requests_only ) ) {
    207             $oc_sql = 'AND is_confirmed = 0';
    208             $friend_sql = $wpdb->prepare( " WHERE friend_user_id = %d", $user_id );
    209         } else {
    210             $oc_sql = 'AND is_confirmed = 1';
    211             $friend_sql = $wpdb->prepare( " WHERE (initiator_user_id = %d OR friend_user_id = %d)", $user_id, $user_id );
    212         }
    213 
    214         $bp = buddypress();
    215         $friends = $wpdb->get_results( "SELECT friend_user_id, initiator_user_id FROM {$bp->friends->table_name} {$friend_sql} {$oc_sql} ORDER BY date_created DESC" );
    216         $fids = array();
    217 
    218         for ( $i = 0, $count = count( $friends ); $i < $count; ++$i ) {
    219             if ( !empty( $assoc_arr ) ) {
    220                 $fids[] = array( 'user_id' => ( $friends[$i]->friend_user_id == $user_id ) ? $friends[$i]->initiator_user_id : $friends[$i]->friend_user_id );
    221             } else {
    222                 $fids[] = ( $friends[$i]->friend_user_id == $user_id ) ? $friends[$i]->initiator_user_id : $friends[$i]->friend_user_id;
    223             }
    224         }
    225 
    226         return $fids;
    227     }
    228 
    229     /**
    230      * Get the ID of the friendship object, if any, between a pair of users.
    231      *
    232      * @param int $user_id The ID of the first user.
    233      * @param int $friend_id The ID of the second user.
    234      * @return int|bool The ID of the friendship object if found, otherwise
    235      *         false.
    236      */
    237     public static function get_friendship_id( $user_id, $friend_id ) {
    238         global $wpdb;
    239 
    240         $bp = buddypress();
    241 
    242         return $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$bp->friends->table_name} WHERE ( initiator_user_id = %d AND friend_user_id = %d ) OR ( initiator_user_id = %d AND friend_user_id = %d ) AND is_confirmed = 1", $user_id, $friend_id, $friend_id, $user_id ) );
    243     }
    244 
    245     /**
    246      * Get a list of IDs of users who have requested friendship of a given user.
    247      *
    248      * @param int $user_id The ID of the user who has received the
    249      *        friendship requests.
    250      * @return array|bool An array of user IDs, or false if none are found.
    251      */
    252     public static function get_friendship_request_user_ids( $user_id ) {
    253         $friend_requests = wp_cache_get( $user_id, 'bp_friends_requests' );
    254 
    255         if ( false === $friend_requests ) {
    256             global $wpdb;
    257 
    258             $bp = buddypress();
    259 
    260             $friend_requests = $wpdb->get_col( $wpdb->prepare( "SELECT initiator_user_id FROM {$bp->friends->table_name} WHERE friend_user_id = %d AND is_confirmed = 0", $user_id ) );
    261 
    262             wp_cache_set( $user_id, $friend_requests, 'bp_friends_requests' );
    263         }
    264 
    265         return $friend_requests;
    266     }
    267 
    268     /**
    269      * Get a total friend count for a given user.
    270      *
    271      * @param int $user_id Optional. ID of the user whose friendships you
    272      *        are counting. Default: displayed user (if any), otherwise
    273      *        logged-in user.
    274      * @return int Friend count for the user.
    275      */
    276     public static function total_friend_count( $user_id = 0 ) {
    277         global $wpdb;
    278 
    279         if ( empty( $user_id ) )
    280             $user_id = ( bp_displayed_user_id() ) ? bp_displayed_user_id() : bp_loggedin_user_id();
    281 
    282         $bp = buddypress();
    283 
    284         /* This is stored in 'total_friend_count' usermeta.
    285            This function will recalculate, update and return. */
    286 
    287         $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM {$bp->friends->table_name} WHERE (initiator_user_id = %d OR friend_user_id = %d) AND is_confirmed = 1", $user_id, $user_id ) );
    288 
    289         // Do not update meta if user has never had friends
    290         if ( empty( $count ) && !bp_get_user_meta( $user_id, 'total_friend_count', true ) )
    291             return 0;
    292 
    293         bp_update_user_meta( $user_id, 'total_friend_count', (int) $count );
    294 
    295         return absint( $count );
    296     }
    297 
    298     /**
    299      * Search the friends of a user by a search string.
    300      *
    301      * @param string $filter The search string, matched against xprofile
    302      *        fields (if available), or usermeta 'nickname' field.
    303      * @param int $user_id ID of the user whose friends are being searched.
    304      * @param int $limit Optional. Max number of friends to return.
    305      * @param int $page Optional. The page of results to return. Default:
    306      *        null (no pagination - return all results).
    307      * @return array|bool On success, an array: {
    308      *     @type array $friends IDs of friends returned by the query.
    309      *     @type int $count Total number of friends (disregarding
    310      *           pagination) who match the search.
    311      * }. Returns false on failure.
    312      */
    313     public static function search_friends( $filter, $user_id, $limit = null, $page = null ) {
    314         global $wpdb;
    315 
    316         // TODO: Optimize this function.
    317 
    318         if ( empty( $user_id ) )
    319             $user_id = bp_loggedin_user_id();
    320 
    321         // Only search for matching strings at the beginning of the
    322         // name (@todo - figure out why this restriction)
    323         $search_terms_like = bp_esc_like( $filter ) . '%';
    324 
    325         $pag_sql = '';
    326         if ( !empty( $limit ) && !empty( $page ) )
    327             $pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
    328 
    329         if ( !$friend_ids = BP_Friends_Friendship::get_friend_user_ids( $user_id ) )
    330             return false;
    331 
    332         // Get all the user ids for the current user's friends.
    333         $fids = implode( ',', wp_parse_id_list( $friend_ids ) );
    334 
    335         if ( empty( $fids ) )
    336             return false;
    337 
    338         $bp = buddypress();
    339 
    340         // filter the user_ids based on the search criteria.
    341         if ( bp_is_active( 'xprofile' ) ) {
    342             $sql       = $wpdb->prepare( "SELECT DISTINCT user_id FROM {$bp->profile->table_name_data} WHERE user_id IN ({$fids}) AND value LIKE %s {$pag_sql}", $search_terms_like );
    343             $total_sql = $wpdb->prepare( "SELECT COUNT(DISTINCT user_id) FROM {$bp->profile->table_name_data} WHERE user_id IN ({$fids}) AND value LIKE %s", $search_terms_like );
    344         } else {
    345             $sql       = $wpdb->prepare( "SELECT DISTINCT user_id FROM {$wpdb->usermeta} WHERE user_id IN ({$fids}) AND meta_key = 'nickname' AND meta_value LIKE %s' {$pag_sql}", $search_terms_like );
    346             $total_sql = $wpdb->prepare( "SELECT COUNT(DISTINCT user_id) FROM {$wpdb->usermeta} WHERE user_id IN ({$fids}) AND meta_key = 'nickname' AND meta_value LIKE %s", $search_terms_like );
    347         }
    348 
    349         $filtered_friend_ids = $wpdb->get_col( $sql );
    350         $total_friend_ids    = $wpdb->get_var( $total_sql );
    351 
    352         if ( empty( $filtered_friend_ids ) )
    353             return false;
    354 
    355         return array( 'friends' => $filtered_friend_ids, 'total' => (int) $total_friend_ids );
    356     }
    357 
    358     /**
    359      * Check friendship status between two users.
    360      *
    361      * Note that 'pending' means that $initiator_userid has sent a friend
    362      * request to $possible_friend_userid that has not yet been approved,
    363      * while 'awaiting_response' is the other way around ($possible_friend_userid
    364      * sent the initial request)
    365      *
    366      * @param int $initiator_userid The ID of the user who is the initiator
    367      *        of the potential friendship/request.
    368      * @param int $possible_friend_userid The ID of the user who is the
    369      *        recipient of the potential friendship/request.
    370      * @return string The friendship status, from among 'not_friends',
    371      *        'is_friend', 'pending', and 'awaiting_response'.
    372      */
    373     public static function check_is_friend( $initiator_userid, $possible_friend_userid ) {
    374         global $wpdb;
    375 
    376         if ( empty( $initiator_userid ) || empty( $possible_friend_userid ) ) {
    377             return false;
    378         }
    379 
    380         $bp = buddypress();
    381 
    382         $result = $wpdb->get_results( $wpdb->prepare( "SELECT id, initiator_user_id, is_confirmed FROM {$bp->friends->table_name} WHERE (initiator_user_id = %d AND friend_user_id = %d) OR (initiator_user_id = %d AND friend_user_id = %d)", $initiator_userid, $possible_friend_userid, $possible_friend_userid, $initiator_userid ) );
    383 
    384         if ( ! empty( $result ) ) {
    385             if ( 0 == (int) $result[0]->is_confirmed ) {
    386                 $status = $initiator_userid == $result[0]->initiator_user_id ? 'pending' : 'awaiting_response';
    387             } else {
    388                 $status = 'is_friend';
    389             }
    390         } else {
    391             $status = 'not_friends';
    392         }
    393 
    394         return $status;
    395     }
    396 
    397     /**
    398      * Get the last active date of many users at once.
    399      *
    400      * @todo Why is this in the Friends component?
    401      *
    402      * @param array $user_ids IDs of users whose last_active meta is
    403      *        being queried.
    404      * @return array Array of last_active values + user_ids.
    405      */
    406     public static function get_bulk_last_active( $user_ids ) {
    407         global $wpdb;
    408 
    409         $last_activities = BP_Core_User::get_last_activity( $user_ids );
    410 
    411         // Sort and structure as expected in legacy function
    412         usort( $last_activities, create_function( '$a, $b', '
    413             if ( $a["date_recorded"] == $b["date_recorded"] ) {
    414                 return 0;
    415             }
    416 
    417             return ( strtotime( $a["date_recorded"] ) < strtotime( $b["date_recorded"] ) ) ? 1 : -1;
    418         ' ) );
    419 
    420         $retval = array();
    421         foreach ( $last_activities as $last_activity ) {
    422             $u = new stdClass;
    423             $u->last_activity = $last_activity['date_recorded'];
    424             $u->user_id       = $last_activity['user_id'];
    425 
    426             $retval[] = $u;
    427         }
    428 
    429         return $retval;
    430     }
    431 
    432     /**
    433      * Mark a friendship as accepted.
    434      *
    435      * @param int $friendship_id ID of the friendship to be accepted.
    436      * @return int Number of database rows updated.
    437      */
    438     public static function accept($friendship_id) {
    439         global $wpdb;
    440 
    441         $bp = buddypress();
    442 
    443         return $wpdb->query( $wpdb->prepare( "UPDATE {$bp->friends->table_name} SET is_confirmed = 1, date_created = %s WHERE id = %d AND friend_user_id = %d", bp_core_current_time(), $friendship_id, bp_loggedin_user_id() ) );
    444     }
    445 
    446     /**
    447      * Remove a friendship or a friendship request INITIATED BY the logged-in user.
    448      *
    449      * @param int $friendship_id ID of the friendship to be withdrawn.
    450      * @return int Number of database rows deleted.
    451      */
    452     public static function withdraw($friendship_id) {
    453         global $wpdb;
    454 
    455         $bp = buddypress();
    456 
    457         return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->friends->table_name} WHERE id = %d AND initiator_user_id = %d", $friendship_id, bp_loggedin_user_id() ) );
    458     }
    459 
    460     /**
    461      * Remove a friendship or a friendship request MADE OF the logged-in user.
    462      *
    463      * @param int $friendship_id ID of the friendship to be rejected.
    464      * @return int Number of database rows deleted.
    465      */
    466     public static function reject($friendship_id) {
    467         global $wpdb;
    468 
    469         $bp = buddypress();
    470 
    471         return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->friends->table_name} WHERE id = %d AND friend_user_id = %d", $friendship_id, bp_loggedin_user_id() ) );
    472     }
    473 
    474     /**
    475      * Search users.
    476      *
    477      * @todo Why does this exist, and why is it in bp-friends?
    478      *
    479      * @param string $filter String to search by.
    480      * @param int $user_id A user ID param that is unused.
    481      * @param int $limit Optional. Max number of records to return.
    482      * @param int $page Optional. Number of the page to return. Default:
    483      *        false (no pagination - return all results).
    484      * @return array $filtered_ids IDs of users who match the query.
    485      */
    486     public static function search_users( $filter, $user_id, $limit = null, $page = null ) {
    487         global $wpdb;
    488 
    489         // Only search for matching strings at the beginning of the
    490         // name (@todo - figure out why this restriction)
    491         $search_terms_like = bp_esc_like( $filter ) . '%';
    492 
    493         $usermeta_table = $wpdb->base_prefix . 'usermeta';
    494         $users_table    = $wpdb->base_prefix . 'users';
    495 
    496         $pag_sql = '';
    497         if ( !empty( $limit ) && !empty( $page ) )
    498             $pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * intval( $limit ) ), intval( $limit ) );
    499 
    500         $bp = buddypress();
    501 
    502         // filter the user_ids based on the search criteria.
    503         if ( bp_is_active( 'xprofile' ) ) {
    504             $sql = $wpdb->prepare( "SELECT DISTINCT d.user_id as id FROM {$bp->profile->table_name_data} d, {$users_table} u WHERE d.user_id = u.id AND d.value LIKE %s ORDER BY d.value DESC {$pag_sql}", $search_terms_like );
    505         } else {
    506             $sql = $wpdb->prepare( "SELECT DISTINCT user_id as id FROM {$usermeta_table} WHERE meta_value LIKE %s ORDER BY d.value DESC {$pag_sql}", $search_terms_like );
    507         }
    508 
    509         $filtered_fids = $wpdb->get_col($sql);
    510 
    511         if ( empty( $filtered_fids ) )
    512             return false;
    513 
    514         return $filtered_fids;
    515     }
    516 
    517     /**
    518      * Get a count of users who match a search term.
    519      *
    520      * @todo Why does this exist, and why is it in bp-friends?
    521      *
    522      * @param string $filter Search term.
    523      * @return int Count of users matching the search term.
    524      */
    525     public static function search_users_count( $filter ) {
    526         global $wpdb;
    527 
    528         // Only search for matching strings at the beginning of the
    529         // name (@todo - figure out why this restriction)
    530         $search_terms_like = bp_esc_like( $filter ) . '%';
    531 
    532         $usermeta_table = $wpdb->prefix . 'usermeta';
    533         $users_table    = $wpdb->base_prefix . 'users';
    534 
    535         $bp = buddypress();
    536 
    537         // filter the user_ids based on the search criteria.
    538         if ( bp_is_active( 'xprofile' ) ) {
    539             $sql = $wpdb->prepare( "SELECT COUNT(DISTINCT d.user_id) FROM {$bp->profile->table_name_data} d, {$users_table} u WHERE d.user_id = u.id AND d.value LIKE %s", $search_terms_like );
    540         } else {
    541             $sql = $wpdb->prepare( "SELECT COUNT(DISTINCT user_id) FROM {$usermeta_table} WHERE meta_value LIKE %s", $search_terms_like );
    542         }
    543 
    544         $user_count = $wpdb->get_col($sql);
    545 
    546         if ( empty( $user_count ) )
    547             return false;
    548 
    549         return $user_count[0];
    550     }
    551 
    552     /**
    553      * Sort a list of user IDs by their display names.
    554      *
    555      * @todo Why does this exist, and why is it in bp-friends?
    556      *
    557      * @param array $user_ids Array of user IDs.
    558      * @return array User IDs, sorted by the associated display names.
    559      */
    560     public static function sort_by_name( $user_ids ) {
    561         global $wpdb;
    562 
    563         if ( !bp_is_active( 'xprofile' ) )
    564             return false;
    565 
    566         $bp = buddypress();
    567 
    568         $user_ids = implode( ',', wp_parse_id_list( $user_ids ) );
    569 
    570         return $wpdb->get_results( $wpdb->prepare( "SELECT user_id FROM {$bp->profile->table_name_data} pd, {$bp->profile->table_name_fields} pf WHERE pf.id = pd.field_id AND pf.name = %s AND pd.user_id IN ( {$user_ids} ) ORDER BY pd.value ASC", bp_xprofile_fullname_field_name() ) );
    571     }
    572 
    573     /**
    574      * Get a list of random friend IDs.
    575      *
    576      * @param int $user_id ID of the user whose friends are being retrieved.
    577      * @param int $total_friends Optional. Number of random friends to get.
    578      *        Default: 5.
    579      * @return array|bool An array of random friend user IDs on success;
    580      *         false if none are found.
    581      */
    582     public static function get_random_friends( $user_id, $total_friends = 5 ) {
    583         global $wpdb;
    584 
    585         $bp      = buddypress();
    586         $fids    = array();
    587         $sql     = $wpdb->prepare( "SELECT friend_user_id, initiator_user_id FROM {$bp->friends->table_name} WHERE (friend_user_id = %d || initiator_user_id = %d) && is_confirmed = 1 ORDER BY rand() LIMIT %d", $user_id, $user_id, $total_friends );
    588         $results = $wpdb->get_results( $sql );
    589 
    590         for ( $i = 0, $count = count( $results ); $i < $count; ++$i ) {
    591             $fids[] = ( $results[$i]->friend_user_id == $user_id ) ? $results[$i]->initiator_user_id : $results[$i]->friend_user_id;
    592         }
    593 
    594         // remove duplicates
    595         if ( count( $fids ) > 0 )
    596             return array_flip( array_flip( $fids ) );
    597         else
    598             return false;
    599     }
    600 
    601     /**
    602      * Get a count of a user's friends who can be invited to a given group.
    603      *
    604      * Users can invite any of their friends except:
    605      *
    606      * - users who are already in the group
    607      * - users who have a pending invite to the group
    608      * - users who have been banned from the group
    609      *
    610      * @param int $user_id ID of the user whose friends are being counted.
    611      * @param int $group_id ID of the group friends are being invited to.
    612      * @return int $invitable_count Eligible friend count.
    613      */
    614     public static function get_invitable_friend_count( $user_id, $group_id ) {
    615 
    616         // Setup some data we'll use below
    617         $is_group_admin  = BP_Groups_Member::check_is_admin( $user_id, $group_id );
    618         $friend_ids      = BP_Friends_Friendship::get_friend_user_ids( $user_id );
    619         $invitable_count = 0;
    620 
    621         for ( $i = 0, $count = count( $friend_ids ); $i < $count; ++$i ) {
    622 
    623             // If already a member, they cannot be invited again
    624             if ( BP_Groups_Member::check_is_member( (int) $friend_ids[$i], $group_id ) )
    625                 continue;
    626 
    627             // If user already has invite, they cannot be added
    628             if ( BP_Groups_Member::check_has_invite( (int) $friend_ids[$i], $group_id )  )
    629                 continue;
    630 
    631             // If user is not group admin and friend is banned, they cannot be invited
    632             if ( ( false === $is_group_admin ) && BP_Groups_Member::check_is_banned( (int) $friend_ids[$i], $group_id ) )
    633                 continue;
    634 
    635             $invitable_count++;
    636         }
    637 
    638         return $invitable_count;
    639     }
    640 
    641     /**
    642      * Get the friend user IDs for a given friendship.
    643      *
    644      * @param int $friendship_id ID of the friendship.
    645      * @return object friend_user_id and initiator_user_id.
    646      */
    647     public static function get_user_ids_for_friendship( $friendship_id ) {
    648         global $wpdb;
    649 
    650         $bp = buddypress();
    651 
    652         return $wpdb->get_row( $wpdb->prepare( "SELECT friend_user_id, initiator_user_id FROM {$bp->friends->table_name} WHERE id = %d", $friendship_id ) );
    653     }
    654 
    655     /**
    656      * Delete all friendships and friend notifications related to a user.
    657      *
    658      * @param int $user_id ID of the user being expunged.
    659      */
    660     public static function delete_all_for_user( $user_id ) {
    661         global $wpdb;
    662 
    663         $bp = buddypress();
    664 
    665         // Get friends of $user_id
    666         $friend_ids = BP_Friends_Friendship::get_friend_user_ids( $user_id );
    667 
    668         // Delete all friendships related to $user_id
    669         $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->friends->table_name} WHERE friend_user_id = %d OR initiator_user_id = %d", $user_id, $user_id ) );
    670 
    671         // Delete friend request notifications for members who have a
    672         // notification from this user.
    673         if ( bp_is_active( 'notifications' ) ) {
    674             $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->notifications->table_name} WHERE component_name = 'friends' AND ( component_action = 'friendship_request' OR component_action = 'friendship_accepted' ) AND item_id = %d", $user_id ) );
    675         }
    676 
    677         // Loop through friend_ids and update their counts
    678         foreach ( (array) $friend_ids as $friend_id ) {
    679             BP_Friends_Friendship::total_friend_count( $friend_id );
    680         }
    681     }
    682 }
     12require __DIR__ . '/classes/class-bp-friends-friendship.php';
  • trunk/src/bp-groups/bp-groups-classes.php

    r9471 r9485  
    11<?php
    2 
    32/**
    43 * BuddyPress Groups Classes
     
    1110defined( 'ABSPATH' ) || exit;
    1211
    13 /**
    14  * BuddyPress Group object.
    15  */
    16 class BP_Groups_Group {
    17 
    18     /**
    19      * ID of the group.
    20      *
    21      * @access public
    22      * @var int
    23      */
    24     public $id;
    25 
    26     /**
    27      * User ID of the group's creator.
    28      *
    29      * @access public
    30      * @var int
    31      */
    32     public $creator_id;
    33 
    34     /**
    35      * Name of the group.
    36      *
    37      * @access public
    38      * @var string
    39      */
    40     public $name;
    41 
    42     /**
    43      * Group slug.
    44      *
    45      * @access public
    46      * @var string
    47      */
    48     public $slug;
    49 
    50     /**
    51      * Group description.
    52      *
    53      * @access public
    54      * @var string
    55      */
    56     public $description;
    57 
    58     /**
    59      * Group status.
    60      *
    61      * Core statuses are 'public', 'private', and 'hidden'.
    62      *
    63      * @access public
    64      * @var string
    65      */
    66     public $status;
    67 
    68     /**
    69      * Should (legacy) bbPress forums be enabled for this group?
    70      *
    71      * @access public
    72      * @var int
    73      */
    74     public $enable_forum;
    75 
    76     /**
    77      * Date the group was created.
    78      *
    79      * @access public
    80      * @var string
    81      */
    82     public $date_created;
    83 
    84     /**
    85      * Data about the group's admins.
    86      *
    87      * @access public
    88      * @var array
    89      */
    90     public $admins;
    91 
    92     /**
    93      * Data about the group's moderators.
    94      *
    95      * @access public
    96      * @var array
    97      */
    98     public $mods;
    99 
    100     /**
    101      * Total count of group members.
    102      *
    103      * @access public
    104      * @var int
    105      */
    106     public $total_member_count;
    107 
    108     /**
    109      * Is the current user a member of this group?
    110      *
    111      * @since BuddyPress (1.2.0)
    112      * @var bool
    113      */
    114     public $is_member;
    115 
    116     /**
    117      * Does the current user have an outstanding invitation to this group?
    118      *
    119      * @since BuddyPress (1.9.0)
    120      * @var bool
    121      */
    122     public $is_invited;
    123 
    124     /**
    125      * Does the current user have a pending membership request to this group?
    126      *
    127      * @since BuddyPress (1.9.0)
    128      * @var bool
    129      */
    130     public $is_pending;
    131 
    132     /**
    133      * Timestamp of the last activity that happened in this group.
    134      *
    135      * @since BuddyPress (1.2.0)
    136      * @var string
    137      */
    138     public $last_activity;
    139 
    140     /**
    141      * If this is a private or hidden group, does the current user have access?
    142      *
    143      * @since BuddyPress (1.6.0)
    144      * @var bool
    145      */
    146     public $user_has_access;
    147 
    148     /**
    149      * Raw arguments passed to the constructor.
    150      *
    151      * @since BuddyPress (2.0.0)
    152      * @var array
    153      */
    154     public $args;
    155 
    156     /**
    157      * Constructor method.
    158      *
    159      * @param int $id Optional. If the ID of an existing group is provided,
    160      *        the object will be pre-populated with info about that group.
    161      * @param array $args {
    162      *     Array of optional arguments.
    163      *     @type bool $populate_extras Whether to fetch "extra" data about
    164      *           the group (group admins/mods, access for the current user).
    165      *           Default: false.
    166      * }
    167      */
    168     public function __construct( $id = null, $args = array() ) {
    169         $this->args = wp_parse_args( $args, array(
    170             'populate_extras' => false,
    171         ) );
    172 
    173         if ( !empty( $id ) ) {
    174             $this->id = $id;
    175             $this->populate();
    176         }
    177     }
    178 
    179     /**
    180      * Set up data about the current group.
    181      */
    182     public function populate() {
    183         global $wpdb;
    184 
    185         // Get BuddyPress
    186         $bp    = buddypress();
    187 
    188         // Check cache for group data
    189         $group = wp_cache_get( $this->id, 'bp_groups' );
    190 
    191         // Cache missed, so query the DB
    192         if ( false === $group ) {
    193             $group = $wpdb->get_row( $wpdb->prepare( "SELECT g.* FROM {$bp->groups->table_name} g WHERE g.id = %d", $this->id ) );
    194 
    195             wp_cache_set( $this->id, $group, 'bp_groups' );
    196         }
    197 
    198         // No group found so set the ID and bail
    199         if ( empty( $group ) || is_wp_error( $group ) ) {
    200             $this->id = 0;
    201             return;
    202         }
    203 
    204         // Group found so setup the object variables
    205         $this->id           = $group->id;
    206         $this->creator_id   = $group->creator_id;
    207         $this->name         = stripslashes( $group->name );
    208         $this->slug         = $group->slug;
    209         $this->description  = stripslashes( $group->description );
    210         $this->status       = $group->status;
    211         $this->enable_forum = $group->enable_forum;
    212         $this->date_created = $group->date_created;
    213 
    214         // Are we getting extra group data?
    215         if ( ! empty( $this->args['populate_extras'] ) ) {
    216 
    217             // Get group admins and mods
    218             $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 ) ) );
    219 
    220             // Add admins and moderators to their respective arrays
    221             foreach ( (array) $admin_mods as $user ) {
    222                 if ( !empty( $user->is_admin ) ) {
    223                     $this->admins[] = $user;
    224                 } else {
    225                     $this->mods[] = $user;
    226                 }
    227             }
    228 
    229             // Set up some specific group vars from meta. Excluded
    230             // from the bp_groups cache because it's cached independently
    231             $this->last_activity      = groups_get_groupmeta( $this->id, 'last_activity' );
    232             $this->total_member_count = groups_get_groupmeta( $this->id, 'total_member_count' );
    233 
    234             // Set user-specific data
    235             $user_id          = bp_loggedin_user_id();
    236             $this->is_member  = BP_Groups_Member::check_is_member( $user_id, $this->id );
    237             $this->is_invited = BP_Groups_Member::check_has_invite( $user_id, $this->id );
    238             $this->is_pending = BP_Groups_Member::check_for_membership_request( $user_id, $this->id );
    239 
    240             // If this is a private or hidden group, does the current user have access?
    241             if ( ( 'private' === $this->status ) || ( 'hidden' === $this->status ) ) {
    242 
    243                 // Assume user does not have access to hidden/private groups
    244                 $this->user_has_access = false;
    245 
    246                 // Group members or community moderators have access
    247                 if ( ( $this->is_member && is_user_logged_in() ) || bp_current_user_can( 'bp_moderate' ) ) {
    248                     $this->user_has_access = true;
    249                 }
    250             } else {
    251                 $this->user_has_access = true;
    252             }
    253         }
    254     }
    255 
    256     /**
    257      * Save the current group to the database.
    258      *
    259      * @return bool True on success, false on failure.
    260      */
    261     public function save() {
    262         global $wpdb;
    263 
    264         $bp = buddypress();
    265 
    266         $this->creator_id   = apply_filters( 'groups_group_creator_id_before_save',   $this->creator_id,   $this->id );
    267         $this->name         = apply_filters( 'groups_group_name_before_save',         $this->name,         $this->id );
    268         $this->slug         = apply_filters( 'groups_group_slug_before_save',         $this->slug,         $this->id );
    269         $this->description  = apply_filters( 'groups_group_description_before_save',  $this->description,  $this->id );
    270         $this->status       = apply_filters( 'groups_group_status_before_save',       $this->status,       $this->id );
    271         $this->enable_forum = apply_filters( 'groups_group_enable_forum_before_save', $this->enable_forum, $this->id );
    272         $this->date_created = apply_filters( 'groups_group_date_created_before_save', $this->date_created, $this->id );
    273 
    274         do_action_ref_array( 'groups_group_before_save', array( &$this ) );
    275 
    276         // Groups need at least a name
    277         if ( empty( $this->name ) ) {
    278             return false;
    279         }
    280 
    281         // Set slug with group title if not passed
    282         if ( empty( $this->slug ) ) {
    283             $this->slug = sanitize_title( $this->name );
    284         }
    285 
    286         // Sanity check
    287         if ( empty( $this->slug ) ) {
    288             return false;
    289         }
    290 
    291         // Check for slug conflicts if creating new group
    292         if ( empty( $this->id ) ) {
    293             $this->slug = groups_check_slug( $this->slug );
    294         }
    295 
    296         if ( !empty( $this->id ) ) {
    297             $sql = $wpdb->prepare(
    298                 "UPDATE {$bp->groups->table_name} SET
    299                     creator_id = %d,
    300                     name = %s,
    301                     slug = %s,
    302                     description = %s,
    303                     status = %s,
    304                     enable_forum = %d,
    305                     date_created = %s
    306                 WHERE
    307                     id = %d
    308                 ",
    309                     $this->creator_id,
    310                     $this->name,
    311                     $this->slug,
    312                     $this->description,
    313                     $this->status,
    314                     $this->enable_forum,
    315                     $this->date_created,
    316                     $this->id
    317             );
    318         } else {
    319             $sql = $wpdb->prepare(
    320                 "INSERT INTO {$bp->groups->table_name} (
    321                     creator_id,
    322                     name,
    323                     slug,
    324                     description,
    325                     status,
    326                     enable_forum,
    327                     date_created
    328                 ) VALUES (
    329                     %d, %s, %s, %s, %s, %d, %s
    330                 )",
    331                     $this->creator_id,
    332                     $this->name,
    333                     $this->slug,
    334                     $this->description,
    335                     $this->status,
    336                     $this->enable_forum,
    337                     $this->date_created
    338             );
    339         }
    340 
    341         if ( false === $wpdb->query($sql) )
    342             return false;
    343 
    344         if ( empty( $this->id ) )
    345             $this->id = $wpdb->insert_id;
    346 
    347         do_action_ref_array( 'groups_group_after_save', array( &$this ) );
    348 
    349         wp_cache_delete( $this->id, 'bp_groups' );
    350 
    351         return true;
    352     }
    353 
    354     /**
    355      * Delete the current group.
    356      *
    357      * @return bool True on success, false on failure.
    358      */
    359     public function delete() {
    360         global $wpdb;
    361 
    362         // Delete groupmeta for the group
    363         groups_delete_groupmeta( $this->id );
    364 
    365         // Fetch the user IDs of all the members of the group
    366         $user_ids    = BP_Groups_Member::get_group_member_ids( $this->id );
    367         $user_id_str = esc_sql( implode( ',', wp_parse_id_list( $user_ids ) ) );
    368 
    369         // Modify group count usermeta for members
    370         $wpdb->query( "UPDATE {$wpdb->usermeta} SET meta_value = meta_value - 1 WHERE meta_key = 'total_group_count' AND user_id IN ( {$user_id_str} )" );
    371 
    372         // Now delete all group member entries
    373         BP_Groups_Member::delete_all( $this->id );
    374 
    375         do_action_ref_array( 'bp_groups_delete_group', array( &$this, $user_ids ) );
    376 
    377         wp_cache_delete( $this->id, 'bp_groups' );
    378 
    379         $bp = buddypress();
    380 
    381         // Finally remove the group entry from the DB
    382         if ( !$wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->groups->table_name} WHERE id = %d", $this->id ) ) )
    383             return false;
    384 
    385         return true;
    386     }
    387 
    388     /** Static Methods ****************************************************/
    389 
    390     /**
    391      * Get whether a group exists for a given slug.
    392      *
    393      * @param string $slug Slug to check.
    394      * @param string $table_name Optional. Name of the table to check
    395      *        against. Default: $bp->groups->table_name.
    396      * @return string|null ID of the group, if one is found, else null.
    397      */
    398     public static function group_exists( $slug, $table_name = false ) {
    399         global $wpdb;
    400 
    401         if ( empty( $table_name ) )
    402             $table_name = buddypress()->groups->table_name;
    403 
    404         if ( empty( $slug ) )
    405             return false;
    406 
    407         return $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$table_name} WHERE slug = %s", strtolower( $slug ) ) );
    408     }
    409 
    410     /**
    411      * Get the ID of a group by the group's slug.
    412      *
    413      * Alias of {@link BP_Groups_Group::group_exists()}.
    414      *
    415      * @param string $slug See {@link BP_Groups_Group::group_exists()}.
    416      * @return string|null See {@link BP_Groups_Group::group_exists()}.
    417      */
    418     public static function get_id_from_slug( $slug ) {
    419         return BP_Groups_Group::group_exists( $slug );
    420     }
    421 
    422     /**
    423      * Get IDs of users with outstanding invites to a given group from a specified user.
    424      *
    425      * @param int $user_id ID of the inviting user.
    426      * @param int $group_id ID of the group.
    427      * @return array IDs of users who have been invited to the group by the
    428      *         user but have not yet accepted.
    429      */
    430     public static function get_invites( $user_id, $group_id ) {
    431         global $wpdb;
    432 
    433         $bp = buddypress();
    434 
    435         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 ) );
    436     }
    437 
    438     /**
    439      * Get a list of a user's groups, filtered by a search string.
    440      *
    441      * @param string $filter Search term. Matches against 'name' and
    442      *        'description' fields.
    443      * @param int $user_id ID of the user whose groups are being searched.
    444      *        Default: the displayed user.
    445      * @param mixed $order Not used.
    446      * @param int $limit Optional. The max number of results to return.
    447      *        Default: null (no limit).
    448      * @param int $page Optional. The page offset of results to return.
    449      *        Default: null (no limit).
    450      * @return array {
    451      *     @type array $groups Array of matched and paginated group objects.
    452      *     @type int $total Total count of groups matching the query.
    453      * }
    454      */
    455     public static function filter_user_groups( $filter, $user_id = 0, $order = false, $limit = null, $page = null ) {
    456         global $wpdb;
    457 
    458         if ( empty( $user_id ) )
    459             $user_id = bp_displayed_user_id();
    460 
    461         $search_terms_like = bp_esc_like( $filter ) . '%';
    462 
    463         $pag_sql = $order_sql = $hidden_sql = '';
    464 
    465         if ( !empty( $limit ) && !empty( $page ) )
    466             $pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
    467 
    468         // Get all the group ids for the current user's groups.
    469         $gids = BP_Groups_Member::get_group_ids( $user_id );
    470 
    471         if ( empty( $gids['groups'] ) )
    472             return false;
    473 
    474         $bp = buddypress();
    475 
    476         $gids = esc_sql( implode( ',', wp_parse_id_list( $gids['groups'] ) ) );
    477 
    478         $paged_groups = $wpdb->get_results( $wpdb->prepare( "SELECT id as group_id FROM {$bp->groups->table_name} WHERE ( name LIKE %s OR description LIKE %s ) AND id IN ({$gids}) {$pag_sql}", $search_terms_like, $search_terms_like ) );
    479         $total_groups = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM {$bp->groups->table_name} WHERE ( name LIKE %s OR description LIKE %s ) AND id IN ({$gids})", $search_terms_like, $search_terms_like ) );
    480 
    481         return array( 'groups' => $paged_groups, 'total' => $total_groups );
    482     }
    483 
    484     /**
    485      * Get a list of groups, filtered by a search string.
    486      *
    487      * @param string $filter Search term. Matches against 'name' and
    488      *        'description' fields.
    489      * @param int $limit Optional. The max number of results to return.
    490      *        Default: null (no limit).
    491      * @param int $page Optional. The page offset of results to return.
    492      *        Default: null (no limit).
    493      * @param string $sort_by Column to sort by. Default: false (default
    494      *        sort).
    495      * @param string $order ASC or DESC. Default: false (default sort).
    496      * @return array {
    497      *     @type array $groups Array of matched and paginated group objects.
    498      *     @type int $total Total count of groups matching the query.
    499      * }
    500      */
    501     public static function search_groups( $filter, $limit = null, $page = null, $sort_by = false, $order = false ) {
    502         global $wpdb;
    503 
    504         $search_terms_like = '%' . bp_esc_like( $filter ) . '%';
    505 
    506         $pag_sql = $order_sql = $hidden_sql = '';
    507 
    508         if ( !empty( $limit ) && !empty( $page ) )
    509             $pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
    510 
    511         if ( !empty( $sort_by ) && !empty( $order ) ) {
    512             $sort_by   = esc_sql( $sort_by );
    513             $order     = esc_sql( $order );
    514             $order_sql = "ORDER BY {$sort_by} {$order}";
    515         }
    516 
    517         if ( !bp_current_user_can( 'bp_moderate' ) )
    518             $hidden_sql = "AND status != 'hidden'";
    519 
    520         $bp = buddypress();
    521 
    522         $paged_groups = $wpdb->get_results( $wpdb->prepare( "SELECT id as group_id FROM {$bp->groups->table_name} WHERE ( name LIKE %s OR description LIKE %s ) {$hidden_sql} {$order_sql} {$pag_sql}", $search_terms_like, $search_terms_like ) );
    523         $total_groups = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM {$bp->groups->table_name} WHERE ( name LIKE %s OR description LIKE %s ) {$hidden_sql}", $search_terms_like, $search_terms_like ) );
    524 
    525         return array( 'groups' => $paged_groups, 'total' => $total_groups );
    526     }
    527 
    528     /**
    529      * Check for the existence of a slug.
    530      *
    531      * @param string $slug Slug to check.
    532      * @return string|null The slug, if found. Otherwise null.
    533      */
    534     public static function check_slug( $slug ) {
    535         global $wpdb;
    536 
    537         $bp = buddypress();
    538 
    539         return $wpdb->get_var( $wpdb->prepare( "SELECT slug FROM {$bp->groups->table_name} WHERE slug = %s", $slug ) );
    540     }
    541 
    542     /**
    543      * Get the slug for a given group ID.
    544      *
    545      * @param int $group_id ID of the group.
    546      * @return string|null The slug, if found. Otherwise null.
    547      */
    548     public static function get_slug( $group_id ) {
    549         global $wpdb;
    550 
    551         $bp = buddypress();
    552 
    553         return $wpdb->get_var( $wpdb->prepare( "SELECT slug FROM {$bp->groups->table_name} WHERE id = %d", $group_id ) );
    554     }
    555 
    556     /**
    557      * Check whether a given group has any members.
    558      *
    559      * @param int $group_id ID of the group.
    560      * @return bool True if the group has members, otherwise false.
    561      */
    562     public static function has_members( $group_id ) {
    563         global $wpdb;
    564 
    565         $bp = buddypress();
    566 
    567         $members = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM {$bp->groups->table_name_members} WHERE group_id = %d", $group_id ) );
    568 
    569         if ( empty( $members ) )
    570             return false;
    571 
    572         return true;
    573     }
    574 
    575     /**
    576      * Check whether a group has outstanding membership requests.
    577      *
    578      * @param int $group_id ID of the group.
    579      * @return int|null The number of outstanding requests, or null if
    580      *         none are found.
    581      */
    582     public static function has_membership_requests( $group_id ) {
    583         global $wpdb;
    584 
    585         $bp = buddypress();
    586 
    587         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 ) );
    588     }
    589 
    590     /**
    591      * Get outstanding membership requests for a group.
    592      *
    593      * @param int $group_id ID of the group.
    594      * @param int $limit Optional. Max number of results to return.
    595      *        Default: null (no limit).
    596      * @param int $page Optional. Page offset of results returned. Default:
    597      *        null (no limit).
    598      * @return array {
    599      *     @type array $requests The requested page of located requests.
    600      *     @type int $total Total number of requests outstanding for the
    601      *           group.
    602      * }
    603      */
    604     public static function get_membership_requests( $group_id, $limit = null, $page = null ) {
    605         global $wpdb;
    606 
    607         if ( !empty( $limit ) && !empty( $page ) ) {
    608             $pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
    609         }
    610 
    611         $bp = buddypress();
    612 
    613         $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 ) );
    614         $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 ) );
    615 
    616         return array( 'requests' => $paged_requests, 'total' => $total_requests );
    617     }
    618 
    619     /**
    620      * Query for groups.
    621      *
    622      * @see WP_Meta_Query::queries for a description of the 'meta_query'
    623      *      parameter format.
    624      *
    625      * @param array {
    626      *     Array of parameters. All items are optional.
    627      *     @type string $type Optional. Shorthand for certain orderby/
    628      *           order combinations. 'newest', 'active', 'popular',
    629      *           'alphabetical', 'random'. When present, will override
    630      *           orderby and order params. Default: null.
    631      *     @type string $orderby Optional. Property to sort by.
    632      *           'date_created', 'last_activity', 'total_member_count',
    633      *           'name', 'random'. Default: 'date_created'.
    634      *     @type string $order Optional. Sort order. 'ASC' or 'DESC'.
    635      *           Default: 'DESC'.
    636      *     @type int $per_page Optional. Number of items to return per page
    637      *           of results. Default: null (no limit).
    638      *     @type int $page Optional. Page offset of results to return.
    639      *           Default: null (no limit).
    640      *     @type int $user_id Optional. If provided, results will be limited
    641      *           to groups of which the specified user is a member. Default:
    642      *           null.
    643      *     @type string $search_terms Optional. If provided, only groups
    644      *           whose names or descriptions match the search terms will be
    645      *           returned. Default: false.
    646      *     @type array $meta_query Optional. An array of meta_query
    647      *           conditions. See {@link WP_Meta_Query::queries} for
    648      *           description.
    649      *     @type array|string Optional. Array or comma-separated list of
    650      *           group IDs. Results will be limited to groups within the
    651      *           list. Default: false.
    652      *     @type bool $populate_extras Whether to fetch additional
    653      *           information (such as member count) about groups. Default:
    654      *           true.
    655      *     @type array|string $exclude Optional. Array or comma-separated
    656      *           list of group IDs. Results will exclude the listed groups.
    657      *           Default: false.
    658      *     @type bool $update_meta_cache Whether to pre-fetch groupmeta for
    659      *           the returned groups. Default: true.
    660      *     @type bool $show_hidden Whether to include hidden groups in
    661      *           results. Default: false.
    662      * }
    663      * @return array {
    664      *     @type array $groups Array of group objects returned by the
    665      *           paginated query.
    666      *     @type int $total Total count of all groups matching non-
    667      *           paginated query params.
    668      * }
    669      */
    670     public static function get( $args = array() ) {
    671         global $wpdb;
    672 
    673         // Backward compatibility with old method of passing arguments
    674         if ( ! is_array( $args ) || func_num_args() > 1 ) {
    675             _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__ ) );
    676 
    677             $old_args_keys = array(
    678                 0 => 'type',
    679                 1 => 'per_page',
    680                 2 => 'page',
    681                 3 => 'user_id',
    682                 4 => 'search_terms',
    683                 5 => 'include',
    684                 6 => 'populate_extras',
    685                 7 => 'exclude',
    686                 8 => 'show_hidden',
    687             );
    688 
    689             $func_args = func_get_args();
    690             $args      = bp_core_parse_args_array( $old_args_keys, $func_args );
    691         }
    692 
    693         $defaults = array(
    694             'type'              => null,
    695             'orderby'           => 'date_created',
    696             'order'             => 'DESC',
    697             'per_page'          => null,
    698             'page'              => null,
    699             'user_id'           => 0,
    700             'search_terms'      => false,
    701             'meta_query'        => false,
    702             'include'           => false,
    703             'populate_extras'   => true,
    704             'update_meta_cache' => true,
    705             'exclude'           => false,
    706             'show_hidden'       => false,
    707         );
    708 
    709         $r = wp_parse_args( $args, $defaults );
    710 
    711         $bp = buddypress();
    712 
    713         $sql       = array();
    714         $total_sql = array();
    715 
    716         $sql['select'] = "SELECT DISTINCT g.id, g.*, gm1.meta_value AS total_member_count, gm2.meta_value AS last_activity";
    717         $sql['from']   = " FROM {$bp->groups->table_name_groupmeta} gm1, {$bp->groups->table_name_groupmeta} gm2,";
    718 
    719         if ( ! empty( $r['user_id'] ) ) {
    720             $sql['members_from'] = " {$bp->groups->table_name_members} m,";
    721         }
    722 
    723         $sql['group_from'] = " {$bp->groups->table_name} g WHERE";
    724 
    725         if ( ! empty( $r['user_id'] ) ) {
    726             $sql['user_where'] = " g.id = m.group_id AND";
    727         }
    728 
    729         $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'";
    730 
    731         if ( empty( $r['show_hidden'] ) ) {
    732             $sql['hidden'] = " AND g.status != 'hidden'";
    733         }
    734 
    735         if ( ! empty( $r['search_terms'] ) ) {
    736             $search_terms_like = '%' . bp_esc_like( $r['search_terms'] ) . '%';
    737             $sql['search'] = $wpdb->prepare( " AND ( g.name LIKE %s OR g.description LIKE %s )", $search_terms_like, $search_terms_like );
    738         }
    739 
    740         $meta_query_sql = self::get_meta_query_sql( $r['meta_query'] );
    741 
    742         if ( ! empty( $meta_query_sql['join'] ) ) {
    743             $sql['from'] .= $meta_query_sql['join'];
    744         }
    745 
    746         if ( ! empty( $meta_query_sql['where'] ) ) {
    747             $sql['meta'] = $meta_query_sql['where'];
    748         }
    749 
    750         if ( ! empty( $r['user_id'] ) ) {
    751             $sql['user'] = $wpdb->prepare( " AND m.user_id = %d AND m.is_confirmed = 1 AND m.is_banned = 0", $r['user_id'] );
    752         }
    753 
    754         if ( ! empty( $r['include'] ) ) {
    755             $include        = implode( ',', wp_parse_id_list( $r['include'] ) );
    756             $sql['include'] = " AND g.id IN ({$include})";
    757         }
    758 
    759         if ( ! empty( $r['exclude'] ) ) {
    760             $exclude        = implode( ',', wp_parse_id_list( $r['exclude'] ) );
    761             $sql['exclude'] = " AND g.id NOT IN ({$exclude})";
    762         }
    763 
    764         /** Order/orderby ********************************************/
    765 
    766         $order   = $r['order'];
    767         $orderby = $r['orderby'];
    768 
    769         // If a 'type' parameter was passed, parse it and overwrite
    770         // 'order' and 'orderby' params passed to the function
    771         if (  ! empty( $r['type'] ) ) {
    772             $order_orderby = apply_filters( 'bp_groups_get_orderby', self::convert_type_to_order_orderby( $r['type'] ), $r['type'] );
    773 
    774             // If an invalid type is passed, $order_orderby will be
    775             // an array with empty values. In this case, we stick
    776             // with the default values of $order and $orderby
    777             if ( ! empty( $order_orderby['order'] ) ) {
    778                 $order = $order_orderby['order'];
    779             }
    780 
    781             if ( ! empty( $order_orderby['orderby'] ) ) {
    782                 $orderby = $order_orderby['orderby'];
    783             }
    784         }
    785 
    786         // Sanitize 'order'
    787         $order = bp_esc_sql_order( $order );
    788 
    789         // Convert 'orderby' into the proper ORDER BY term
    790         $orderby = apply_filters( 'bp_groups_get_orderby_converted_by_term', self::convert_orderby_to_order_by_term( $orderby ), $orderby, $r['type'] );
    791 
    792         // Random order is a special case
    793         if ( 'rand()' === $orderby ) {
    794             $sql[] = "ORDER BY rand()";
    795         } else {
    796             $sql[] = "ORDER BY {$orderby} {$order}";
    797         }
    798 
    799         if ( ! empty( $r['per_page'] ) && ! empty( $r['page'] ) && $r['per_page'] != -1 ) {
    800             $sql['pagination'] = $wpdb->prepare( "LIMIT %d, %d", intval( ( $r['page'] - 1 ) * $r['per_page']), intval( $r['per_page'] ) );
    801         }
    802 
    803         // Get paginated results
    804         $paged_groups_sql = apply_filters( 'bp_groups_get_paged_groups_sql', join( ' ', (array) $sql ), $sql, $r );
    805         $paged_groups     = $wpdb->get_results( $paged_groups_sql );
    806 
    807         $total_sql['select'] = "SELECT COUNT(DISTINCT g.id) FROM {$bp->groups->table_name} g, {$bp->groups->table_name_groupmeta} gm";
    808 
    809         if ( ! empty( $r['user_id'] ) ) {
    810             $total_sql['select'] .= ", {$bp->groups->table_name_members} m";
    811         }
    812 
    813         if ( ! empty( $sql['hidden'] ) ) {
    814             $total_sql['where'][] = "g.status != 'hidden'";
    815         }
    816 
    817         if ( ! empty( $sql['search'] ) ) {
    818             $total_sql['where'][] = $wpdb->prepare( "( g.name LIKE %s OR g.description LIKE %s )", $search_terms_like, $search_terms_like );
    819         }
    820 
    821         if ( ! empty( $r['user_id'] ) ) {
    822             $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'] );
    823         }
    824 
    825         // Temporary implementation of meta_query for total count
    826         // See #5099
    827         if ( ! empty( $meta_query_sql['where'] ) ) {
    828             // Join the groupmeta table
    829             $total_sql['select'] .= ", ". substr( $meta_query_sql['join'], 0, -2 );
    830 
    831             // Modify the meta_query clause from paged_sql for our syntax
    832             $meta_query_clause = preg_replace( '/^\s*AND/', '', $meta_query_sql['where'] );
    833             $total_sql['where'][] = $meta_query_clause;
    834         }
    835 
    836         // Already escaped in the paginated results block
    837         if ( ! empty( $include ) ) {
    838             $total_sql['where'][] = "g.id IN ({$include})";
    839         }
    840 
    841         // Already escaped in the paginated results block
    842         if ( ! empty( $exclude ) ) {
    843             $total_sql['where'][] = "g.id NOT IN ({$exclude})";
    844         }
    845 
    846         $total_sql['where'][] = "g.id = gm.group_id";
    847         $total_sql['where'][] = "gm.meta_key = 'last_activity'";
    848 
    849         $t_sql = $total_sql['select'];
    850 
    851         if ( ! empty( $total_sql['where'] ) ) {
    852             $t_sql .= " WHERE " . join( ' AND ', (array) $total_sql['where'] );
    853         }
    854 
    855         // Get total group results
    856         $total_groups_sql = apply_filters( 'bp_groups_get_total_groups_sql', $t_sql, $total_sql, $r );
    857         $total_groups     = $wpdb->get_var( $total_groups_sql );
    858 
    859         $group_ids = array();
    860         foreach ( (array) $paged_groups as $group ) {
    861             $group_ids[] = $group->id;
    862         }
    863 
    864         // Populate some extra information instead of querying each time in the loop
    865         if ( !empty( $r['populate_extras'] ) ) {
    866             $paged_groups = BP_Groups_Group::get_group_extras( $paged_groups, $group_ids, $r['type'] );
    867         }
    868 
    869         // Grab all groupmeta
    870         if ( ! empty( $r['update_meta_cache'] ) ) {
    871             bp_groups_update_meta_cache( $group_ids );
    872         }
    873 
    874         unset( $sql, $total_sql );
    875 
    876         return array( 'groups' => $paged_groups, 'total' => $total_groups );
    877     }
    878 
    879     /**
    880      * Get the SQL for the 'meta_query' param in BP_Activity_Activity::get()
    881      *
    882      * We use WP_Meta_Query to do the heavy lifting of parsing the
    883      * meta_query array and creating the necessary SQL clauses. However,
    884      * since BP_Activity_Activity::get() builds its SQL differently than
    885      * WP_Query, we have to alter the return value (stripping the leading
    886      * AND keyword from the 'where' clause).
    887      *
    888      * @since BuddyPress (1.8.0)
    889      * @access protected
    890      *
    891      * @param array $meta_query An array of meta_query filters. See the
    892      *        documentation for {@link WP_Meta_Query} for details.
    893      * @return array $sql_array 'join' and 'where' clauses.
    894      */
    895     protected static function get_meta_query_sql( $meta_query = array() ) {
    896         global $wpdb;
    897 
    898         $sql_array = array(
    899             'join'  => '',
    900             'where' => '',
    901         );
    902 
    903         if ( ! empty( $meta_query ) ) {
    904             $groups_meta_query = new WP_Meta_Query( $meta_query );
    905 
    906             // WP_Meta_Query expects the table name at
    907             // $wpdb->group
    908             $wpdb->groupmeta = buddypress()->groups->table_name_groupmeta;
    909 
    910             $meta_sql = $groups_meta_query->get_sql( 'group', 'g', 'id' );
    911 
    912             // BP_Groups_Group::get uses the comma syntax for table
    913             // joins, which means that we have to do some regex to
    914             // convert the INNER JOIN and move the ON clause to a
    915             // WHERE condition
    916             //
    917             // @todo It may be better in the long run to refactor
    918             // the more general query syntax to accord better with
    919             // BP/WP convention
    920             preg_match_all( '/JOIN (.+?) ON/', $meta_sql['join'], $matches_a );
    921             preg_match_all( '/ON \((.+?)\)/', $meta_sql['join'], $matches_b );
    922 
    923             if ( ! empty( $matches_a[1] ) && ! empty( $matches_b[1] ) ) {
    924                 $sql_array['join']  = implode( ',', $matches_a[1] ) . ', ';
    925                 $sql_array['where'] = $meta_sql['where'] . ' AND ' . implode( ' AND ', $matches_b[1] );
    926             }
    927         }
    928 
    929         return $sql_array;
    930     }
    931 
    932     /**
    933      * Convert the 'type' parameter to 'order' and 'orderby'.
    934      *
    935      * @since BuddyPress (1.8.0)
    936      * @access protected
    937      *
    938      * @param string $type The 'type' shorthand param.
    939      * @return array {
    940      *  @type string $order SQL-friendly order string.
    941      *  @type string $orderby SQL-friendly orderby column name.
    942      * }
    943      */
    944     protected static function convert_type_to_order_orderby( $type = '' ) {
    945         $order = $orderby = '';
    946 
    947         switch ( $type ) {
    948             case 'newest' :
    949                 $order   = 'DESC';
    950                 $orderby = 'date_created';
    951                 break;
    952 
    953             case 'active' :
    954                 $order   = 'DESC';
    955                 $orderby = 'last_activity';
    956                 break;
    957 
    958             case 'popular' :
    959                 $order   = 'DESC';
    960                 $orderby = 'total_member_count';
    961                 break;
    962 
    963             case 'alphabetical' :
    964                 $order   = 'ASC';
    965                 $orderby = 'name';
    966                 break;
    967 
    968             case 'random' :
    969                 $order   = '';
    970                 $orderby = 'random';
    971                 break;
    972         }
    973 
    974         return array( 'order' => $order, 'orderby' => $orderby );
    975     }
    976 
    977     /**
    978      * Convert the 'orderby' param into a proper SQL term/column.
    979      *
    980      * @since BuddyPress (1.8.0)
    981      * @access protected
    982      *
    983      * @param string $orderby Orderby term as passed to get().
    984      * @return string $order_by_term SQL-friendly orderby term.
    985      */
    986     protected static function convert_orderby_to_order_by_term( $orderby ) {
    987         $order_by_term = '';
    988 
    989         switch ( $orderby ) {
    990             case 'date_created' :
    991             default :
    992                 $order_by_term = 'g.date_created';
    993                 break;
    994 
    995             case 'last_activity' :
    996                 $order_by_term = 'last_activity';
    997                 break;
    998 
    999             case 'total_member_count' :
    1000                 $order_by_term = 'CONVERT(gm1.meta_value, SIGNED)';
    1001                 break;
    1002 
    1003             case 'name' :
    1004                 $order_by_term = 'g.name';
    1005                 break;
    1006 
    1007             case 'random' :
    1008                 $order_by_term = 'rand()';
    1009                 break;
    1010         }
    1011 
    1012         return $order_by_term;
    1013     }
    1014 
    1015     /**
    1016      * Get a list of groups, sorted by those that have the most legacy forum topics.
    1017      *
    1018      * @param int $limit Optional. The max number of results to return.
    1019      *        Default: null (no limit).
    1020      * @param int $page Optional. The page offset of results to return.
    1021      *        Default: null (no limit).
    1022      * @param int $user_id Optional. If present, groups will be limited to
    1023      *        those of which the specified user is a member.
    1024      * @param string $search_terms Optional. Limit groups to those whose
    1025      *        name or description field contain the search string.
    1026      * @param bool $populate_extras Optional. Whether to fetch extra
    1027      *        information about the groups. Default: true.
    1028      * @param string|array Optional. Array or comma-separated list of group
    1029      *        IDs to exclude from results.
    1030      * @return array {
    1031      *     @type array $groups Array of group objects returned by the
    1032      *           paginated query.
    1033      *     @type int $total Total count of all groups matching non-
    1034      *           paginated query params.
    1035      * }
    1036      */
    1037     public static function get_by_most_forum_topics( $limit = null, $page = null, $user_id = 0, $search_terms = false, $populate_extras = true, $exclude = false ) {
    1038         global $wpdb, $bbdb;
    1039 
    1040         if ( empty( $bbdb ) )
    1041             do_action( 'bbpress_init' );
    1042 
    1043         if ( !empty( $limit ) && !empty( $page ) ) {
    1044             $pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
    1045         }
    1046 
    1047         if ( !is_user_logged_in() || ( !bp_current_user_can( 'bp_moderate' ) && ( $user_id != bp_loggedin_user_id() ) ) )
    1048             $hidden_sql = " AND g.status != 'hidden'";
    1049 
    1050         if ( !empty( $search_terms ) ) {
    1051             $search_terms_like = '%' . bp_esc_like( $search_terms ) . '%';
    1052             $search_sql        = $wpdb->prepare( ' AND ( g.name LIKE %s OR g.description LIKE %s ) ', $search_terms_like, $search_terms_like );
    1053         }
    1054 
    1055         if ( !empty( $exclude ) ) {
    1056             $exclude     = implode( ',', wp_parse_id_list( $exclude ) );
    1057             $exclude_sql = " AND g.id NOT IN ({$exclude})";
    1058         }
    1059 
    1060         $bp = buddypress();
    1061 
    1062         if ( !empty( $user_id ) ) {
    1063             $user_id      = absint( esc_sql( $user_id ) );
    1064             $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}" );
    1065             $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}" );
    1066         } else {
    1067             $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}" );
    1068             $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}" );
    1069         }
    1070 
    1071         if ( !empty( $populate_extras ) ) {
    1072             foreach ( (array) $paged_groups as $group ) {
    1073                 $group_ids[] = $group->id;
    1074             }
    1075             $paged_groups = BP_Groups_Group::get_group_extras( $paged_groups, $group_ids, 'newest' );
    1076         }
    1077 
    1078         return array( 'groups' => $paged_groups, 'total' => $total_groups );
    1079     }
    1080 
    1081     /**
    1082      * Get a list of groups, sorted by those that have the most legacy forum posts.
    1083      *
    1084      * @param int $limit Optional. The max number of results to return.
    1085      *        Default: null (no limit).
    1086      * @param int $page Optional. The page offset of results to return.
    1087      *        Default: null (no limit).
    1088      * @param int $user_id Optional. If present, groups will be limited to
    1089      *        those of which the specified user is a member.
    1090      * @param string $search_terms Optional. Limit groups to those whose
    1091      *        name or description field contain the search string.
    1092      * @param bool $populate_extras Optional. Whether to fetch extra
    1093      *        information about the groups. Default: true.
    1094      * @param string|array Optional. Array or comma-separated list of group
    1095      *        IDs to exclude from results.
    1096      * @return array {
    1097      *     @type array $groups Array of group objects returned by the
    1098      *           paginated query.
    1099      *     @type int $total Total count of all groups matching non-
    1100      *           paginated query params.
    1101      * }
    1102      */
    1103     public static function get_by_most_forum_posts( $limit = null, $page = null, $search_terms = false, $populate_extras = true, $exclude = false ) {
    1104         global $wpdb, $bbdb;
    1105 
    1106         if ( empty( $bbdb ) )
    1107             do_action( 'bbpress_init' );
    1108 
    1109         if ( !empty( $limit ) && !empty( $page ) ) {
    1110             $pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
    1111         }
    1112 
    1113         if ( !is_user_logged_in() || ( !bp_current_user_can( 'bp_moderate' ) && ( $user_id != bp_loggedin_user_id() ) ) )
    1114             $hidden_sql = " AND g.status != 'hidden'";
    1115 
    1116         if ( !empty( $search_terms ) ) {
    1117             $search_terms_like = '%' . bp_esc_like( $search_terms ) . '%';
    1118             $search_sql        = $wpdb->prepare( ' AND ( g.name LIKE %s OR g.description LIKE %s ) ', $search_terms_like, $search_terms_like );
    1119         }
    1120 
    1121         if ( !empty( $exclude ) ) {
    1122             $exclude     = implode( ',', wp_parse_id_list( $exclude ) );
    1123             $exclude_sql = " AND g.id NOT IN ({$exclude})";
    1124         }
    1125 
    1126         $bp = buddypress();
    1127 
    1128         if ( !empty( $user_id ) ) {
    1129             $user_id = esc_sql( $user_id );
    1130             $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}" );
    1131             $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} " );
    1132         } else {
    1133             $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}" );
    1134             $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}" );
    1135         }
    1136 
    1137         if ( !empty( $populate_extras ) ) {
    1138             foreach ( (array) $paged_groups as $group ) {
    1139                 $group_ids[] = $group->id;
    1140             }
    1141             $paged_groups = BP_Groups_Group::get_group_extras( $paged_groups, $group_ids, 'newest' );
    1142         }
    1143 
    1144         return array( 'groups' => $paged_groups, 'total' => $total_groups );
    1145     }
    1146 
    1147     /**
    1148      * Get a list of groups whose names start with a given letter.
    1149      *
    1150      * @param string $letter The letter.
    1151      * @param int $limit Optional. The max number of results to return.
    1152      *        Default: null (no limit).
    1153      * @param int $page Optional. The page offset of results to return.
    1154      *        Default: null (no limit).
    1155      * @param bool $populate_extras Optional. Whether to fetch extra
    1156      *        information about the groups. Default: true.
    1157      * @param string|array Optional. Array or comma-separated list of group
    1158      *        IDs to exclude from results.
    1159      * @return array {
    1160      *     @type array $groups Array of group objects returned by the
    1161      *           paginated query.
    1162      *     @type int $total Total count of all groups matching non-
    1163      *           paginated query params.
    1164      * }
    1165      */
    1166     public static function get_by_letter( $letter, $limit = null, $page = null, $populate_extras = true, $exclude = false ) {
    1167         global $wpdb;
    1168 
    1169         $pag_sql = $hidden_sql = $exclude_sql = '';
    1170 
    1171         // Multibyte compliance
    1172         if ( function_exists( 'mb_strlen' ) ) {
    1173             if ( mb_strlen( $letter, 'UTF-8' ) > 1 || is_numeric( $letter ) || !$letter ) {
    1174                 return false;
    1175             }
    1176         } else {
    1177             if ( strlen( $letter ) > 1 || is_numeric( $letter ) || !$letter ) {
    1178                 return false;
    1179             }
    1180         }
    1181 
    1182         $bp = buddypress();
    1183 
    1184         if ( !empty( $exclude ) ) {
    1185             $exclude     = implode( ',', wp_parse_id_list( $exclude ) );
    1186             $exclude_sql = " AND g.id NOT IN ({$exclude})";
    1187         }
    1188 
    1189         if ( !bp_current_user_can( 'bp_moderate' ) )
    1190             $hidden_sql = " AND status != 'hidden'";
    1191 
    1192         $letter_like = bp_esc_like( $letter ) . '%';
    1193 
    1194         if ( !empty( $limit ) && !empty( $page ) ) {
    1195             $pag_sql      = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
    1196         }
    1197 
    1198         $total_groups = $wpdb->get_var( $wpdb->prepare( "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 %s {$hidden_sql} {$exclude_sql}", $letter_like ) );
    1199 
    1200         $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} 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 %s {$hidden_sql} {$exclude_sql} ORDER BY g.name ASC {$pag_sql}", $letter_like ) );
    1201 
    1202         if ( !empty( $populate_extras ) ) {
    1203             foreach ( (array) $paged_groups as $group ) {
    1204                 $group_ids[] = $group->id;
    1205             }
    1206             $paged_groups = BP_Groups_Group::get_group_extras( $paged_groups, $group_ids, 'newest' );
    1207         }
    1208 
    1209         return array( 'groups' => $paged_groups, 'total' => $total_groups );
    1210     }
    1211 
    1212     /**
    1213      * Get a list of random groups.
    1214      *
    1215      * Use BP_Groups_Group::get() with 'type' = 'random' instead.
    1216      *
    1217      * @param int $limit Optional. The max number of results to return.
    1218      *        Default: null (no limit).
    1219      * @param int $page Optional. The page offset of results to return.
    1220      *        Default: null (no limit).
    1221      * @param int $user_id Optional. If present, groups will be limited to
    1222      *        those of which the specified user is a member.
    1223      * @param string $search_terms Optional. Limit groups to those whose
    1224      *        name or description field contain the search string.
    1225      * @param bool $populate_extras Optional. Whether to fetch extra
    1226      *        information about the groups. Default: true.
    1227      * @param string|array Optional. Array or comma-separated list of group
    1228      *        IDs to exclude from results.
    1229      * @return array {
    1230      *     @type array $groups Array of group objects returned by the
    1231      *           paginated query.
    1232      *     @type int $total Total count of all groups matching non-
    1233      *           paginated query params.
    1234      * }
    1235      */
    1236     public static function get_random( $limit = null, $page = null, $user_id = 0, $search_terms = false, $populate_extras = true, $exclude = false ) {
    1237         global $wpdb;
    1238 
    1239         $pag_sql = $hidden_sql = $search_sql = $exclude_sql = '';
    1240 
    1241         if ( !empty( $limit ) && !empty( $page ) )
    1242             $pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
    1243 
    1244         if ( !is_user_logged_in() || ( !bp_current_user_can( 'bp_moderate' ) && ( $user_id != bp_loggedin_user_id() ) ) )
    1245             $hidden_sql = "AND g.status != 'hidden'";
    1246 
    1247         if ( !empty( $search_terms ) ) {
    1248             $search_terms_like = '%' . bp_esc_like( $search_terms ) . '%';
    1249             $search_sql = $wpdb->prepare( " AND ( g.name LIKE %s OR g.description LIKE %s )", $search_terms_like, $search_terms_like );
    1250         }
    1251 
    1252         if ( !empty( $exclude ) ) {
    1253             $exclude     = wp_parse_id_list( $exclude );
    1254             $exclude     = esc_sql( implode( ',', $exclude ) );
    1255             $exclude_sql = " AND g.id NOT IN ({$exclude})";
    1256         }
    1257 
    1258         $bp = buddypress();
    1259 
    1260         if ( !empty( $user_id ) ) {
    1261             $user_id = esc_sql( $user_id );
    1262             $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}" );
    1263             $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}" );
    1264         } else {
    1265             $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}" );
    1266             $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}" );
    1267         }
    1268 
    1269         if ( !empty( $populate_extras ) ) {
    1270             foreach ( (array) $paged_groups as $group ) {
    1271                 $group_ids[] = $group->id;
    1272             }
    1273             $paged_groups = BP_Groups_Group::get_group_extras( $paged_groups, $group_ids, 'newest' );
    1274         }
    1275 
    1276         return array( 'groups' => $paged_groups, 'total' => $total_groups );
    1277     }
    1278 
    1279     /**
    1280      * Fetch extra data for a list of groups.
    1281      *
    1282      * This method is used throughout the class, by methods that take a
    1283      * $populate_extras parameter.
    1284      *
    1285      * Data fetched:
    1286      *
    1287      *     - Logged-in user's status within each group (is_member,
    1288      *       is_confirmed, is_pending, is_banned)
    1289      *
    1290      * @param array $paged_groups Array of groups.
    1291      * @param string|array Array or comma-separated list of IDs matching
    1292      *        $paged_groups.
    1293      * @param string $type Not used.
    1294      * @return array $paged_groups
    1295      */
    1296     public static function get_group_extras( &$paged_groups, &$group_ids, $type = false ) {
    1297         global $wpdb;
    1298 
    1299         if ( empty( $group_ids ) )
    1300             return $paged_groups;
    1301 
    1302         $bp = buddypress();
    1303 
    1304         // Sanitize group IDs
    1305         $group_ids = implode( ',', wp_parse_id_list( $group_ids ) );
    1306 
    1307         // Fetch the logged-in user's status within each group
    1308         if ( is_user_logged_in() ) {
    1309             $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() ) );
    1310         } else {
    1311             $user_status_results = array();
    1312         }
    1313 
    1314         // Reindex
    1315         $user_status = array();
    1316         foreach ( $user_status_results as $user_status_result ) {
    1317             $user_status[ $user_status_result->group_id ] = $user_status_result;
    1318         }
    1319 
    1320         for ( $i = 0, $count = count( $paged_groups ); $i < $count; ++$i ) {
    1321             $is_member = $is_invited = $is_pending = '0';
    1322             $gid = $paged_groups[ $i ]->id;
    1323 
    1324             if ( isset( $user_status[ $gid ] ) ) {
    1325 
    1326                 // is_confirmed means the user is a member
    1327                 if ( $user_status[ $gid ]->is_confirmed ) {
    1328                     $is_member = '1';
    1329 
    1330                 // invite_sent means the user has been invited
    1331                 } elseif ( $user_status[ $gid ]->invite_sent ) {
    1332                     $is_invited = '1';
    1333 
    1334                 // User has sent request, but has not been confirmed
    1335                 } else {
    1336                     $is_pending = '1';
    1337                 }
    1338             }
    1339 
    1340             $paged_groups[ $i ]->is_member = $is_member;
    1341             $paged_groups[ $i ]->is_invited = $is_invited;
    1342             $paged_groups[ $i ]->is_pending = $is_pending;
    1343         }
    1344 
    1345         if ( is_user_logged_in() ) {
    1346             $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() ) );
    1347         } else {
    1348             $user_banned = array();
    1349         }
    1350 
    1351         for ( $i = 0, $count = count( $paged_groups ); $i < $count; ++$i ) {
    1352             $paged_groups[$i]->is_banned = false;
    1353 
    1354             foreach ( (array) $user_banned as $group_id ) {
    1355                 if ( $group_id == $paged_groups[$i]->id ) {
    1356                     $paged_groups[$i]->is_banned = true;
    1357                 }
    1358             }
    1359         }
    1360 
    1361         return $paged_groups;
    1362     }
    1363 
    1364     /**
    1365      * Delete all invitations to a given group.
    1366      *
    1367      * @param int $group_id ID of the group whose invitations are being
    1368      *        deleted.
    1369      * @return int|null Number of rows records deleted on success, null on
    1370      *         failure.
    1371      */
    1372     public static function delete_all_invites( $group_id ) {
    1373         global $wpdb;
    1374 
    1375         $bp = buddypress();
    1376 
    1377         return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->groups->table_name_members} WHERE group_id = %d AND invite_sent = 1", $group_id ) );
    1378     }
    1379 
    1380     /**
    1381      * Get a total group count for the site.
    1382      *
    1383      * Will include hidden groups in the count only if
    1384      * current_user_can( 'bp_moderate' ).
    1385      *
    1386      * @return int Group count.
    1387      */
    1388     public static function get_total_group_count() {
    1389         global $wpdb;
    1390 
    1391         $hidden_sql = '';
    1392         if ( !bp_current_user_can( 'bp_moderate' ) )
    1393             $hidden_sql = "WHERE status != 'hidden'";
    1394 
    1395         $bp = buddypress();
    1396 
    1397         return $wpdb->get_var( "SELECT COUNT(id) FROM {$bp->groups->table_name} {$hidden_sql}" );
    1398     }
    1399 
    1400     /**
    1401      * Get global count of forum topics in public groups (legacy forums).
    1402      *
    1403      * @param $type Optional. If 'unreplied', count will be limited to
    1404      *        those topics that have received no replies.
    1405      * @return int Forum topic count.
    1406      */
    1407     public static function get_global_forum_topic_count( $type ) {
    1408         global $bbdb, $wpdb;
    1409 
    1410         $bp = buddypress();
    1411 
    1412         if ( 'unreplied' == $type )
    1413             $bp->groups->filter_sql = ' AND t.topic_posts = 1';
    1414 
    1415         // https://buddypress.trac.wordpress.org/ticket/4306
    1416         $extra_sql = apply_filters( 'get_global_forum_topic_count_extra_sql', $bp->groups->filter_sql, $type );
    1417 
    1418         // Make sure the $extra_sql begins with an AND
    1419         if ( 'AND' != substr( trim( strtoupper( $extra_sql ) ), 0, 3 ) )
    1420             $extra_sql = ' AND ' . $extra_sql;
    1421 
    1422         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} " );
    1423     }
    1424 
    1425     /**
    1426      * Get the member count for a group.
    1427      *
    1428      * @param int $group_id Group ID.
    1429      * @return int Count of confirmed members for the group.
    1430      */
    1431     public static function get_total_member_count( $group_id ) {
    1432         global $wpdb;
    1433 
    1434         $bp = buddypress();
    1435 
    1436         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 ) );
    1437     }
    1438 
    1439     /**
    1440      * Get a total count of all topics of a given status, across groups/forums
    1441      *
    1442      * @since BuddyPress (1.5.0)
    1443      *
    1444      * @param string $status Which group type to count. 'public', 'private',
    1445      *        'hidden', or 'all'. Default: 'public'.
    1446      * @return int The topic count
    1447      */
    1448     public static function get_global_topic_count( $status = 'public', $search_terms = false ) {
    1449         global $bbdb, $wpdb;
    1450 
    1451         switch ( $status ) {
    1452             case 'all' :
    1453                 $status_sql = '';
    1454                 break;
    1455 
    1456             case 'hidden' :
    1457                 $status_sql = "AND g.status = 'hidden'";
    1458                 break;
    1459 
    1460             case 'private' :
    1461                 $status_sql = "AND g.status = 'private'";
    1462                 break;
    1463 
    1464             case 'public' :
    1465             default :
    1466                 $status_sql = "AND g.status = 'public'";
    1467                 break;
    1468         }
    1469 
    1470         $bp = buddypress();
    1471 
    1472         $sql = array();
    1473 
    1474         $sql['select'] = "SELECT COUNT(t.topic_id)";
    1475         $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";
    1476         $sql['where']  = "WHERE gm.meta_key = 'forum_id' {$status_sql} AND t.topic_status = '0' AND t.topic_sticky != '2'";
    1477 
    1478         if ( !empty( $search_terms ) ) {
    1479             $search_terms_like = '%' . bp_esc_like( $search_terms ) . '%';
    1480             $sql['where'] .= $wpdb->prepare( " AND ( t.topic_title LIKE %s )", $search_terms_like );
    1481         }
    1482 
    1483         return $wpdb->get_var( implode( ' ', $sql ) );
    1484     }
    1485 
    1486     /**
    1487      * Get an array containing ids for each group type.
    1488      *
    1489      * A bit of a kludge workaround for some issues
    1490      * with bp_has_groups().
    1491      *
    1492      * @since BuddyPress (1.7.0)
    1493      *
    1494      * @return array
    1495      */
    1496     public static function get_group_type_ids() {
    1497         global $wpdb;
    1498 
    1499         $bp  = buddypress();
    1500         $ids = array();
    1501 
    1502         $ids['all']     = $wpdb->get_col( "SELECT id FROM {$bp->groups->table_name}" );
    1503         $ids['public']  = $wpdb->get_col( "SELECT id FROM {$bp->groups->table_name} WHERE status = 'public'" );
    1504         $ids['private'] = $wpdb->get_col( "SELECT id FROM {$bp->groups->table_name} WHERE status = 'private'" );
    1505         $ids['hidden']  = $wpdb->get_col( "SELECT id FROM {$bp->groups->table_name} WHERE status = 'hidden'" );
    1506 
    1507         return $ids;
    1508     }
    1509 }
    1510 
    1511 /**
    1512  * Query for the members of a group.
    1513  *
    1514  * Special notes about the group members data schema:
    1515  * - *Members* are entries with is_confirmed = 1
    1516  * - *Pending requests* are entries with is_confirmed = 0 and inviter_id = 0
    1517  * - *Pending and sent invitations* are entries with is_confirmed = 0 and
    1518  *   inviter_id != 0 and invite_sent = 1
    1519  * - *Pending and unsent invitations* are entries with is_confirmed = 0 and
    1520  *   inviter_id != 0 and invite_sent = 0
    1521  * - *Membership requests* are entries with is_confirmed = 0 and
    1522  *   inviter_id = 0 (and invite_sent = 0)
    1523  *
    1524  * @since BuddyPress (1.8.0)
    1525  *
    1526  * @param array $args {
    1527  *     Array of arguments. Accepts all arguments from
    1528  *     {@link BP_User_Query}, with the following additions:
    1529  *     @type int $group_id ID of the group to limit results to.
    1530  *     @type array $group_role Array of group roles to match ('member',
    1531  *           'mod', 'admin', 'banned'). Default: array( 'member' ).
    1532  *     @type bool $is_confirmed Whether to limit to confirmed members.
    1533  *           Default: true.
    1534  *     @type string $type Sort order. Accepts any value supported by
    1535  *           {@link BP_User_Query}, in addition to 'last_joined' and
    1536  *           'first_joined'. Default: 'last_joined'.
    1537  * }
    1538  */
    1539 class BP_Group_Member_Query extends BP_User_Query {
    1540 
    1541     /**
    1542      * Array of group member ids, cached to prevent redundant lookups.
    1543      *
    1544      * @since BuddyPress (1.8.1)
    1545      * @access protected
    1546      * @var null|array Null if not yet defined, otherwise an array of ints.
    1547      */
    1548     protected $group_member_ids;
    1549 
    1550     /**
    1551      * Set up action hooks.
    1552      *
    1553      * @since BuddyPress (1.8.0)
    1554      */
    1555     public function setup_hooks() {
    1556         // Take this early opportunity to set the default 'type' param
    1557         // to 'last_joined', which will ensure that BP_User_Query
    1558         // trusts our order and does not try to apply its own
    1559         if ( empty( $this->query_vars_raw['type'] ) ) {
    1560             $this->query_vars_raw['type'] = 'last_joined';
    1561         }
    1562 
    1563         // Set the sort order
    1564         add_action( 'bp_pre_user_query', array( $this, 'set_orderby' ) );
    1565 
    1566         // Set up our populate_extras method
    1567         add_action( 'bp_user_query_populate_extras', array( $this, 'populate_group_member_extras' ), 10, 2 );
    1568     }
    1569 
    1570     /**
    1571      * Get a list of user_ids to include in the IN clause of the main query.
    1572      *
    1573      * Overrides BP_User_Query::get_include_ids(), adding our additional
    1574      * group-member logic.
    1575      *
    1576      * @since BuddyPress (1.8.0)
    1577      *
    1578      * @param array $include Existing group IDs in the $include parameter,
    1579      *        as calculated in BP_User_Query.
    1580      * @return array
    1581      */
    1582     public function get_include_ids( $include = array() ) {
    1583         // The following args are specific to group member queries, and
    1584         // are not present in the query_vars of a normal BP_User_Query.
    1585         // We loop through to make sure that defaults are set (though
    1586         // values passed to the constructor will, as usual, override
    1587         // these defaults).
    1588         $this->query_vars = wp_parse_args( $this->query_vars, array(
    1589             'group_id'     => 0,
    1590             'group_role'   => array( 'member' ),
    1591             'is_confirmed' => true,
    1592             'invite_sent'  => null,
    1593             'inviter_id'   => null,
    1594             'type'         => 'last_joined',
    1595         ) );
    1596 
    1597         $group_member_ids = $this->get_group_member_ids();
    1598 
    1599         // If the group member query returned no users, bail with an
    1600         // array that will guarantee no matches for BP_User_Query
    1601         if ( empty( $group_member_ids ) ) {
    1602             return array( 0 );
    1603         }
    1604 
    1605         if ( ! empty( $include ) ) {
    1606             $group_member_ids = array_intersect( $include, $group_member_ids );
    1607         }
    1608 
    1609         return $group_member_ids;
    1610     }
    1611 
    1612     /**
    1613      * Get the members of the queried group.
    1614      *
    1615      * @since BuddyPress (1.8.0)
    1616      *
    1617      * @return array $ids User IDs of relevant group member ids.
    1618      */
    1619     protected function get_group_member_ids() {
    1620         global $wpdb;
    1621 
    1622         if ( is_array( $this->group_member_ids ) ) {
    1623             return $this->group_member_ids;
    1624         }
    1625 
    1626         $bp  = buddypress();
    1627         $sql = array(
    1628             'select'  => "SELECT user_id FROM {$bp->groups->table_name_members}",
    1629             'where'   => array(),
    1630             'orderby' => '',
    1631             'order'   => '',
    1632         );
    1633 
    1634         /** WHERE clauses *****************************************************/
    1635 
    1636         // Group id
    1637         $sql['where'][] = $wpdb->prepare( "group_id = %d", $this->query_vars['group_id'] );
    1638 
    1639         // is_confirmed
    1640         $is_confirmed = ! empty( $this->query_vars['is_confirmed'] ) ? 1 : 0;
    1641         $sql['where'][] = $wpdb->prepare( "is_confirmed = %d", $is_confirmed );
    1642 
    1643         // invite_sent
    1644         if ( ! is_null( $this->query_vars['invite_sent'] ) ) {
    1645             $invite_sent = ! empty( $this->query_vars['invite_sent'] ) ? 1 : 0;
    1646             $sql['where'][] = $wpdb->prepare( "invite_sent = %d", $invite_sent );
    1647         }
    1648 
    1649         // inviter_id
    1650         if ( ! is_null( $this->query_vars['inviter_id'] ) ) {
    1651             $inviter_id = $this->query_vars['inviter_id'];
    1652 
    1653             // Empty: inviter_id = 0. (pass false, 0, or empty array)
    1654             if ( empty( $inviter_id ) ) {
    1655                 $sql['where'][] = "inviter_id = 0";
    1656 
    1657             // The string 'any' matches any non-zero value (inviter_id != 0)
    1658             } elseif ( 'any' === $inviter_id ) {
    1659                 $sql['where'][] = "inviter_id != 0";
    1660 
    1661             // Assume that a list of inviter IDs has been passed
    1662             } else {
    1663                 // Parse and sanitize
    1664                 $inviter_ids = wp_parse_id_list( $inviter_id );
    1665                 if ( ! empty( $inviter_ids ) ) {
    1666                     $inviter_ids_sql = implode( ',', $inviter_ids );
    1667                     $sql['where'][] = "inviter_id IN ({$inviter_ids_sql})";
    1668                 }
    1669             }
    1670         }
    1671 
    1672         // Role information is stored as follows: admins have
    1673         // is_admin = 1, mods have is_mod = 1, banned have is_banned =
    1674         // 1, and members have all three set to 0.
    1675         $roles = !empty( $this->query_vars['group_role'] ) ? $this->query_vars['group_role'] : array();
    1676         if ( is_string( $roles ) ) {
    1677             $roles = explode( ',', $roles );
    1678         }
    1679 
    1680         // Sanitize: Only 'admin', 'mod', 'member', and 'banned' are valid
    1681         $allowed_roles = array( 'admin', 'mod', 'member', 'banned' );
    1682         foreach ( $roles as $role_key => $role_value ) {
    1683             if ( ! in_array( $role_value, $allowed_roles ) ) {
    1684                 unset( $roles[ $role_key ] );
    1685             }
    1686         }
    1687 
    1688         $roles = array_unique( $roles );
    1689 
    1690         // When querying for a set of roles containing 'member' (for
    1691         // which there is no dedicated is_ column), figure out a list
    1692         // of columns *not* to match
    1693         $roles_sql = '';
    1694         if ( in_array( 'member', $roles ) ) {
    1695             $role_columns = array();
    1696             foreach ( array_diff( $allowed_roles, $roles ) as $excluded_role ) {
    1697                 $role_columns[] = 'is_' . $excluded_role . ' = 0';
    1698             }
    1699 
    1700             if ( ! empty( $role_columns ) ) {
    1701                 $roles_sql = '(' . implode( ' AND ', $role_columns ) . ')';
    1702             }
    1703 
    1704         // When querying for a set of roles *not* containing 'member',
    1705         // simply construct a list of is_* = 1 clauses
    1706         } else {
    1707             $role_columns = array();
    1708             foreach ( $roles as $role ) {
    1709                 $role_columns[] = 'is_' . $role . ' = 1';
    1710             }
    1711 
    1712             if ( ! empty( $role_columns ) ) {
    1713                 $roles_sql = '(' . implode( ' OR ', $role_columns ) . ')';
    1714             }
    1715         }
    1716 
    1717         if ( ! empty( $roles_sql ) ) {
    1718             $sql['where'][] = $roles_sql;
    1719         }
    1720 
    1721         $sql['where'] = ! empty( $sql['where'] ) ? 'WHERE ' . implode( ' AND ', $sql['where'] ) : '';
    1722 
    1723         // We fetch group members in order of last_joined, regardless
    1724         // of 'type'. If the 'type' value is not 'last_joined' or
    1725         // 'first_joined', the order will be overridden in
    1726         // BP_Group_Member_Query::set_orderby()
    1727         $sql['orderby'] = "ORDER BY date_modified";
    1728         $sql['order']   = 'first_joined' === $this->query_vars['type'] ? 'ASC' : 'DESC';
    1729 
    1730         $this->group_member_ids = $wpdb->get_col( "{$sql['select']} {$sql['where']} {$sql['orderby']} {$sql['order']}" );
    1731 
    1732         /**
    1733          * Use this filter to build a custom query (such as when you've
    1734          * defined a custom 'type').
    1735          */
    1736         $this->group_member_ids = apply_filters( 'bp_group_member_query_group_member_ids', $this->group_member_ids, $this );
    1737 
    1738         return $this->group_member_ids;
    1739     }
    1740 
    1741     /**
    1742      * Tell BP_User_Query to order by the order of our query results.
    1743      *
    1744      * We only override BP_User_Query's native ordering in case of the
    1745      * 'last_joined' and 'first_joined' $type parameters.
    1746      *
    1747      * @param BP_User_Query $query BP_User_Query object.
    1748      */
    1749     public function set_orderby( $query ) {
    1750         $gm_ids = $this->get_group_member_ids();
    1751         if ( empty( $gm_ids ) ) {
    1752             $gm_ids = array( 0 );
    1753         }
    1754 
    1755         // For 'last_joined', 'first_joined', and 'group_activity'
    1756         // types, we override the default orderby clause of
    1757         // BP_User_Query. In the case of 'group_activity', we perform
    1758         // a separate query to get the necessary order. In the case of
    1759         // 'last_joined' and 'first_joined', we can trust the order of
    1760         // results from  BP_Group_Member_Query::get_group_members().
    1761         // In all other cases, we fall through and let BP_User_Query
    1762         // do its own (non-group-specific) ordering.
    1763         if ( in_array( $query->query_vars['type'], array( 'last_joined', 'first_joined', 'group_activity' ) ) ) {
    1764 
    1765             // Group Activity DESC
    1766             if ( 'group_activity' == $query->query_vars['type'] ) {
    1767                 $gm_ids = $this->get_gm_ids_ordered_by_activity( $query, $gm_ids );
    1768             }
    1769 
    1770             // The first param in the FIELD() clause is the sort column id
    1771             $gm_ids = array_merge( array( 'u.id' ), wp_parse_id_list( $gm_ids ) );
    1772             $gm_ids_sql = implode( ',', $gm_ids );
    1773 
    1774             $query->uid_clauses['orderby'] = "ORDER BY FIELD(" . $gm_ids_sql . ")";
    1775         }
    1776 
    1777         // Prevent this filter from running on future BP_User_Query
    1778         // instances on the same page
    1779         remove_action( 'bp_pre_user_query', array( $this, 'set_orderby' ) );
    1780     }
    1781 
    1782     /**
    1783      * Fetch additional data required in bp_group_has_members() loops.
    1784      *
    1785      * Additional data fetched:
    1786      *
    1787      *      - is_banned
    1788      *      - date_modified
    1789      *
    1790      * @since BuddyPress (1.8.0)
    1791      *
    1792      * @param BP_User_Query $query BP_User_Query object. Because we're
    1793      *        filtering the current object, we use $this inside of the
    1794      *        method instead.
    1795      * @param string $user_ids_sql Sanitized, comma-separated string of
    1796      *        the user ids returned by the main query.
    1797      */
    1798     public function populate_group_member_extras( $query, $user_ids_sql ) {
    1799         global $wpdb;
    1800 
    1801         $bp     = buddypress();
    1802         $extras = $wpdb->get_results( $wpdb->prepare( "SELECT id, user_id, date_modified, is_admin, is_mod, comments, user_title, invite_sent, is_confirmed, inviter_id, is_banned FROM {$bp->groups->table_name_members} WHERE user_id IN ({$user_ids_sql}) AND group_id = %d", $this->query_vars['group_id'] ) );
    1803 
    1804         foreach ( (array) $extras as $extra ) {
    1805             if ( isset( $this->results[ $extra->user_id ] ) ) {
    1806                 // user_id is provided for backward compatibility
    1807                 $this->results[ $extra->user_id ]->user_id       = (int) $extra->user_id;
    1808                 $this->results[ $extra->user_id ]->is_admin      = (int) $extra->is_admin;
    1809                 $this->results[ $extra->user_id ]->is_mod        = (int) $extra->is_mod;
    1810                 $this->results[ $extra->user_id ]->is_banned     = (int) $extra->is_banned;
    1811                 $this->results[ $extra->user_id ]->date_modified = $extra->date_modified;
    1812                 $this->results[ $extra->user_id ]->user_title    = $extra->user_title;
    1813                 $this->results[ $extra->user_id ]->comments      = $extra->comments;
    1814                 $this->results[ $extra->user_id ]->invite_sent   = (int) $extra->invite_sent;
    1815                 $this->results[ $extra->user_id ]->inviter_id    = (int) $extra->inviter_id;
    1816                 $this->results[ $extra->user_id ]->is_confirmed  = (int) $extra->is_confirmed;
    1817                 $this->results[ $extra->user_id ]->membership_id = (int) $extra->id;
    1818             }
    1819         }
    1820 
    1821         // Don't filter other BP_User_Query objects on the same page
    1822         remove_action( 'bp_user_query_populate_extras', array( $this, 'populate_group_member_extras' ), 10, 2 );
    1823     }
    1824 
    1825     /**
    1826      * Sort user IDs by how recently they have generated activity within a given group.
    1827      *
    1828      * @since BuddyPress (2.1.0)
    1829      *
    1830      * @param BP_User_Query $query BP_User_Query object.
    1831      * @param array $gm_ids array of group member ids.
    1832      * @return array
    1833      */
    1834     public function get_gm_ids_ordered_by_activity( $query, $gm_ids = array() ) {
    1835         global $wpdb;
    1836 
    1837         if ( empty( $gm_ids ) ) {
    1838             return $gm_ids;
    1839         }
    1840 
    1841         if ( ! bp_is_active( 'activity' ) ) {
    1842             return $gm_ids;
    1843         }
    1844 
    1845         $activity_table = buddypress()->activity->table_name;
    1846 
    1847         $sql = array(
    1848             'select'  => "SELECT user_id, max( date_recorded ) as date_recorded FROM {$activity_table}",
    1849             'where'   => array(),
    1850             'groupby' => 'GROUP BY user_id',
    1851             'orderby' => 'ORDER BY date_recorded',
    1852             'order'   => 'DESC',
    1853         );
    1854 
    1855         $sql['where'] = array(
    1856             'user_id IN (' . implode( ',', wp_parse_id_list( $gm_ids ) ) . ')',
    1857             'item_id = ' . absint( $query->query_vars['group_id'] ),
    1858             $wpdb->prepare( "component = %s", buddypress()->groups->id ),
    1859         );
    1860 
    1861         $sql['where'] = 'WHERE ' . implode( ' AND ', $sql['where'] );
    1862 
    1863         $group_user_ids = $wpdb->get_results( "{$sql['select']} {$sql['where']} {$sql['groupby']} {$sql['orderby']} {$sql['order']}" );
    1864 
    1865         return wp_list_pluck( $group_user_ids, 'user_id' );
    1866     }
    1867 }
    1868 
    1869 /**
    1870  * BuddyPress Group Membership object.
    1871  */
    1872 class BP_Groups_Member {
    1873 
    1874     /**
    1875      * ID of the membership.
    1876      *
    1877      * @access public
    1878      * @var int
    1879      */
    1880     var $id;
    1881 
    1882     /**
    1883      * ID of the group associated with the membership.
    1884      *
    1885      * @access public
    1886      * @var int
    1887      */
    1888     var $group_id;
    1889 
    1890     /**
    1891      * ID of the user associated with the membership.
    1892      *
    1893      * @access public
    1894      * @var int
    1895      */
    1896     var $user_id;
    1897 
    1898     /**
    1899      * ID of the user whose invitation initiated the membership.
    1900      *
    1901      * @access public
    1902      * @var int
    1903      */
    1904     var $inviter_id;
    1905 
    1906     /**
    1907      * Whether the member is an admin of the group.
    1908      *
    1909      * @access public
    1910      * @var int
    1911      */
    1912     var $is_admin;
    1913 
    1914     /**
    1915      * Whether the member is a mod of the group.
    1916      *
    1917      * @access public
    1918      * @var int
    1919      */
    1920     var $is_mod;
    1921 
    1922     /**
    1923      * Whether the member is banned from the group.
    1924      *
    1925      * @access public
    1926      * @var int
    1927      */
    1928     var $is_banned;
    1929 
    1930     /**
    1931      * Title used to describe the group member's role in the group.
    1932      *
    1933      * Eg, 'Group Admin'.
    1934      *
    1935      * @access public
    1936      * @var int
    1937      */
    1938     var $user_title;
    1939 
    1940     /**
    1941      * Last modified date of the membership.
    1942      *
    1943      * This value is updated when, eg, invitations are accepted.
    1944      *
    1945      * @access public
    1946      * @var string
    1947      */
    1948     var $date_modified;
    1949 
    1950     /**
    1951      * Whether the membership has been confirmed.
    1952      *
    1953      * @access public
    1954      * @var int
    1955      */
    1956     var $is_confirmed;
    1957 
    1958     /**
    1959      * Comments associated with the membership.
    1960      *
    1961      * In BP core, these are limited to the optional message users can
    1962      * include when requesting membership to a private group.
    1963      *
    1964      * @access public
    1965      * @var string
    1966      */
    1967     var $comments;
    1968 
    1969     /**
    1970      * Whether an invitation has been sent for this membership.
    1971      *
    1972      * The purpose of this flag is to mark when an invitation has been
    1973      * "drafted" (the user has been added via the interface at Send
    1974      * Invites), but the Send button has not been pressed, so the
    1975      * invitee has not yet been notified.
    1976      *
    1977      * @access public
    1978      * @var int
    1979      */
    1980     var $invite_sent;
    1981 
    1982     /**
    1983      * WP_User object representing the membership's user.
    1984      *
    1985      * @access public
    1986      * @var WP_User
    1987      */
    1988     var $user;
    1989 
    1990     /**
    1991      * Constructor method.
    1992      *
    1993      * @param int $user_id Optional. Along with $group_id, can be used to
    1994      *        look up a membership.
    1995      * @param int $group_id Optional. Along with $user_id, can be used to
    1996      *        look up a membership.
    1997      * @param int $id Optional. The unique ID of the membership object.
    1998      * @param bool $populate Whether to populate the properties of the
    1999      *        located membership. Default: true.
    2000      */
    2001     public function __construct( $user_id = 0, $group_id = 0, $id = false, $populate = true ) {
    2002 
    2003         // User and group are not empty, and ID is
    2004         if ( !empty( $user_id ) && !empty( $group_id ) && empty( $id ) ) {
    2005             $this->user_id  = $user_id;
    2006             $this->group_id = $group_id;
    2007 
    2008             if ( !empty( $populate ) ) {
    2009                 $this->populate();
    2010             }
    2011         }
    2012 
    2013         // ID is not empty
    2014         if ( !empty( $id ) ) {
    2015             $this->id = $id;
    2016 
    2017             if ( !empty( $populate ) ) {
    2018                 $this->populate();
    2019             }
    2020         }
    2021     }
    2022 
    2023     /**
    2024      * Populate the object's properties.
    2025      */
    2026     public function populate() {
    2027         global $wpdb;
    2028 
    2029         $bp = buddypress();
    2030 
    2031         if ( $this->user_id && $this->group_id && !$this->id )
    2032             $sql = $wpdb->prepare( "SELECT * FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d", $this->user_id, $this->group_id );
    2033 
    2034         if ( !empty( $this->id ) )
    2035             $sql = $wpdb->prepare( "SELECT * FROM {$bp->groups->table_name_members} WHERE id = %d", $this->id );
    2036 
    2037         $member = $wpdb->get_row($sql);
    2038 
    2039         if ( !empty( $member ) ) {
    2040             $this->id            = $member->id;
    2041             $this->group_id      = $member->group_id;
    2042             $this->user_id       = $member->user_id;
    2043             $this->inviter_id    = $member->inviter_id;
    2044             $this->is_admin      = $member->is_admin;
    2045             $this->is_mod        = $member->is_mod;
    2046             $this->is_banned     = $member->is_banned;
    2047             $this->user_title    = $member->user_title;
    2048             $this->date_modified = $member->date_modified;
    2049             $this->is_confirmed  = $member->is_confirmed;
    2050             $this->comments      = $member->comments;
    2051             $this->invite_sent   = $member->invite_sent;
    2052 
    2053             $this->user = new BP_Core_User( $this->user_id );
    2054         }
    2055     }
    2056 
    2057     /**
    2058      * Save the membership data to the database.
    2059      *
    2060      * @return bool True on success, false on failure.
    2061      */
    2062     public function save() {
    2063         global $wpdb;
    2064 
    2065         $bp = buddypress();
    2066 
    2067         $this->user_id       = apply_filters( 'groups_member_user_id_before_save',       $this->user_id,       $this->id );
    2068         $this->group_id      = apply_filters( 'groups_member_group_id_before_save',      $this->group_id,      $this->id );
    2069         $this->inviter_id    = apply_filters( 'groups_member_inviter_id_before_save',    $this->inviter_id,    $this->id );
    2070         $this->is_admin      = apply_filters( 'groups_member_is_admin_before_save',      $this->is_admin,      $this->id );
    2071         $this->is_mod        = apply_filters( 'groups_member_is_mod_before_save',        $this->is_mod,        $this->id );
    2072         $this->is_banned     = apply_filters( 'groups_member_is_banned_before_save',     $this->is_banned,     $this->id );
    2073         $this->user_title    = apply_filters( 'groups_member_user_title_before_save',    $this->user_title,    $this->id );
    2074         $this->date_modified = apply_filters( 'groups_member_date_modified_before_save', $this->date_modified, $this->id );
    2075         $this->is_confirmed  = apply_filters( 'groups_member_is_confirmed_before_save',  $this->is_confirmed,  $this->id );
    2076         $this->comments      = apply_filters( 'groups_member_comments_before_save',      $this->comments,      $this->id );
    2077         $this->invite_sent   = apply_filters( 'groups_member_invite_sent_before_save',   $this->invite_sent,   $this->id );
    2078 
    2079         do_action_ref_array( 'groups_member_before_save', array( &$this ) );
    2080 
    2081         if ( !empty( $this->id ) ) {
    2082             $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 );
    2083         } else {
    2084             // Ensure that user is not already a member of the group before inserting
    2085             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 ) ) ) {
    2086                 return false;
    2087             }
    2088 
    2089             $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 );
    2090         }
    2091 
    2092         if ( !$wpdb->query( $sql ) )
    2093             return false;
    2094 
    2095         $this->id = $wpdb->insert_id;
    2096 
    2097         // Update the user's group count
    2098         self::refresh_total_group_count_for_user( $this->user_id );
    2099 
    2100         // Update the group's member count
    2101         self::refresh_total_member_count_for_group( $this->group_id );
    2102 
    2103         do_action_ref_array( 'groups_member_after_save', array( &$this ) );
    2104 
    2105         return true;
    2106     }
    2107 
    2108     /**
    2109      * Promote a member to a new status.
    2110      *
    2111      * @param string $status The new status. 'mod' or 'admin'.
    2112      * @return bool True on success, false on failure.
    2113      */
    2114     public function promote( $status = 'mod' ) {
    2115         if ( 'mod' == $status ) {
    2116             $this->is_admin   = 0;
    2117             $this->is_mod     = 1;
    2118             $this->user_title = __( 'Group Mod', 'buddypress' );
    2119         }
    2120 
    2121         if ( 'admin' == $status ) {
    2122             $this->is_admin   = 1;
    2123             $this->is_mod     = 0;
    2124             $this->user_title = __( 'Group Admin', 'buddypress' );
    2125         }
    2126 
    2127         return $this->save();
    2128     }
    2129 
    2130     /**
    2131      * Demote membership to Member status (non-admin, non-mod).
    2132      *
    2133      * @return bool True on success, false on failure.
    2134      */
    2135     public function demote() {
    2136         $this->is_mod     = 0;
    2137         $this->is_admin   = 0;
    2138         $this->user_title = false;
    2139 
    2140         return $this->save();
    2141     }
    2142 
    2143     /**
    2144      * Ban the user from the group.
    2145      *
    2146      * @return bool True on success, false on failure.
    2147      */
    2148     public function ban() {
    2149         if ( !empty( $this->is_admin ) )
    2150             return false;
    2151 
    2152         $this->is_mod = 0;
    2153         $this->is_banned = 1;
    2154 
    2155         return $this->save();
    2156     }
    2157 
    2158     /**
    2159      * Unban the user from the group.
    2160      *
    2161      * @return bool True on success, false on failure.
    2162      */
    2163     public function unban() {
    2164         if ( !empty( $this->is_admin ) )
    2165             return false;
    2166 
    2167         $this->is_banned = 0;
    2168 
    2169         return $this->save();
    2170     }
    2171 
    2172     /**
    2173      * Mark a pending invitation as accepted.
    2174      */
    2175     public function accept_invite() {
    2176         $this->inviter_id    = 0;
    2177         $this->is_confirmed  = 1;
    2178         $this->date_modified = bp_core_current_time();
    2179     }
    2180 
    2181     /**
    2182      * Confirm a membership request.
    2183      */
    2184     public function accept_request() {
    2185         $this->is_confirmed = 1;
    2186         $this->date_modified = bp_core_current_time();
    2187     }
    2188 
    2189     /**
    2190      * Remove the current membership.
    2191      *
    2192      * @return bool True on success, false on failure.
    2193      */
    2194     public function remove() {
    2195         global $wpdb;
    2196 
    2197         $bp  = buddypress();
    2198         $sql = $wpdb->prepare( "DELETE FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d", $this->user_id, $this->group_id );
    2199 
    2200         if ( !$result = $wpdb->query( $sql ) )
    2201             return false;
    2202 
    2203         // Update the user's group count
    2204         self::refresh_total_group_count_for_user( $this->user_id );
    2205 
    2206         // Update the group's member count
    2207         self::refresh_total_member_count_for_group( $this->group_id );
    2208 
    2209         return $result;
    2210     }
    2211 
    2212     /** Static Methods ****************************************************/
    2213 
    2214     /**
    2215      * Refresh the total_group_count for a user.
    2216      *
    2217      * @since BuddyPress (1.8.0)
    2218      *
    2219      * @param int $user_id ID of the user.
    2220      * @return bool True on success, false on failure.
    2221      */
    2222     public static function refresh_total_group_count_for_user( $user_id ) {
    2223         return bp_update_user_meta( $user_id, 'total_group_count', (int) self::total_group_count( $user_id ) );
    2224     }
    2225 
    2226     /**
    2227      * Refresh the total_member_count for a group.
    2228      *
    2229      * @since BuddyPress (1.8.0)
    2230      *
    2231      * @param int $group_id ID of the group.
    2232      * @return bool True on success, false on failure.
    2233      */
    2234     public static function refresh_total_member_count_for_group( $group_id ) {
    2235         return groups_update_groupmeta( $group_id, 'total_member_count', (int) BP_Groups_Group::get_total_member_count( $group_id ) );
    2236     }
    2237 
    2238     /**
    2239      * Delete a membership, based on user + group IDs.
    2240      *
    2241      * @param int $user_id ID of the user.
    2242      * @param int $group_id ID of the group.
    2243      * @return True on success, false on failure.
    2244      */
    2245     public static function delete( $user_id, $group_id ) {
    2246         global $wpdb;
    2247 
    2248         $bp = buddypress();
    2249         $remove = $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->groups->table_name_members} WHERE user_id = %d AND group_id = %d", $user_id, $group_id ) );
    2250 
    2251         // Update the user's group count
    2252         self::refresh_total_group_count_for_user( $user_id );
    2253 
    2254         // Update the group's member count
    2255         self::refresh_total_member_count_for_group( $group_id );
    2256 
    2257         return $remove;
    2258     }
    2259 
    2260     /**
    2261      * Get the IDs of the groups of which a specified user is a member.
    2262      *
    2263      * @param int $user_id ID of the user.
    2264      * @param int $limit Optional. Max number of results to return.
    2265      *        Default: false (no limit).
    2266      * @param int $page Optional. Page offset of results to return.
    2267      *        Default: false (no limit).
    2268      * @return array {
    2269      *     @type array $groups Array of groups returned by paginated query.
    2270      *     @type int $total Count of groups matching query.
    2271      * }
    2272      */
    2273     public static function get_group_ids( $user_id, $limit = false, $page = false ) {
    2274         global $wpdb;
    2275 
    2276         $pag_sql = '';
    2277         if ( !empty( $limit ) && !empty( $page ) )
    2278             $pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
    2279 
    2280         $bp = buddypress();
    2281 
    2282         // If the user is logged in and viewing their own groups, we can show hidden and private groups
    2283         if ( $user_id != bp_loggedin_user_id() ) {
    2284             $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 );
    2285             $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 ) );
    2286         } else {
    2287             $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 );
    2288             $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 ) );
    2289         }
    2290 
    2291         $groups = $wpdb->get_col( $group_sql );
    2292 
    2293         return array( 'groups' => $groups, 'total' => (int) $total_groups );
    2294     }
    2295 
    2296     /**
    2297      * Get the IDs of the groups of which a specified user is a member, sorted by the date joined.
    2298      *
    2299      * @param int $user_id ID of the user.
    2300      * @param int $limit Optional. Max number of results to return.
    2301      *        Default: false (no limit).
    2302      * @param int $page Optional. Page offset of results to return.
    2303      *        Default: false (no limit).
    2304      * @param string $filter Optional. Limit results to groups whose name or
    2305      *        description field matches search terms.
    2306      * @return array {
    2307      *     @type array $groups Array of groups returned by paginated query.
    2308      *     @type int $total Count of groups matching query.
    2309      * }
    2310      */
    2311     public static function get_recently_joined( $user_id, $limit = false, $page = false, $filter = false ) {
    2312         global $wpdb;
    2313 
    2314         $user_id_sql = $pag_sql = $hidden_sql = $filter_sql = '';
    2315 
    2316         $user_id_sql = $wpdb->prepare( 'm.user_id = %d', $user_id );
    2317 
    2318         if ( !empty( $limit ) && !empty( $page ) )
    2319             $pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
    2320 
    2321         if ( !empty( $filter ) ) {
    2322             $search_terms_like = '%' . bp_esc_like( $filter ) . '%';
    2323             $filter_sql = $wpdb->prepare( " AND ( g.name LIKE %s OR g.description LIKE %s )", $search_terms_like, $search_terms_like );
    2324         }
    2325 
    2326         if ( $user_id != bp_loggedin_user_id() )
    2327             $hidden_sql = " AND g.status != 'hidden'";
    2328 
    2329         $bp = buddypress();
    2330 
    2331         $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}{$filter_sql} AND {$user_id_sql} AND m.is_confirmed = 1 AND m.is_banned = 0 ORDER BY m.date_modified DESC {$pag_sql}" );
    2332         $total_groups = $wpdb->get_var( "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 {$user_id_sql} AND m.is_banned = 0 AND m.is_confirmed = 1 ORDER BY m.date_modified DESC" );
    2333 
    2334         return array( 'groups' => $paged_groups, 'total' => $total_groups );
    2335     }
    2336 
    2337     /**
    2338      * Get the IDs of the groups of which a specified user is an admin.
    2339      *
    2340      * @param int $user_id ID of the user.
    2341      * @param int $limit Optional. Max number of results to return.
    2342      *        Default: false (no limit).
    2343      * @param int $page Optional. Page offset of results to return.
    2344      *        Default: false (no limit).
    2345      * @param string $filter Optional. Limit results to groups whose name or
    2346      *        description field matches search terms.
    2347      * @return array {
    2348      *     @type array $groups Array of groups returned by paginated query.
    2349      *     @type int $total Count of groups matching query.
    2350      * }
    2351      */
    2352     public static function get_is_admin_of( $user_id, $limit = false, $page = false, $filter = false ) {
    2353         global $wpdb;
    2354 
    2355         $user_id_sql = $pag_sql = $hidden_sql = $filter_sql = '';
    2356 
    2357         $user_id_sql = $wpdb->prepare( 'm.user_id = %d', $user_id );
    2358 
    2359         if ( !empty( $limit ) && !empty( $page ) )
    2360             $pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
    2361 
    2362         if ( !empty( $filter ) ) {
    2363             $search_terms_like = '%' . bp_esc_like( $filter ) . '%';
    2364             $filter_sql = $wpdb->prepare( " AND ( g.name LIKE %s OR g.description LIKE %s )", $search_terms_like, $search_terms_like );
    2365         }
    2366 
    2367         if ( $user_id != bp_loggedin_user_id() )
    2368             $hidden_sql = " AND g.status != 'hidden'";
    2369 
    2370         $bp = buddypress();
    2371 
    2372         $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}{$filter_sql} AND {$user_id_sql} AND m.is_confirmed = 1 AND m.is_banned = 0 AND m.is_admin = 1 ORDER BY m.date_modified ASC {$pag_sql}" );
    2373         $total_groups = $wpdb->get_var( "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 {$user_id_sql} AND m.is_confirmed = 1 AND m.is_banned = 0 AND m.is_admin = 1 ORDER BY date_modified ASC" );
    2374 
    2375         return array( 'groups' => $paged_groups, 'total' => $total_groups );
    2376     }
    2377 
    2378     /**
    2379      * Get the IDs of the groups of which a specified user is a moderator.
    2380      *
    2381      * @param int $user_id ID of the user.
    2382      * @param int $limit Optional. Max number of results to return.
    2383      *        Default: false (no limit).
    2384      * @param int $page Optional. Page offset of results to return.
    2385      *        Default: false (no limit).
    2386      * @param string $filter Optional. Limit results to groups whose name or
    2387      *        description field matches search terms.
    2388      * @return array {
    2389      *     @type array $groups Array of groups returned by paginated query.
    2390      *     @type int $total Count of groups matching query.
    2391      * }
    2392      */
    2393     public static function get_is_mod_of( $user_id, $limit = false, $page = false, $filter = false ) {
    2394         global $wpdb;
    2395 
    2396         $user_id_sql = $pag_sql = $hidden_sql = $filter_sql = '';
    2397 
    2398         $user_id_sql = $wpdb->prepare( 'm.user_id = %d', $user_id );
    2399 
    2400         if ( !empty( $limit ) && !empty( $page ) )
    2401             $pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
    2402 
    2403         if ( !empty( $filter ) ) {
    2404             $search_terms_like = '%' . bp_esc_like( $filter ) . '%';
    2405             $filter_sql = $wpdb->prepare( " AND ( g.name LIKE %s OR g.description LIKE %s )", $search_terms_like, $search_terms_like );
    2406         }
    2407 
    2408         if ( $user_id != bp_loggedin_user_id() )
    2409             $hidden_sql = " AND g.status != 'hidden'";
    2410 
    2411         $bp = buddypress();
    2412 
    2413         $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}{$filter_sql} AND {$user_id_sql} AND m.is_confirmed = 1 AND m.is_banned = 0 AND m.is_mod = 1 ORDER BY m.date_modified ASC {$pag_sql}" );
    2414         $total_groups = $wpdb->get_var( "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 {$user_id_sql} AND m.is_confirmed = 1 AND m.is_banned = 0 AND m.is_mod = 1 ORDER BY date_modified ASC" );
    2415 
    2416         return array( 'groups' => $paged_groups, 'total' => $total_groups );
    2417     }
    2418 
    2419     /**
    2420      * Get the count of groups of which the specified user is a member.
    2421      *
    2422      * @param int $user_id Optional. Default: ID of the displayed user.
    2423      * @return int Group count.
    2424      */
    2425     public static function total_group_count( $user_id = 0 ) {
    2426         global $wpdb;
    2427 
    2428         if ( empty( $user_id ) )
    2429             $user_id = bp_displayed_user_id();
    2430 
    2431         $bp = buddypress();
    2432 
    2433         if ( $user_id != bp_loggedin_user_id() && !bp_current_user_can( 'bp_moderate' ) ) {
    2434             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 ) );
    2435         } else {
    2436             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 ) );
    2437         }
    2438     }
    2439 
    2440     /**
    2441      * Get a user's outstanding group invitations.
    2442      *
    2443      * @param int $user_id ID of the invitee.
    2444      * @param int $limit Optional. Max number of results to return.
    2445      *        Default: false (no limit).
    2446      * @param int $page Optional. Page offset of results to return.
    2447      *        Default: false (no limit).
    2448      * @param string|array $exclude Optional. Array or comma-separated list
    2449      *        of group IDs to exclude from results.
    2450      * @return array {
    2451      *     @type array $groups Array of groups returned by paginated query.
    2452      *     @type int $total Count of groups matching query.
    2453      * }
    2454      */
    2455     public static function get_invites( $user_id, $limit = false, $page = false, $exclude = false ) {
    2456         global $wpdb;
    2457 
    2458         $pag_sql = ( !empty( $limit ) && !empty( $page ) ) ? $wpdb->prepare( " LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) ) : '';
    2459 
    2460         if ( !empty( $exclude ) ) {
    2461             $exclude     = implode( ',', wp_parse_id_list( $exclude ) );
    2462             $exclude_sql = " AND g.id NOT IN ({$exclude})";
    2463         } else {
    2464             $exclude_sql = '';
    2465         }
    2466 
    2467         $bp = buddypress();
    2468 
    2469         $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 ) );
    2470 
    2471         return array( 'groups' => $paged_groups, 'total' => self::get_invite_count_for_user( $user_id ) );
    2472     }
    2473 
    2474     /**
    2475      * Gets the total group invite count for a user.
    2476      *
    2477      * @since BuddyPress (2.0.0)
    2478      *
    2479      * @param int $user_id The user ID
    2480      * @return int
    2481      */
    2482     public static function get_invite_count_for_user( $user_id = 0 ) {
    2483         global $wpdb;
    2484 
    2485         $bp = buddypress();
    2486 
    2487         $count = wp_cache_get( $user_id, 'bp_group_invite_count' );
    2488 
    2489         if ( false === $count ) {
    2490             $count = (int) $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", $user_id ) );
    2491             wp_cache_set( $user_id, $count, 'bp_group_invite_count' );
    2492         }
    2493 
    2494         return $count;
    2495     }
    2496 
    2497     /**
    2498      * Check whether a user has an outstanding invitation to a given group.
    2499      *
    2500      * @param int $user_id ID of the potential invitee.
    2501      * @param int $group_id ID of the group.
    2502      * @param string $type If 'sent', results are limited to those
    2503      *        invitations that have actually been sent (non-draft).
    2504      *        Default: 'sent'.
    2505      * @return int|null The ID of the invitation if found, otherwise null.
    2506      */
    2507     public static function check_has_invite( $user_id, $group_id, $type = 'sent' ) {
    2508         global $wpdb;
    2509 
    2510         if ( empty( $user_id ) )
    2511             return false;
    2512 
    2513         $bp  = buddypress();
    2514         $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";
    2515 
    2516         if ( 'sent' == $type )
    2517             $sql .= " AND invite_sent = 1";
    2518 
    2519         return $wpdb->get_var( $wpdb->prepare( $sql, $user_id, $group_id ) );
    2520     }
    2521 
    2522     /**
    2523      * Delete an invitation, by specifying user ID and group ID.
    2524      *
    2525      * @param int $user_id ID of the user.
    2526      * @param int $group_id ID of the group.
    2527      * @return int Number of records deleted.
    2528      */
    2529     public static function delete_invite( $user_id, $group_id ) {
    2530         global $wpdb;
    2531 
    2532         if ( empty( $user_id ) )
    2533             return false;
    2534 
    2535         $bp = buddypress();
    2536 
    2537         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 ) );
    2538     }
    2539 
    2540     /**
    2541      * Delete an unconfirmed membership request, by user ID and group ID.
    2542      *
    2543      * @param int $user_id ID of the user.
    2544      * @param int $group_id ID of the group.
    2545      * @return int Number of records deleted.
    2546      */
    2547     public static function delete_request( $user_id, $group_id ) {
    2548         global $wpdb;
    2549 
    2550         if ( empty( $user_id ) )
    2551             return false;
    2552 
    2553         $bp = buddypress();
    2554 
    2555         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 ) );
    2556     }
    2557 
    2558     /**
    2559      * Check whether a user is an admin of a given group.
    2560      *
    2561      * @param int $user_id ID of the user.
    2562      * @param int $group_id ID of the group.
    2563      * @param int|null ID of the membership if the user is an admin,
    2564      *        otherwise null.
    2565      */
    2566     public static function check_is_admin( $user_id, $group_id ) {
    2567         global $wpdb;
    2568 
    2569         if ( empty( $user_id ) )
    2570             return false;
    2571 
    2572         $bp = buddypress();
    2573 
    2574         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 ) );
    2575     }
    2576 
    2577     /**
    2578      * Check whether a user is a mod of a given group.
    2579      *
    2580      * @param int $user_id ID of the user.
    2581      * @param int $group_id ID of the group.
    2582      * @param int|null ID of the membership if the user is a mod,
    2583      *        otherwise null.
    2584      */
    2585     public static function check_is_mod( $user_id, $group_id ) {
    2586         global $wpdb;
    2587 
    2588         if ( empty( $user_id ) )
    2589             return false;
    2590 
    2591         $bp = buddypress();
    2592 
    2593         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 ) );
    2594     }
    2595 
    2596     /**
    2597      * Check whether a user is a member of a given group.
    2598      *
    2599      * @param int $user_id ID of the user.
    2600      * @param int $group_id ID of the group.
    2601      * @param int|null ID of the membership if the user is a member,
    2602      *        otherwise null.
    2603      */
    2604     public static function check_is_member( $user_id, $group_id ) {
    2605         global $wpdb;
    2606 
    2607         if ( empty( $user_id ) )
    2608             return false;
    2609 
    2610         $bp = buddypress();
    2611 
    2612         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 ) );
    2613     }
    2614 
    2615     /**
    2616      * Check whether a user is banned from a given group.
    2617      *
    2618      * @param int $user_id ID of the user.
    2619      * @param int $group_id ID of the group.
    2620      * @param int|null ID of the membership if the user is banned,
    2621      *        otherwise null.
    2622      */
    2623     public static function check_is_banned( $user_id, $group_id ) {
    2624         global $wpdb;
    2625 
    2626         if ( empty( $user_id ) )
    2627             return false;
    2628 
    2629         $bp = buddypress();
    2630 
    2631         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 ) );
    2632     }
    2633 
    2634     /**
    2635      * Is the specified user the creator of the group?
    2636      *
    2637      * @since BuddyPress (1.2.6)
    2638      *
    2639      * @param int $user_id ID of the user.
    2640      * @param int $group_id ID of the group.
    2641      * @return int|null ID of the group if the user is the creator,
    2642      *         otherwise false.
    2643      */
    2644     public static function check_is_creator( $user_id, $group_id ) {
    2645         global $wpdb;
    2646 
    2647         if ( empty( $user_id ) )
    2648             return false;
    2649 
    2650         $bp = buddypress();
    2651 
    2652         return $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$bp->groups->table_name} WHERE creator_id = %d AND id = %d", $user_id, $group_id ) );
    2653     }
    2654 
    2655     /**
    2656      * Check whether a user has an outstanding membership request for a given group.
    2657      *
    2658      * @param int $user_id ID of the user.
    2659      * @param int $group_id ID of the group.
    2660      * @return int|null ID of the membership if found, otherwise false.
    2661      */
    2662     public static function check_for_membership_request( $user_id, $group_id ) {
    2663         global $wpdb;
    2664 
    2665         if ( empty( $user_id ) )
    2666             return false;
    2667 
    2668         $bp = buddypress();
    2669 
    2670         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 ) );
    2671     }
    2672 
    2673     /**
    2674      * Get a list of randomly selected IDs of groups that the member belongs to.
    2675      *
    2676      * @param int $user_id ID of the user.
    2677      * @param int $total_groups Max number of group IDs to return. Default: 5.
    2678      * @return array Group IDs.
    2679      */
    2680     public static function get_random_groups( $user_id = 0, $total_groups = 5 ) {
    2681         global $wpdb;
    2682 
    2683         $bp = buddypress();
    2684 
    2685         // If the user is logged in and viewing their random groups, we can show hidden and private groups
    2686         if ( bp_is_my_profile() ) {
    2687             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 ) );
    2688         } else {
    2689             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 ) );
    2690         }
    2691     }
    2692 
    2693     /**
    2694      * Get the IDs of all a given group's members.
    2695      *
    2696      * @param int $group_id ID of the group.
    2697      * @return array IDs of all group members.
    2698      */
    2699     public static function get_group_member_ids( $group_id ) {
    2700         global $wpdb;
    2701 
    2702         $bp = buddypress();
    2703 
    2704         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 ) );
    2705     }
    2706 
    2707     /**
    2708      * Get a list of all a given group's admins.
    2709      *
    2710      * @param int $group_id ID of the group.
    2711      * @return array Info about group admins (user_id + date_modified).
    2712      */
    2713     public static function get_group_administrator_ids( $group_id ) {
    2714         global $wpdb;
    2715 
    2716         $group_admins = wp_cache_get( $group_id, 'bp_group_admins' );
    2717 
    2718         if ( false === $group_admins ) {
    2719             $bp = buddypress();
    2720             $group_admins = $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 ) );
    2721 
    2722             wp_cache_set( $group_id, $group_admins, 'bp_group_admins' );
    2723         }
    2724 
    2725         return $group_admins;
    2726     }
    2727 
    2728     /**
    2729      * Get a list of all a given group's moderators.
    2730      *
    2731      * @param int $group_id ID of the group.
    2732      * @return array Info about group mods (user_id + date_modified).
    2733      */
    2734     public static function get_group_moderator_ids( $group_id ) {
    2735         global $wpdb;
    2736 
    2737         $bp = buddypress();
    2738 
    2739         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 ) );
    2740     }
    2741 
    2742     /**
    2743      * Get the IDs users with outstanding membership requests to the group.
    2744      *
    2745      * @param int $group_id ID of the group.
    2746      * @return array IDs of users with outstanding membership requests.
    2747      */
    2748     public static function get_all_membership_request_user_ids( $group_id ) {
    2749         global $wpdb;
    2750 
    2751         $bp = buddypress();
    2752 
    2753         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 ) );
    2754     }
    2755 
    2756     /**
    2757      * Get members of a group.
    2758      *
    2759      * @deprecated BuddyPress (1.8.0)
    2760      */
    2761     public static function get_all_for_group( $group_id, $limit = false, $page = false, $exclude_admins_mods = true, $exclude_banned = true, $exclude = false ) {
    2762         global $wpdb;
    2763 
    2764         _deprecated_function( __METHOD__, '1.8', 'BP_Group_Member_Query' );
    2765 
    2766         $pag_sql = '';
    2767         if ( !empty( $limit ) && !empty( $page ) )
    2768             $pag_sql = $wpdb->prepare( "LIMIT %d, %d", intval( ( $page - 1 ) * $limit), intval( $limit ) );
    2769 
    2770         $exclude_admins_sql = '';
    2771         if ( !empty( $exclude_admins_mods ) )
    2772             $exclude_admins_sql = "AND is_admin = 0 AND is_mod = 0";
    2773 
    2774         $banned_sql = '';
    2775         if ( !empty( $exclude_banned ) )
    2776             $banned_sql = " AND is_banned = 0";
    2777 
    2778         $exclude_sql = '';
    2779         if ( !empty( $exclude ) ) {
    2780             $exclude     = implode( ',', wp_parse_id_list( $exclude ) );
    2781             $exclude_sql = " AND m.user_id NOT IN ({$exclude})";
    2782         }
    2783 
    2784         $bp = buddypress();
    2785 
    2786         if ( bp_is_active( 'xprofile' ) ) {
    2787             $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 ) ) );
    2788         } else {
    2789             $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 ) ) );
    2790         }
    2791 
    2792         if ( empty( $members ) ) {
    2793             return false;
    2794         }
    2795 
    2796         if ( empty( $pag_sql ) ) {
    2797             $total_member_count = count( $members );
    2798         } else {
    2799             $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 ) ) );
    2800         }
    2801 
    2802         // Fetch whether or not the user is a friend
    2803         foreach ( (array) $members as $user )
    2804             $user_ids[] = $user->user_id;
    2805 
    2806         $user_ids = implode( ',', wp_parse_id_list( $user_ids ) );
    2807 
    2808         if ( bp_is_active( 'friends' ) ) {
    2809             $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() ) );
    2810             for ( $i = 0, $count = count( $members ); $i < $count; ++$i ) {
    2811                 foreach ( (array) $friend_status as $status ) {
    2812                     if ( $status->initiator_user_id == $members[$i]->user_id || $status->friend_user_id == $members[$i]->user_id ) {
    2813                         $members[$i]->is_friend = $status->is_confirmed;
    2814                     }
    2815                 }
    2816             }
    2817         }
    2818 
    2819         return array( 'members' => $members, 'count' => $total_member_count );
    2820     }
    2821 
    2822     /**
    2823      * Delete all memberships for a given group.
    2824      *
    2825      * @param int $group_id ID of the group.
    2826      * @return int Number of records deleted.
    2827      */
    2828     public static function delete_all( $group_id ) {
    2829         global $wpdb;
    2830 
    2831         $bp = buddypress();
    2832 
    2833         return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->groups->table_name_members} WHERE group_id = %d", $group_id ) );
    2834     }
    2835 
    2836     /**
    2837      * Delete all group membership information for the specified user.
    2838      *
    2839      * @since BuddyPress (1.0.0)
    2840      *
    2841      * @param int $user_id ID of the user.
    2842      */
    2843     public static function delete_all_for_user( $user_id ) {
    2844         global $wpdb;
    2845 
    2846         $bp = buddypress();
    2847 
    2848         // Get all the group ids for the current user's groups and update counts
    2849         $group_ids = BP_Groups_Member::get_group_ids( $user_id );
    2850         foreach ( $group_ids['groups'] as $group_id ) {
    2851             groups_update_groupmeta( $group_id, 'total_member_count', groups_get_total_member_count( $group_id ) - 1 );
    2852 
    2853             // If current user is the creator of a group and is the sole admin, delete that group to avoid counts going out-of-sync
    2854             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 ) )
    2855                 groups_delete_group( $group_id );
    2856         }
    2857 
    2858         return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->groups->table_name_members} WHERE user_id = %d", $user_id ) );
    2859     }
    2860 }
    2861 
    2862 /**
    2863  * API for creating group extensions without having to hardcode the content into
    2864  * the theme.
    2865  *
    2866  * To implement, extend this class. In your constructor, pass an optional array
    2867  * of arguments to parent::init() to configure your widget. The config array
    2868  * supports the following values:
    2869  *   - 'slug' A unique identifier for your extension. This value will be used
    2870  *     to build URLs, so make it URL-safe.
    2871  *   - 'name' A translatable name for your extension. This value is used to
    2872  *     populate the navigation tab, as well as the default titles for admin/
    2873  *     edit/create tabs.
    2874  *   - 'visibility' Set to 'public' (default) for your extension (the main tab
    2875  *     as well as the widget) to be available to anyone who can access the
    2876  *     group, 'private' otherwise.
    2877  *   - 'nav_item_position' An integer explaining where the nav item should
    2878  *     appear in the tab list.
    2879  *   - 'enable_nav_item' Set to true for your extension's main tab to be
    2880  *     available to anyone who can access the group.
    2881  *   - 'nav_item_name' The translatable text you want to appear in the nav tab.
    2882  *     Defaults to the value of 'name'.
    2883  *   - 'display_hook' The WordPress action that the widget_display() method is
    2884  *     hooked to.
    2885  *   - 'template_file' The template file that will be used to load the content
    2886  *     of your main extension tab. Defaults to 'groups/single/plugins.php'.
    2887  *   - 'screens' A multi-dimensional array, described below.
    2888  *   - 'access' Which users can visit the plugin's tab.
    2889  *   - 'show_tab' Which users can see the plugin's navigation tab.
    2890  *
    2891  * BP_Group_Extension uses the concept of "settings screens". There are three
    2892  * contexts for settings screens:
    2893  *   - 'create', which inserts a new step into the group creation process
    2894  *   - 'edit', which adds a tab for your extension into the Admin section of
    2895  *     a group
    2896  *   - 'admin', which adds a metabox to the Groups administration panel in the
    2897  *     WordPress Dashboard
    2898  * Each of these settings screens is populated by a pair of methods: one that
    2899  * creates the markup for the screen, and one that processes form data
    2900  * submitted from the screen. If your plugin needs screens in all three
    2901  * contexts, and if the markup and form processing logic will be the same in
    2902  * each case, you can define two methods to handle all of the screens:
    2903  *   function settings_screen() {}
    2904  *   function settings_screen_save() {}
    2905  * If one or more of your settings screen needs separate logic, you may define
    2906  * context-specific methods, for example:
    2907  *   function edit_screen() {}
    2908  *   function edit_screen_save() {}
    2909  * BP_Group_Extension will use the more specific methods if they are available.
    2910  *
    2911  * You can further customize the settings screens (tab names, etc) by passing
    2912  * an optional 'screens' parameter to the init array. The format is as follows:
    2913  *   'screens' => array(
    2914  *       'create' => array(
    2915  *       'slug' => 'foo',
    2916  *       'name' => 'Foo',
    2917  *       'position' => 55,
    2918  *       'screen_callback' => 'my_create_screen_callback',
    2919  *       'screen_save_callback' => 'my_create_screen_save_callback',
    2920  *   ),
    2921  *   'edit' => array( // ...
    2922  *   ),
    2923  * Only provide those arguments that you actually want to change from the
    2924  * default configuration. BP_Group_Extension will do the rest.
    2925  *
    2926  * Note that the 'edit' screen accepts an additional parameter: 'submit_text',
    2927  * which defines the text of the Submit button automatically added to the Edit
    2928  * screen of the extension (defaults to 'Save Changes'). Also, the 'admin'
    2929  * screen accepts two additional parameters: 'metabox_priority' and
    2930  * 'metabox_context'. See the docs for add_meta_box() for more details on these
    2931  * arguments.
    2932  *
    2933  * Prior to BuddyPress 1.7, group extension configurations were set slightly
    2934  * differently. The legacy method is still supported, though deprecated.
    2935  *
    2936  * @package BuddyPress
    2937  * @subpackage Groups
    2938  * @since BuddyPress (1.1.0)
    2939  */
    2940 class BP_Group_Extension {
    2941 
    2942     /** Public ************************************************************/
    2943 
    2944     /**
    2945      * Information about this extension's screens.
    2946      *
    2947      * @since BuddyPress (1.8.0)
    2948      * @access public
    2949      * @var array
    2950      */
    2951     public $screens = array();
    2952 
    2953     /**
    2954      * The name of the extending class.
    2955      *
    2956      * @since BuddyPress (1.8.0)
    2957      * @access public
    2958      * @var string
    2959      */
    2960     public $class_name = '';
    2961 
    2962     /**
    2963      * A ReflectionClass object of the current extension.
    2964      *
    2965      * @since BuddyPress (1.8.0)
    2966      * @access public
    2967      * @var ReflectionClass
    2968      */
    2969     public $class_reflection = null;
    2970 
    2971     /**
    2972      * Parsed configuration parameters for the extension.
    2973      *
    2974      * @since BuddyPress (1.8.0)
    2975      * @access public
    2976      * @var array
    2977      */
    2978     public $params = array();
    2979 
    2980     /**
    2981      * Raw config params, as passed by the extending class.
    2982      *
    2983      * @since BuddyPress (2.1.0)
    2984      * @access public
    2985      * @var array
    2986      */
    2987     public $params_raw = array();
    2988 
    2989     /**
    2990      * The ID of the current group.
    2991      *
    2992      * @since BuddyPress (1.8.0)
    2993      * @access public
    2994      * @var int
    2995      */
    2996     public $group_id = 0;
    2997 
    2998     /**
    2999      * The slug of the current extension.
    3000      *
    3001      * @access public
    3002      * @var string
    3003      */
    3004     public $slug = '';
    3005 
    3006     /**
    3007      * The translatable name of the current extension.
    3008      *
    3009      * @access public
    3010      * @var string
    3011      */
    3012     public $name = '';
    3013 
    3014     /**
    3015      * The visibility of the extension tab. 'public' or 'private'.
    3016      *
    3017      * @access public
    3018      * @var string
    3019      */
    3020     public $visibility = 'public';
    3021 
    3022     /**
    3023      * The numeric position of the main nav item.
    3024      *
    3025      * @access public
    3026      * @var int
    3027      */
    3028     public $nav_item_position = 81;
    3029 
    3030     /**
    3031      * Whether to show the nav item.
    3032      *
    3033      * @access public
    3034      * @var bool
    3035      */
    3036     public $enable_nav_item = true;
    3037 
    3038     /**
    3039      * Whether the current user should see the navigation item.
    3040      *
    3041      * @since BuddyPress (2.1.0)
    3042      * @access public
    3043      * @var bool
    3044      */
    3045     public $user_can_see_nav_item;
    3046 
    3047     /**
    3048      * Whether the current user can visit the tab.
    3049      *
    3050      * @since BuddyPress (2.1.0)
    3051      * @access public
    3052      * @var bool
    3053      */
    3054     public $user_can_visit;
    3055 
    3056     /**
    3057      * The text of the nav item. Defaults to self::name.
    3058      *
    3059      * @access public
    3060      * @var string
    3061      */
    3062     public $nav_item_name = '';
    3063 
    3064     /**
    3065      * The WP action that self::widget_display() is attached to.
    3066      *
    3067      * Default: 'groups_custom_group_boxes'.
    3068      *
    3069      * @access public
    3070      * @var string
    3071      */
    3072     public $display_hook = 'groups_custom_group_boxes';
    3073 
    3074     /**
    3075      * The template file used to load the plugin content.
    3076      *
    3077      * Default: 'groups/single/plugins'.
    3078      *
    3079      * @access public
    3080      * @var string
    3081      */
    3082     public $template_file = 'groups/single/plugins';
    3083 
    3084     /** Protected *********************************************************/
    3085 
    3086     /**
    3087      * Has the extension been initialized?
    3088      *
    3089      * @since BuddyPress (1.8.0)
    3090      * @access protected
    3091      * @var bool
    3092      */
    3093     protected $initialized = false;
    3094 
    3095     /**
    3096      * Extension properties as set by legacy extensions.
    3097      *
    3098      * @since BuddyPress (1.8.0)
    3099      * @access protected
    3100      * @var array
    3101      */
    3102     protected $legacy_properties = array();
    3103 
    3104     /**
    3105      * Converted legacy parameters.
    3106      *
    3107      * These are the extension properties as set by legacy extensions, but
    3108      * then converted to match the new format for params.
    3109      *
    3110      * @since BuddyPress (1.8.0)
    3111      * @access protected
    3112      * @var array
    3113      */
    3114     protected $legacy_properties_converted = array();
    3115 
    3116     /**
    3117      * Redirect location as defined by post-edit save callback.
    3118      *
    3119      * @since BuddyPress (2.1.0)
    3120      * @access protected
    3121      * @var string
    3122      */
    3123     protected $post_save_redirect;
    3124 
    3125     /**
    3126      * Miscellaneous data as set by the __set() magic method.
    3127      *
    3128      * @since BuddyPress (1.8.0)
    3129      * @access protected
    3130      * @var array
    3131      */
    3132     protected $data = array();
    3133 
    3134     /** Screen Overrides **************************************************/
    3135 
    3136     /*
    3137      * Screen override methods are how your extension will display content
    3138      * and handle form submits. Your extension should only override those
    3139      * methods that it needs for its purposes.
    3140      */
    3141 
    3142     // The content of the group tab
    3143     public function display( $group_id = null ) {}
    3144 
    3145     // Content displayed in a widget sidebar, if applicable
    3146     public function widget_display() {}
    3147 
    3148     // *_screen() displays the settings form for the given context
    3149     // *_screen_save() processes data submitted via the settings form
    3150     // The settings_* methods are generic fallbacks, which can optionally
    3151     // be overridden by the more specific edit_*, create_*, and admin_*
    3152     // versions.
    3153     public function settings_screen( $group_id = null ) {}
    3154     public function settings_screen_save( $group_id = null ) {}
    3155     public function edit_screen( $group_id = null ) {}
    3156     public function edit_screen_save( $group_id = null ) {}
    3157     public function create_screen( $group_id = null ) {}
    3158     public function create_screen_save( $group_id = null ) {}
    3159     public function admin_screen( $group_id = null ) {}
    3160     public function admin_screen_save( $group_id = null ) {}
    3161 
    3162     /** Setup *************************************************************/
    3163 
    3164     /**
    3165      * Initialize the extension, using your config settings
    3166      *
    3167      * Your plugin should call this method at the very end of its
    3168      * constructor, like so:
    3169      *
    3170      *   public function __construct() {
    3171      *       $args = array(
    3172      *           'slug' => 'my-group-extension',
    3173      *           'name' => 'My Group Extension',
    3174      *           // ...
    3175      *       );
    3176      *
    3177      *       parent::init( $args );
    3178      *   }
    3179      *
    3180      * @since BuddyPress (1.8.0)
    3181      * @since BuddyPress (2.1.0) Added 'access' and 'show_tab' arguments
    3182      *        to $args.
    3183      * @param array $args {
    3184      *     Array of initialization arguments.
    3185      *     @type string $slug Unique, URL-safe identifier for your
    3186      *           extension.
    3187      *     @type string $name Translatable name for your extension. Used to
    3188      *           populate navigation items.
    3189      *     @type string $visibility Optional. Set to 'public' for your
    3190      *           extension (the main tab as well as the widget) to be
    3191      *           available to anyone who can access the group; set to
    3192      *           'private' otherwise. Default: 'public'.
    3193      *     @type int $nav_item_position Optional. Location of the nav item
    3194      *           in the tab list. Default: 81.
    3195      *     @type bool $enable_nav_item Optional. Whether the extension's
    3196      *           tab should be accessible to anyone who can view the group.
    3197      *           Default: true.
    3198      *     @type string $nav_item_name Optional. The translatable text you
    3199      *           want to appear in the nav tab. Default: the value of $name.
    3200      *     @type string $display_hook Optional. The WordPress action that
    3201      *           the widget_display() method is hooked to.
    3202      *           Default: 'groups_custom_group_boxes'.
    3203      *     @type string $template_file Optional. Theme-relative path to the
    3204      *           template file BP should use to load the content of your
    3205      *           main extension tab. Default: 'groups/single/plugins.php'.
    3206      *     @type array $screens A multi-dimensional array of configuration
    3207      *           information for the extension screens. See docblock of
    3208      *           {@link BP_Group_Extension} for more details.
    3209      *     @type string $access Which users can visit the plugin's tab.
    3210      *           Possible values: 'anyone', 'loggedin', 'member',
    3211      *           'mod', 'admin' or 'noone'
    3212      *           ('member', 'mod', 'admin' refer to user's role in group.)
    3213      *           Defaults to 'anyone' for public groups and 'member' for
    3214      *           private groups.
    3215      *     @type string $show_tab Which users can see the plugin's navigation
    3216      *           tab.
    3217      *           Possible values: 'anyone', 'loggedin', 'member',
    3218      *           'mod', 'admin' or 'noone'
    3219      *           ('member', 'mod', 'admin' refer to user's role in group.)
    3220      *           Defaults to 'anyone' for public groups and 'member' for
    3221      *           private groups.
    3222      * }
    3223      */
    3224     public function init( $args = array() ) {
    3225         // Store the raw arguments
    3226         $this->params_raw = $args;
    3227 
    3228         // Before this init() method was introduced, plugins were
    3229         // encouraged to set their config directly. For backward
    3230         // compatibility with these plugins, we detect whether this is
    3231         // one of those legacy plugins, and parse any legacy arguments
    3232         // with those passed to init()
    3233         $this->parse_legacy_properties();
    3234         $args = $this->parse_args_r( $args, $this->legacy_properties_converted );
    3235 
    3236         // Parse with defaults
    3237         $this->params = $this->parse_args_r( $args, array(
    3238             'slug'              => $this->slug,
    3239             'name'              => $this->name,
    3240             'visibility'        => $this->visibility,
    3241             'nav_item_position' => $this->nav_item_position,
    3242             'enable_nav_item'   => (bool) $this->enable_nav_item,
    3243             'nav_item_name'     => $this->nav_item_name,
    3244             'display_hook'      => $this->display_hook,
    3245             'template_file'     => $this->template_file,
    3246             'screens'           => $this->get_default_screens(),
    3247             'access'            => null,
    3248             'show_tab'          => null,
    3249         ) );
    3250 
    3251         $this->initialized = true;
    3252     }
    3253 
    3254     /**
    3255      * The main setup routine for the extension.
    3256      *
    3257      * This method contains the primary logic for setting up an extension's
    3258      * configuration, setting up backward compatibility for legacy plugins,
    3259      * and hooking the extension's screen functions into WP and BP.
    3260      *
    3261      * Marked 'public' because it must be accessible to add_action().
    3262      * However, you should never need to invoke this method yourself - it
    3263      * is called automatically at the right point in the load order by
    3264      * bp_register_group_extension().
    3265      *
    3266      * @since BuddyPress (1.1.0)
    3267      */
    3268     public function _register() {
    3269 
    3270         // Detect and parse properties set by legacy extensions
    3271         $this->parse_legacy_properties();
    3272 
    3273         // Initialize, if necessary. This should only happen for
    3274         // legacy extensions that don't call parent::init() themselves
    3275         if ( true !== $this->initialized ) {
    3276             $this->init();
    3277         }
    3278 
    3279         // Set some config values, based on the parsed params
    3280         $this->group_id          = $this->get_group_id();
    3281         $this->slug              = $this->params['slug'];
    3282         $this->name              = $this->params['name'];
    3283         $this->visibility        = $this->params['visibility'];
    3284         $this->nav_item_position = $this->params['nav_item_position'];
    3285         $this->nav_item_name     = $this->params['nav_item_name'];
    3286         $this->display_hook      = $this->params['display_hook'];
    3287         $this->template_file     = $this->params['template_file'];
    3288 
    3289         // Configure 'screens': create, admin, and edit contexts
    3290         $this->setup_screens();
    3291 
    3292         // Configure access-related settings
    3293         $this->setup_access_settings();
    3294 
    3295         // Mirror configuration data so it's accessible to plugins
    3296         // that look for it in its old locations
    3297         $this->setup_legacy_properties();
    3298 
    3299         // Hook the extension into BuddyPress
    3300         $this->setup_display_hooks();
    3301         $this->setup_create_hooks();
    3302         $this->setup_edit_hooks();
    3303         $this->setup_admin_hooks();
    3304     }
    3305 
    3306     /**
    3307      * Set up some basic info about the Extension.
    3308      *
    3309      * Here we collect the name of the extending class, as well as a
    3310      * ReflectionClass that is used in get_screen_callback() to determine
    3311      * whether your extension overrides certain callback methods.
    3312      *
    3313      * @since BuddyPress (1.8.0)
    3314      */
    3315     protected function setup_class_info() {
    3316         if ( empty( $this->class_name ) ) {
    3317             $this->class_name = get_class( $this );
    3318         }
    3319 
    3320         if ( is_null( $this->class_reflection ) ) {
    3321             $this->class_reflection = new ReflectionClass( $this->class_name );
    3322         }
    3323     }
    3324 
    3325     /**
    3326      * Get the current group ID.
    3327      *
    3328      * Check for:
    3329      *   - current group
    3330      *   - new group
    3331      *   - group admin
    3332      *
    3333      * @since BuddyPress (1.8.0)
    3334      *
    3335      * @return int
    3336      */
    3337     public static function get_group_id() {
    3338 
    3339         // Usually this will work
    3340         $group_id = bp_get_current_group_id();
    3341 
    3342         // On the admin, get the group id out of the $_GET params
    3343         if ( empty( $group_id ) && is_admin() && ( isset( $_GET['page'] ) && ( 'bp-groups' === $_GET['page'] ) ) && ! empty( $_GET['gid'] ) ) {
    3344             $group_id = (int) $_GET['gid'];
    3345         }
    3346 
    3347         // This fallback will only be hit when the create step is very
    3348         // early
    3349         if ( empty( $group_id ) && bp_get_new_group_id() ) {
    3350             $group_id = bp_get_new_group_id();
    3351         }
    3352 
    3353         // On some setups, the group id has to be fetched out of the
    3354         // $_POST array
    3355         // @todo Figure out why this is happening during group creation
    3356         if ( empty( $group_id ) && isset( $_POST['group_id'] ) ) {
    3357             $group_id = (int) $_POST['group_id'];
    3358         }
    3359 
    3360         return $group_id;
    3361     }
    3362 
    3363     /**
    3364      * Gather configuration data about your screens.
    3365      *
    3366      * @since BuddyPress (1.8.0)
    3367      *
    3368      * @return array
    3369      */
    3370     protected function get_default_screens() {
    3371         $this->setup_class_info();
    3372 
    3373         $screens = array(
    3374             'create' => array(
    3375                 'position' => 81,
    3376             ),
    3377             'edit'   => array(
    3378                 'submit_text' => __( 'Save Changes', 'buddypress' ),
    3379             ),
    3380             'admin'  => array(
    3381                 'metabox_context'  => 'normal',
    3382                 'metabox_priority' => 'core',
    3383             ),
    3384         );
    3385 
    3386         foreach ( $screens as $context => &$screen ) {
    3387             $screen['enabled']     = true;
    3388             $screen['name']        = $this->name;
    3389             $screen['slug']        = $this->slug;
    3390 
    3391             $screen['screen_callback']      = $this->get_screen_callback( $context, 'screen'      );
    3392             $screen['screen_save_callback'] = $this->get_screen_callback( $context, 'screen_save' );
    3393         }
    3394 
    3395         return $screens;
    3396     }
    3397 
    3398     /**
    3399      * Set up screens array based on params.
    3400      *
    3401      * @since BuddyPress (1.8.0)
    3402      */
    3403     protected function setup_screens() {
    3404         foreach ( (array) $this->params['screens'] as $context => $screen ) {
    3405             if ( empty( $screen['slug'] ) ) {
    3406                 $screen['slug'] = $this->slug;
    3407             }
    3408 
    3409             if ( empty( $screen['name'] ) ) {
    3410                 $screen['name'] = $this->name;
    3411             }
    3412 
    3413             $this->screens[ $context ] = $screen;
    3414         }
    3415     }
    3416 
    3417     /**
    3418      * Set up access-related settings for this extension.
    3419      *
    3420      * @since BuddyPress (2.1.0)
    3421      */
    3422     protected function setup_access_settings() {
    3423         // Bail if no group ID is available
    3424         if ( empty( $this->group_id ) ) {
    3425             return;
    3426         }
    3427 
    3428         // Backward compatibility
    3429         if ( isset( $this->params['enable_nav_item'] ) ) {
    3430             $this->enable_nav_item = (bool) $this->params['enable_nav_item'];
    3431         }
    3432 
    3433         // Tab Access
    3434         $this->user_can_visit = false;
    3435 
    3436         // Backward compatibility for components that do not provide
    3437         // explicit 'access' parameter
    3438         if ( empty( $this->params['access'] ) ) {
    3439             if ( false === $this->enable_nav_item ) {
    3440                 $this->params['access'] = 'noone';
    3441             } else {
    3442                 $group = groups_get_group( array(
    3443                     'group_id' => $this->group_id,
    3444                 ) );
    3445 
    3446                 if ( ! empty( $group->status ) && 'public' === $group->status ) {
    3447                     // Tabs in public groups are accessible to anyone by default
    3448                     $this->params['access'] = 'anyone';
    3449                 } else {
    3450                     // All other groups have members-only as the default
    3451                     $this->params['access'] = 'member';
    3452                 }
    3453             }
    3454         }
    3455 
    3456         // Parse multiple access conditions into an array
    3457         $access_conditions = $this->params['access'];
    3458         if ( ! is_array( $access_conditions ) ) {
    3459             $access_conditions = explode( ',', $access_conditions );
    3460         }
    3461 
    3462         // If the current user meets at least one condition, the
    3463         // get access
    3464         foreach ( $access_conditions as $access_condition ) {
    3465             if ( $this->user_meets_access_condition( $access_condition ) ) {
    3466                 $this->user_can_visit = true;
    3467                 break;
    3468             }
    3469         }
    3470 
    3471         // Tab Visibility
    3472         $this->user_can_see_nav_item = false;
    3473 
    3474         // Backward compatibility for components that do not provide
    3475         // explicit 'show_tab' parameter
    3476         if ( empty( $this->params['show_tab'] ) ) {
    3477             if ( false === $this->params['enable_nav_item'] ) {
    3478                 // enable_nav_item is only false if it's been
    3479                 // defined explicitly as such in the
    3480                 // constructor. So we always trust this value
    3481                 $this->params['show_tab'] = 'noone';
    3482 
    3483             } elseif ( isset( $this->params_raw['enable_nav_item'] ) || isset( $this->params_raw['visibility'] ) ) {
    3484                 // If enable_nav_item or visibility is passed,
    3485                 // we assume this  is a legacy extension.
    3486                 // Legacy behavior is that enable_nav_item=true +
    3487                 // visibility=private implies members-only
    3488                 if ( 'public' !== $this->visibility ) {
    3489                     $this->params['show_tab'] = 'member';
    3490                 } else {
    3491                     $this->params['show_tab'] = 'anyone';
    3492                 }
    3493 
    3494             } else {
    3495                 // No show_tab or enable_nav_item value is
    3496                 // available, so match the value of 'access'
    3497                 $this->params['show_tab'] = $this->params['access'];
    3498             }
    3499         }
    3500 
    3501         // Parse multiple access conditions into an array
    3502         $access_conditions = $this->params['show_tab'];
    3503         if ( ! is_array( $access_conditions ) ) {
    3504             $access_conditions = explode( ',', $access_conditions );
    3505         }
    3506 
    3507         // If the current user meets at least one condition, the
    3508         // get access
    3509         foreach ( $access_conditions as $access_condition ) {
    3510             if ( $this->user_meets_access_condition( $access_condition ) ) {
    3511                 $this->user_can_see_nav_item = true;
    3512                 break;
    3513             }
    3514         }
    3515     }
    3516 
    3517     /**
    3518      * Check whether the current user meets an access condition.
    3519      *
    3520      * @param string $access_condition 'anyone', 'loggedin', 'member',
    3521      *        'mod', 'admin' or 'noone'.
    3522      * @return bool
    3523      */
    3524     protected function user_meets_access_condition( $access_condition ) {
    3525         $group = groups_get_group( array(
    3526             'group_id' => $this->group_id,
    3527         ) );
    3528 
    3529         switch ( $access_condition ) {
    3530             case 'admin' :
    3531                 $meets_condition = groups_is_user_admin( bp_loggedin_user_id(), $this->group_id );
    3532                 break;
    3533 
    3534             case 'mod' :
    3535                 $meets_condition = groups_is_user_mod( bp_loggedin_user_id(), $this->group_id );
    3536                 break;
    3537 
    3538             case 'member' :
    3539                 $meets_condition = groups_is_user_member( bp_loggedin_user_id(), $this->group_id );
    3540                 break;
    3541 
    3542             case 'loggedin' :
    3543                 $meets_condition = is_user_logged_in();
    3544                 break;
    3545 
    3546             case 'noone' :
    3547                 $meets_condition = false;
    3548                 break;
    3549 
    3550             case 'anyone' :
    3551             default :
    3552                 $meets_condition = true;
    3553                 break;
    3554         }
    3555 
    3556         return $meets_condition;
    3557     }
    3558 
    3559     /** Display ***********************************************************/
    3560 
    3561     /**
    3562      * Hook this extension's group tab into BuddyPress, if necessary.
    3563      *
    3564      * @since BuddyPress (1.8.0)
    3565      */
    3566     protected function setup_display_hooks() {
    3567 
    3568         // Bail if not a group
    3569         if ( ! bp_is_group() ) {
    3570             return;
    3571         }
    3572 
    3573         // Backward compatibility only
    3574         if ( ( 'public' !== $this->visibility ) && ! buddypress()->groups->current_group->user_has_access ) {
    3575             return;
    3576         }
    3577 
    3578         $user_can_see_nav_item = $this->user_can_see_nav_item();
    3579 
    3580         if ( $user_can_see_nav_item ) {
    3581             $group_permalink = bp_get_group_permalink( groups_get_current_group() );
    3582 
    3583             bp_core_new_subnav_item( array(
    3584                 'name'            => ! $this->nav_item_name ? $this->name : $this->nav_item_name,
    3585                 'slug'            => $this->slug,
    3586                 'parent_slug'     => bp_get_current_group_slug(),
    3587                 'parent_url'      => $group_permalink,
    3588                 'position'        => $this->nav_item_position,
    3589                 'item_css_id'     => 'nav-' . $this->slug,
    3590                 'screen_function' => array( &$this, '_display_hook' ),
    3591                 'user_has_access' => $user_can_see_nav_item,
    3592                 'no_access_url'   => $group_permalink,
    3593             ) );
    3594 
    3595             // When we are viewing the extension display page, set the title and options title
    3596             if ( bp_is_current_action( $this->slug ) ) {
    3597                 add_filter( 'bp_group_user_has_access',   array( $this, 'group_access_protection' ), 10, 2 );
    3598                 add_action( 'bp_template_content_header', create_function( '', 'echo "' . esc_attr( $this->name ) . '";' ) );
    3599                 add_action( 'bp_template_title',          create_function( '', 'echo "' . esc_attr( $this->name ) . '";' ) );
    3600             }
    3601         }
    3602 
    3603         // Hook the group home widget
    3604         if ( ! bp_current_action() && bp_is_current_action( 'home' ) ) {
    3605             add_action( $this->display_hook, array( &$this, 'widget_display' ) );
    3606         }
    3607     }
    3608 
    3609     /**
    3610      * Hook the main display method, and loads the template file
    3611      */
    3612     public function _display_hook() {
    3613         add_action( 'bp_template_content', array( &$this, 'call_display' ) );
    3614         bp_core_load_template( apply_filters( 'bp_core_template_plugin', $this->template_file ) );
    3615     }
    3616 
    3617     /**
    3618      * Call the display() method.
    3619      *
    3620      * We use this wrapper so that we can pass the group_id to the
    3621      * display() callback.
    3622      *
    3623      * @since BuddyPress (2.1.1)
    3624      */
    3625     public function call_display() {
    3626         $this->display( $this->group_id );
    3627     }
    3628 
    3629     /**
    3630      * Determine whether the current user should see this nav tab.
    3631      *
    3632      * Note that this controls only the display of the navigation item.
    3633      * Access to the tab is controlled by the user_can_visit() check.
    3634      *
    3635      * @since BuddyPress (2.1.0)
    3636      *
    3637      * @return bool
    3638      */
    3639     public function user_can_see_nav_item( $user_can_see_nav_item = false ) {
    3640         if ( 'noone' !== $this->params['show_tab'] && current_user_can( 'bp_moderate' ) ) {
    3641             return true;
    3642         }
    3643 
    3644         return $this->user_can_see_nav_item;
    3645     }
    3646 
    3647     /**
    3648      * Determine whether the current user has access to visit this tab.
    3649      *
    3650      * @since BuddyPress (2.1.0)
    3651      *
    3652      * @return bool
    3653      */
    3654     public function user_can_visit( $user_can_visit = false ) {
    3655         if ( 'noone' !== $this->params['access'] && current_user_can( 'bp_moderate' ) ) {
    3656             return true;
    3657         }
    3658 
    3659         return $this->user_can_visit;
    3660     }
    3661 
    3662     /**
    3663      * Filter the access check in bp_groups_group_access_protection() for this extension.
    3664      *
    3665      * Note that $no_access_args is passed by reference, as there are some
    3666      * circumstances where the bp_core_no_access() arguments need to be
    3667      * modified before the redirect takes place.
    3668      *
    3669      * @since BuddyPress (2.1.0)
    3670      *
    3671      * @param bool $user_can_visit
    3672      * @param array $no_access_args
    3673      * @return bool
    3674      */
    3675     public function group_access_protection( $user_can_visit, &$no_access_args ) {
    3676         $user_can_visit = $this->user_can_visit();
    3677 
    3678         if ( ! $user_can_visit && is_user_logged_in() ) {
    3679             $current_group = groups_get_group( array(
    3680                 'group_id' => $this->group_id,
    3681             ) );
    3682 
    3683             $no_access_args['message'] = __( 'You do not have access to this content.', 'buddypress' );
    3684             $no_access_args['root'] = bp_get_group_permalink( $current_group ) . 'home/';
    3685             $no_access_args['redirect'] = false;
    3686         }
    3687 
    3688         return $user_can_visit;
    3689     }
    3690 
    3691 
    3692     /** Create ************************************************************/
    3693 
    3694     /**
    3695      * Hook this extension's Create step into BuddyPress, if necessary.
    3696      *
    3697      * @since BuddyPress (1.8.0)
    3698      */
    3699     protected function setup_create_hooks() {
    3700         if ( ! $this->is_screen_enabled( 'create' ) ) {
    3701             return;
    3702         }
    3703 
    3704         $screen = $this->screens['create'];
    3705 
    3706         // Insert the group creation step for the new group extension
    3707         buddypress()->groups->group_creation_steps[ $screen['slug'] ] = array(
    3708             'name'     => $screen['name'],
    3709             'slug'     => $screen['slug'],
    3710             'position' => $screen['position'],
    3711         );
    3712 
    3713         // The maybe_ methods check to see whether the create_*
    3714         // callbacks should be invoked (ie, are we on the
    3715         // correct group creation step). Hooked in separate
    3716         // methods because current creation step info not yet
    3717         // available at this point
    3718         add_action( 'groups_custom_create_steps', array( $this, 'maybe_create_screen' ) );
    3719         add_action( 'groups_create_group_step_save_' . $screen['slug'], array( $this, 'maybe_create_screen_save' ) );
    3720     }
    3721 
    3722     /**
    3723      * Call the create_screen() method, if we're on the right page.
    3724      *
    3725      * @since BuddyPress (1.8.0)
    3726      */
    3727     public function maybe_create_screen() {
    3728         if ( ! bp_is_group_creation_step( $this->screens['create']['slug'] ) ) {
    3729             return;
    3730         }
    3731 
    3732         call_user_func( $this->screens['create']['screen_callback'], $this->group_id );
    3733         $this->nonce_field( 'create' );
    3734 
    3735         // The create screen requires an additional nonce field
    3736         // due to a quirk in the way the templates are built
    3737         wp_nonce_field( 'groups_create_save_' . bp_get_groups_current_create_step(), '_wpnonce', false );
    3738     }
    3739 
    3740     /**
    3741      * Call the create_screen_save() method, if we're on the right page.
    3742      *
    3743      * @since BuddyPress (1.8.0)
    3744      */
    3745     public function maybe_create_screen_save() {
    3746         if ( ! bp_is_group_creation_step( $this->screens['create']['slug'] ) ) {
    3747             return;
    3748         }
    3749 
    3750         $this->check_nonce( 'create' );
    3751         call_user_func( $this->screens['create']['screen_save_callback'], $this->group_id );
    3752     }
    3753 
    3754     /** Edit **************************************************************/
    3755 
    3756     /**
    3757      * Hook this extension's Edit panel into BuddyPress, if necessary.
    3758      *
    3759      * @since BuddyPress (1.8.0)
    3760      */
    3761     protected function setup_edit_hooks() {
    3762         // Bail if not in a group
    3763         if ( ! bp_is_group() ) {
    3764             return;
    3765         }
    3766 
    3767         // Bail if not an edit screen
    3768         if ( ! $this->is_screen_enabled( 'edit' ) || ! bp_is_item_admin() ) {
    3769             return;
    3770         }
    3771 
    3772         $screen = $this->screens['edit'];
    3773 
    3774         $position = isset( $screen['position'] ) ? (int) $screen['position'] : 10;
    3775         $position += 40;
    3776 
    3777         $current_group = groups_get_current_group();
    3778         $admin_link = trailingslashit( bp_get_group_permalink( $current_group ) . 'admin' );
    3779 
    3780         $subnav_args = array(
    3781             'name'            => $screen['name'],
    3782             'slug'            => $screen['slug'],
    3783             'parent_slug'     => $current_group->slug . '_manage',
    3784             'parent_url'      => trailingslashit( bp_get_group_permalink( $current_group ) . 'admin' ),
    3785             'user_has_access' => bp_is_item_admin(),
    3786             'position'        => $position,
    3787             'screen_function' => 'groups_screen_group_admin',
    3788         );
    3789 
    3790         // Should we add a menu to the Group's WP Admin Bar
    3791         if ( ! empty( $screen['show_in_admin_bar'] ) ) {
    3792             $subnav_args['show_in_admin_bar'] = true;
    3793         }
    3794 
    3795         // Add the tab to the manage navigation
    3796         bp_core_new_subnav_item( $subnav_args );
    3797 
    3798         // Catch the edit screen and forward it to the plugin template
    3799         if ( bp_is_groups_component() && bp_is_current_action( 'admin' ) && bp_is_action_variable( $screen['slug'], 0 ) ) {
    3800             $this->call_edit_screen_save( $this->group_id );
    3801 
    3802             add_action( 'groups_custom_edit_steps', array( &$this, 'call_edit_screen' ) );
    3803 
    3804             // Determine the proper template and save for later
    3805             // loading
    3806             if ( '' !== bp_locate_template( array( 'groups/single/home.php' ), false ) ) {
    3807                 $this->edit_screen_template = '/groups/single/home';
    3808             } else {
    3809                 add_action( 'bp_template_content_header', create_function( '', 'echo "<ul class=\"content-header-nav\">"; bp_group_admin_tabs(); echo "</ul>";' ) );
    3810                 add_action( 'bp_template_content', array( &$this, 'call_edit_screen' ) );
    3811                 $this->edit_screen_template = '/groups/single/plugins';
    3812             }
    3813 
    3814             // We load the template at bp_screens, to give all
    3815             // extensions a chance to load
    3816             add_action( 'bp_screens', array( $this, 'call_edit_screen_template_loader' ) );
    3817         }
    3818     }
    3819 
    3820     /**
    3821      * Call the edit_screen() method.
    3822      *
    3823      * Previous versions of BP_Group_Extension required plugins to provide
    3824      * their own Submit button and nonce fields when building markup. In
    3825      * BP 1.8, this requirement was lifted - BP_Group_Extension now handles
    3826      * all required submit buttons and nonces.
    3827      *
    3828      * We put the edit screen markup into an output buffer before echoing.
    3829      * This is so that we can check for the presence of a hardcoded submit
    3830      * button, as would be present in legacy plugins; if one is found, we
    3831      * do not auto-add our own button.
    3832      *
    3833      * @since BuddyPress (1.8.0)
    3834      */
    3835     public function call_edit_screen() {
    3836         ob_start();
    3837         call_user_func( $this->screens['edit']['screen_callback'], $this->group_id );
    3838         $screen = ob_get_contents();
    3839         ob_end_clean();
    3840 
    3841         echo $this->maybe_add_submit_button( $screen );
    3842 
    3843         $this->nonce_field( 'edit' );
    3844     }
    3845 
    3846     /**
    3847      * Check the nonce, and call the edit_screen_save() method.
    3848      *
    3849      * @since BuddyPress (1.8.0)
    3850      */
    3851     public function call_edit_screen_save() {
    3852         if ( empty( $_POST ) ) {
    3853             return;
    3854         }
    3855 
    3856         // When DOING_AJAX, the POST global will be populated, but we
    3857         // should assume it's a save
    3858         if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
    3859             return;
    3860         }
    3861 
    3862         $this->check_nonce( 'edit' );
    3863 
    3864         // Detect whether the screen_save_callback is performing a
    3865         // redirect, so that we don't do one of our own
    3866         add_filter( 'wp_redirect', array( $this, 'detect_post_save_redirect' ) );
    3867 
    3868         // Call the extension's save routine
    3869         call_user_func( $this->screens['edit']['screen_save_callback'], $this->group_id );
    3870 
    3871         // Clean up detection filters
    3872         remove_filter( 'wp_redirect', array( $this, 'detect_post_save_redirect' ) );
    3873 
    3874         // Perform a redirect only if one has not already taken place
    3875         if ( empty( $this->post_save_redirect ) ) {
    3876             $redirect_to = apply_filters( 'bp_group_extension_edit_screen_save_redirect', bp_get_requested_url( ) );
    3877 
    3878             bp_core_redirect( $redirect_to );
    3879             die();
    3880         }
    3881     }
    3882 
    3883     /**
    3884      * Load the template that houses the Edit screen.
    3885      *
    3886      * Separated out into a callback so that it can run after all other
    3887      * Group Extensions have had a chance to register their navigation, to
    3888      * avoid missing tabs.
    3889      *
    3890      * Hooked to 'bp_screens'.
    3891      *
    3892      * @since BuddyPress (1.8.0)
    3893      * @access public So that do_action() has access. Do not call directly.
    3894      *
    3895      * @see BP_Group_Extension::setup_edit_hooks()
    3896      */
    3897     public function call_edit_screen_template_loader() {
    3898         bp_core_load_template( $this->edit_screen_template );
    3899     }
    3900 
    3901     /**
    3902      * Add a submit button to the edit form, if it needs one.
    3903      *
    3904      * There's an inconsistency in the way that the group Edit and Create
    3905      * screens are rendered: the Create screen has a submit button built
    3906      * in, but the Edit screen does not. This function allows plugin
    3907      * authors to write markup that does not contain the submit button for
    3908      * use on both the Create and Edit screens - BP will provide the button
    3909      * if one is not found.
    3910      *
    3911      * @since BuddyPress (1.8.0)
    3912      *
    3913      * @param string $screen The screen markup, captured in the output
    3914      *        buffer.
    3915      * @param string $screen The same markup, with a submit button added.
    3916      */
    3917     protected function maybe_add_submit_button( $screen = '' ) {
    3918         if ( $this->has_submit_button( $screen ) ) {
    3919             return $screen;
    3920         }
    3921 
    3922         return $screen . sprintf(
    3923             '<div id="%s"><input type="submit" name="save" value="%s" id="%s"></div>',
    3924             'bp-group-edit-' . $this->slug . '-submit-wrapper',
    3925             $this->screens['edit']['submit_text'],
    3926             'bp-group-edit-' . $this->slug . '-submit'
    3927         );
    3928     }
    3929 
    3930     /**
    3931      * Does the given markup have a submit button?
    3932      *
    3933      * @since BuddyPress (1.8.0)
    3934      *
    3935      * @param string $screen The markup to check.
    3936      * @return bool True if a Submit button is found, otherwise false.
    3937      */
    3938     public static function has_submit_button( $screen = '' ) {
    3939         $pattern = "/<input[^>]+type=[\'\"]submit[\'\"]/";
    3940         preg_match( $pattern, $screen, $matches );
    3941         return ! empty( $matches[0] );
    3942     }
    3943 
    3944     /**
    3945      * Detect redirects hardcoded into edit_screen_save() callbacks.
    3946      *
    3947      * @since BuddyPress (2.1.0)
    3948      *
    3949      * @param string $location
    3950      * @return string
    3951      */
    3952     public function detect_post_save_redirect( $redirect = '' ) {
    3953         if ( ! empty( $redirect ) ) {
    3954             $this->post_save_redirect = $redirect;
    3955         }
    3956 
    3957         return $redirect;
    3958     }
    3959 
    3960     /** Admin *************************************************************/
    3961 
    3962     /**
    3963      * Hook this extension's Admin metabox into BuddyPress, if necessary.
    3964      *
    3965      * @since BuddyPress (1.8.0)
    3966      */
    3967     protected function setup_admin_hooks() {
    3968         if ( ! $this->is_screen_enabled( 'admin' ) || ! is_admin() ) {
    3969             return;
    3970         }
    3971 
    3972         // Hook the admin screen markup function to the content hook
    3973         add_action( 'bp_groups_admin_meta_box_content_' . $this->slug, array( $this, 'call_admin_screen' ) );
    3974 
    3975         // Initialize the metabox
    3976         add_action( 'bp_groups_admin_meta_boxes', array( $this, '_meta_box_display_callback' ) );
    3977 
    3978         // Catch the metabox save
    3979         add_action( 'bp_group_admin_edit_after', array( $this, 'call_admin_screen_save' ), 10 );
    3980     }
    3981 
    3982     /**
    3983      * Call the admin_screen() method, and add a nonce field.
    3984      *
    3985      * @since BuddyPress (1.8.0)
    3986      */
    3987     public function call_admin_screen() {
    3988         call_user_func( $this->screens['admin']['screen_callback'], $this->group_id );
    3989         $this->nonce_field( 'admin' );
    3990     }
    3991 
    3992     /**
    3993      * Check the nonce, and call the admin_screen_save() method
    3994      *
    3995      * @since BuddyPress (1.8.0)
    3996      */
    3997     public function call_admin_screen_save() {
    3998         $this->check_nonce( 'admin' );
    3999         call_user_func( $this->screens['admin']['screen_save_callback'], $this->group_id );
    4000     }
    4001 
    4002     /**
    4003      * Create the Dashboard meta box for this extension.
    4004      *
    4005      * @since BuddyPress (1.7.0)
    4006      */
    4007     public function _meta_box_display_callback() {
    4008         $group_id = isset( $_GET['gid'] ) ? (int) $_GET['gid'] : 0;
    4009         $screen   = $this->screens['admin'];
    4010 
    4011         add_meta_box(
    4012             $screen['slug'],
    4013             $screen['name'],
    4014             create_function( '', 'do_action( "bp_groups_admin_meta_box_content_' . $this->slug . '", ' . $group_id . ' );' ),
    4015             get_current_screen()->id,
    4016             $screen['metabox_context'],
    4017             $screen['metabox_priority']
    4018         );
    4019     }
    4020 
    4021 
    4022     /** Utilities *********************************************************/
    4023 
    4024     /**
    4025      * Generate the nonce fields for a settings form.
    4026      *
    4027      * The nonce field name (the second param passed to wp_nonce_field)
    4028      * contains this extension's slug and is thus unique to this extension.
    4029      * This is necessary because in some cases (namely, the Dashboard),
    4030      * more than one extension may generate nonces on the same page, and we
    4031      * must avoid name clashes.
    4032      *
    4033      * @since BuddyPress (1.8.0)
    4034      *
    4035      * @param string $context Screen context. 'create', 'edit', or 'admin'.
    4036      */
    4037     public function nonce_field( $context = '' ) {
    4038         wp_nonce_field( 'bp_group_extension_' . $this->slug . '_' . $context, '_bp_group_' . $context . '_nonce_' . $this->slug );
    4039     }
    4040 
    4041     /**
    4042      * Check the nonce on a submitted settings form.
    4043      *
    4044      * @since BuddyPress (1.8.0)
    4045      *
    4046      * @param string $context Screen context. 'create', 'edit', or 'admin'.
    4047      */
    4048     public function check_nonce( $context = '' ) {
    4049         check_admin_referer( 'bp_group_extension_' . $this->slug . '_' . $context, '_bp_group_' . $context . '_nonce_' . $this->slug );
    4050     }
    4051 
    4052     /**
    4053      * Is the specified screen enabled?
    4054      *
    4055      * To be enabled, a screen must both have the 'enabled' key set to true
    4056      * (legacy: $this->enable_create_step, etc), and its screen_callback
    4057      * must also exist and be callable.
    4058      *
    4059      * @since BuddyPress (1.8.0)
    4060      *
    4061      * @param string $context Screen context. 'create', 'edit', or 'admin'.
    4062      * @return bool True if the screen is enabled, otherwise false.
    4063      */
    4064     public function is_screen_enabled( $context = '' ) {
    4065         $enabled = false;
    4066 
    4067         if ( isset( $this->screens[ $context ] ) ) {
    4068             $enabled = $this->screens[ $context ]['enabled'] && is_callable( $this->screens[ $context ]['screen_callback'] );
    4069         }
    4070 
    4071         return (bool) $enabled;
    4072     }
    4073 
    4074     /**
    4075      * Get the appropriate screen callback for the specified context/type.
    4076      *
    4077      * BP Group Extensions have three special "screen contexts": create,
    4078      * admin, and edit. Each of these contexts has a corresponding
    4079      * _screen() and _screen_save() method, which allow group extension
    4080      * plugins to define different markup and logic for each context.
    4081      *
    4082      * BP also supports fallback settings_screen() and
    4083      * settings_screen_save() methods, which can be used to define markup
    4084      * and logic that is shared between context. For each context, you may
    4085      * either provide context-specific methods, or you can let BP fall back
    4086      * on the shared settings_* callbacks.
    4087      *
    4088      * For example, consider a BP_Group_Extension implementation that looks
    4089      * like this:
    4090      *
    4091      *   // ...
    4092      *   function create_screen( $group_id ) { ... }
    4093      *   function create_screen_save( $group_id ) { ... }
    4094      *   function settings_screen( $group_id ) { ... }
    4095      *   function settings_screen_save( $group_id ) { ... }
    4096      *   // ...
    4097      *
    4098      * BP_Group_Extension will use your create_* methods for the Create
    4099      * steps, and will use your generic settings_* methods for the Edit
    4100      * and Admin contexts. This schema allows plugin authors maximum
    4101      * flexibility without having to repeat themselves.
    4102      *
    4103      * The get_screen_callback() method uses a ReflectionClass object to
    4104      * determine whether your extension has provided a given callback.
    4105      *
    4106      * @since BuddyPress (1.8.0)
    4107      *
    4108      * @param string $context Screen context. 'create', 'edit', or 'admin'.
    4109      * @param string $type Screen type. 'screen' or 'screen_save'. Default:
    4110      *        'screen'.
    4111      * @return callable A callable function handle.
    4112      */
    4113     public function get_screen_callback( $context = '', $type = 'screen' ) {
    4114         $callback = '';
    4115 
    4116         // Try the context-specific callback first
    4117         $method  = $context . '_' . $type;
    4118         $rmethod = $this->class_reflection->getMethod( $method );
    4119         if ( isset( $rmethod->class ) && $this->class_name === $rmethod->class ) {
    4120             $callback = array( $this, $method );
    4121         }
    4122 
    4123         if ( empty( $callback ) ) {
    4124             $fallback_method  = 'settings_' . $type;
    4125             $rfallback_method = $this->class_reflection->getMethod( $fallback_method );
    4126             if ( isset( $rfallback_method->class ) && $this->class_name === $rfallback_method->class ) {
    4127                 $callback = array( $this, $fallback_method );
    4128             }
    4129         }
    4130 
    4131         return $callback;
    4132     }
    4133 
    4134     /**
    4135      * Recursive argument parsing.
    4136      *
    4137      * This acts like a multi-dimensional version of wp_parse_args() (minus
    4138      * the querystring parsing - you must pass arrays).
    4139      *
    4140      * Values from $a override those from $b; keys in $b that don't exist
    4141      * in $a are passed through.
    4142      *
    4143      * This is different from array_merge_recursive(), both because of the
    4144      * order of preference ($a overrides $b) and because of the fact that
    4145      * array_merge_recursive() combines arrays deep in the tree, rather
    4146      * than overwriting the b array with the a array.
    4147      *
    4148      * The implementation of this function is specific to the needs of
    4149      * BP_Group_Extension, where we know that arrays will always be
    4150      * associative, and that an argument under a given key in one array
    4151      * will be matched by a value of identical depth in the other one. The
    4152      * function is NOT designed for general use, and will probably result
    4153      * in unexpected results when used with data in the wild. See, eg,
    4154      * http://core.trac.wordpress.org/ticket/19888
    4155      *
    4156      * @since BuddyPress (1.8.0)
    4157      *
    4158      * @param array $a First set of arguments.
    4159      * @param array $b Second set of arguments.
    4160      * @return array Parsed arguments.
    4161      */
    4162     public static function parse_args_r( &$a, $b ) {
    4163         $a = (array) $a;
    4164         $b = (array) $b;
    4165         $r = $b;
    4166 
    4167         foreach ( $a as $k => &$v ) {
    4168             if ( is_array( $v ) && isset( $r[ $k ] ) ) {
    4169                 $r[ $k ] = self::parse_args_r( $v, $r[ $k ] );
    4170             } else {
    4171                 $r[ $k ] = $v;
    4172             }
    4173         }
    4174 
    4175         return $r;
    4176     }
    4177 
    4178     /** Legacy Support ********************************************************/
    4179 
    4180     /*
    4181      * In BuddyPress 1.8, the recommended technique for configuring
    4182      * extensions changed from directly setting various object properties
    4183      * in the class constructor, to passing a configuration array to
    4184      * parent::init(). The following methods ensure that extensions created
    4185      * in the old way continue to work, by converting legacy configuration
    4186      * data to the new format.
    4187      */
    4188 
    4189     /**
    4190      * Provide access to otherwise unavailable object properties.
    4191      *
    4192      * This magic method is here for backward compatibility with plugins
    4193      * that refer to config properties that have moved to a different
    4194      * location (such as enable_create_step, which is now at
    4195      * $this->screens['create']['enabled']
    4196      *
    4197      * The legacy_properties array is set up in
    4198      * self::setup_legacy_properties().
    4199      *
    4200      * @since BuddyPress (1.8.0)
    4201      *
    4202      * @param string $key Property name.
    4203      * @return mixed The value if found, otherwise null.
    4204      */
    4205     public function __get( $key ) {
    4206         if ( isset( $this->legacy_properties[ $key ] ) ) {
    4207             return $this->legacy_properties[ $key ];
    4208         } elseif ( isset( $this->data[ $key ] ) ) {
    4209             return $this->data[ $key ];
    4210         } else {
    4211             return null;
    4212         }
    4213     }
    4214 
    4215     /**
    4216      * Provide a fallback for isset( $this->foo ) when foo is unavailable.
    4217      *
    4218      * This magic method is here for backward compatibility with plugins
    4219      * that have set their class config options directly in the class
    4220      * constructor. The parse_legacy_properties() method of the current
    4221      * class needs to check whether any legacy keys have been put into the
    4222      * $this->data array.
    4223      *
    4224      * @since BuddyPress (1.8.0)
    4225      *
    4226      * @param string $key Property name.
    4227      * @return bool True if the value is set, otherwise false.
    4228      */
    4229     public function __isset( $key ) {
    4230         if ( isset( $this->legacy_properties[ $key ] ) ) {
    4231             return true;
    4232         } elseif ( isset( $this->data[ $key ] ) ) {
    4233             return true;
    4234         } else {
    4235             return false;
    4236         }
    4237     }
    4238 
    4239     /**
    4240      * Allow plugins to set otherwise unavailable object properties.
    4241      *
    4242      * This magic method is here for backward compatibility with plugins
    4243      * that may attempt to modify the group extension by manually assigning
    4244      * a value to an object property that no longer exists, such as
    4245      * $this->enable_create_step.
    4246      *
    4247      * @since BuddyPress (1.8.0)
    4248      *
    4249      * @param string $key Property name.
    4250      * @param mixed $value Property value.
    4251      */
    4252     public function __set( $key, $value ) {
    4253 
    4254         if ( empty( $this->initialized ) ) {
    4255             $this->data[ $key ] = $value;
    4256         }
    4257 
    4258         switch ( $key ) {
    4259             case 'enable_create_step' :
    4260                 $this->screens['create']['enabled'] = $value;
    4261                 break;
    4262 
    4263             case 'enable_edit_item' :
    4264                 $this->screens['edit']['enabled'] = $value;
    4265                 break;
    4266 
    4267             case 'enable_admin_item' :
    4268                 $this->screens['admin']['enabled'] = $value;
    4269                 break;
    4270 
    4271             case 'create_step_position' :
    4272                 $this->screens['create']['position'] = $value;
    4273                 break;
    4274 
    4275             // Note: 'admin' becomes 'edit' to distinguish from Dashboard 'admin'
    4276             case 'admin_name' :
    4277                 $this->screens['edit']['name'] = $value;
    4278                 break;
    4279 
    4280             case 'admin_slug' :
    4281                 $this->screens['edit']['slug'] = $value;
    4282                 break;
    4283 
    4284             case 'create_name' :
    4285                 $this->screens['create']['name'] = $value;
    4286                 break;
    4287 
    4288             case 'create_slug' :
    4289                 $this->screens['create']['slug'] = $value;
    4290                 break;
    4291 
    4292             case 'admin_metabox_context' :
    4293                 $this->screens['admin']['metabox_context'] = $value;
    4294                 break;
    4295 
    4296             case 'admin_metabox_priority' :
    4297                 $this->screens['admin']['metabox_priority'] = $value;
    4298                 break;
    4299 
    4300             default :
    4301                 $this->data[ $key ] = $value;
    4302                 break;
    4303         }
    4304     }
    4305 
    4306     /**
    4307      * Return a list of legacy properties.
    4308      *
    4309      * The legacy implementation of BP_Group_Extension used all of these
    4310      * object properties for configuration. Some have been moved.
    4311      *
    4312      * @since BuddyPress (1.8.0)
    4313      *
    4314      * @return array List of legacy property keys.
    4315      */
    4316     protected function get_legacy_property_list() {
    4317         return array(
    4318             'name',
    4319             'slug',
    4320             'admin_name',
    4321             'admin_slug',
    4322             'create_name',
    4323             'create_slug',
    4324             'visibility',
    4325             'create_step_position',
    4326             'nav_item_position',
    4327             'admin_metabox_context',
    4328             'admin_metabox_priority',
    4329             'enable_create_step',
    4330             'enable_nav_item',
    4331             'enable_edit_item',
    4332             'enable_admin_item',
    4333             'nav_item_name',
    4334             'display_hook',
    4335             'template_file',
    4336         );
    4337     }
    4338 
    4339     /**
    4340      * Parse legacy properties.
    4341      *
    4342      * The old standard for BP_Group_Extension was for plugins to register
    4343      * their settings as properties in their constructor. The new method is
    4344      * to pass a config array to the init() method. In order to support
    4345      * legacy plugins, we slurp up legacy properties, and later on we'll
    4346      * parse them into the new init() array.
    4347      *
    4348      * @since BuddyPress (1.8.0)
    4349      */
    4350     protected function parse_legacy_properties() {
    4351 
    4352         // Only run this one time
    4353         if ( ! empty( $this->legacy_properties_converted ) ) {
    4354             return;
    4355         }
    4356 
    4357         $properties = $this->get_legacy_property_list();
    4358 
    4359         // By-reference variable for convenience
    4360         $lpc =& $this->legacy_properties_converted;
    4361 
    4362         foreach ( $properties as $property ) {
    4363 
    4364             // No legacy config exists for this key
    4365             if ( ! isset( $this->{$property} ) ) {
    4366                 continue;
    4367             }
    4368 
    4369             // Grab the value and record it as appropriate
    4370             $value = $this->{$property};
    4371 
    4372             switch ( $property ) {
    4373                 case 'enable_create_step' :
    4374                     $lpc['screens']['create']['enabled'] = (bool) $value;
    4375                     break;
    4376 
    4377                 case 'enable_edit_item' :
    4378                     $lpc['screens']['edit']['enabled'] = (bool) $value;
    4379                     break;
    4380 
    4381                 case 'enable_admin_item' :
    4382                     $lpc['screens']['admin']['enabled'] = (bool) $value;
    4383                     break;
    4384 
    4385                 case 'create_step_position' :
    4386                     $lpc['screens']['create']['position'] = $value;
    4387                     break;
    4388 
    4389                 // Note: 'admin' becomes 'edit' to distinguish from Dashboard 'admin'
    4390                 case 'admin_name' :
    4391                     $lpc['screens']['edit']['name'] = $value;
    4392                     break;
    4393 
    4394                 case 'admin_slug' :
    4395                     $lpc['screens']['edit']['slug'] = $value;
    4396                     break;
    4397 
    4398                 case 'create_name' :
    4399                     $lpc['screens']['create']['name'] = $value;
    4400                     break;
    4401 
    4402                 case 'create_slug' :
    4403                     $lpc['screens']['create']['slug'] = $value;
    4404                     break;
    4405 
    4406                 case 'admin_metabox_context' :
    4407                     $lpc['screens']['admin']['metabox_context'] = $value;
    4408                     break;
    4409 
    4410                 case 'admin_metabox_priority' :
    4411                     $lpc['screens']['admin']['metabox_priority'] = $value;
    4412                     break;
    4413 
    4414                 default :
    4415                     $lpc[ $property ] = $value;
    4416                     break;
    4417             }
    4418         }
    4419     }
    4420 
    4421     /**
    4422      * Set up legacy properties.
    4423      *
    4424      * This method is responsible for ensuring that all legacy config
    4425      * properties are stored in an array $this->legacy_properties, so that
    4426      * they remain available to plugins that reference the variables at
    4427      * their old locations.
    4428      *
    4429      * @since BuddyPress (1.8.0)
    4430      *
    4431      * @see BP_Group_Extension::__get()
    4432      */
    4433     protected function setup_legacy_properties() {
    4434 
    4435         // Only run this one time
    4436         if ( ! empty( $this->legacy_properties ) ) {
    4437             return;
    4438         }
    4439 
    4440         $properties = $this->get_legacy_property_list();
    4441         $params     = $this->params;
    4442         $lp         =& $this->legacy_properties;
    4443 
    4444         foreach ( $properties as $property ) {
    4445             switch ( $property ) {
    4446                 case 'enable_create_step' :
    4447                     $lp['enable_create_step'] = $params['screens']['create']['enabled'];
    4448                     break;
    4449 
    4450                 case 'enable_edit_item' :
    4451                     $lp['enable_edit_item'] = $params['screens']['edit']['enabled'];
    4452                     break;
    4453 
    4454                 case 'enable_admin_item' :
    4455                     $lp['enable_admin_item'] = $params['screens']['admin']['enabled'];
    4456                     break;
    4457 
    4458                 case 'create_step_position' :
    4459                     $lp['create_step_position'] = $params['screens']['create']['position'];
    4460                     break;
    4461 
    4462                 // Note: 'admin' becomes 'edit' to distinguish from Dashboard 'admin'
    4463                 case 'admin_name' :
    4464                     $lp['admin_name'] = $params['screens']['edit']['name'];
    4465                     break;
    4466 
    4467                 case 'admin_slug' :
    4468                     $lp['admin_slug'] = $params['screens']['edit']['slug'];
    4469                     break;
    4470 
    4471                 case 'create_name' :
    4472                     $lp['create_name'] = $params['screens']['create']['name'];
    4473                     break;
    4474 
    4475                 case 'create_slug' :
    4476                     $lp['create_slug'] = $params['screens']['create']['slug'];
    4477                     break;
    4478 
    4479                 case 'admin_metabox_context' :
    4480                     $lp['admin_metabox_context'] = $params['screens']['admin']['metabox_context'];
    4481                     break;
    4482 
    4483                 case 'admin_metabox_priority' :
    4484                     $lp['admin_metabox_priority'] = $params['screens']['admin']['metabox_priority'];
    4485                     break;
    4486 
    4487                 default :
    4488                     // All other items get moved over
    4489                     $lp[ $property ] = $params[ $property ];
    4490 
    4491                     // Also reapply to the object, for backpat
    4492                     $this->{$property} = $params[ $property ];
    4493 
    4494                     break;
    4495             }
    4496         }
    4497     }
    4498 }
    4499 
    4500 /**
    4501  * Register a new Group Extension.
    4502  *
    4503  * @param string Name of the Extension class.
    4504  * @return bool|null Returns false on failure, otherwise null.
    4505  */
    4506 function bp_register_group_extension( $group_extension_class = '' ) {
    4507 
    4508     if ( ! class_exists( $group_extension_class ) ) {
    4509         return false;
    4510     }
    4511 
    4512     // Register the group extension on the bp_init action so we have access
    4513     // to all plugins.
    4514     add_action( 'bp_init', create_function( '', '
    4515         $extension = new ' . $group_extension_class . ';
    4516         add_action( "bp_actions", array( &$extension, "_register" ), 8 );
    4517         add_action( "admin_init", array( &$extension, "_register" ) );
    4518     ' ), 11 );
    4519 }
    4520 
    4521 /**
    4522  * Adds support for user at-mentions (for users in a specific Group) to the Suggestions API.
    4523  *
    4524  * @since BuddyPress (2.1.0)
    4525  */
    4526 class BP_Groups_Member_Suggestions extends BP_Members_Suggestions {
    4527 
    4528     /**
    4529      * Default arguments for this suggestions service.
    4530      *
    4531      * @since BuddyPress (2.1.0)
    4532      * @access protected
    4533      * @var array $args {
    4534      *     @type int $group_id Positive integers will restrict the search to members in that group.
    4535      *           Negative integers will restrict the search to members in every other group.
    4536      *     @type int $limit Maximum number of results to display. Default: 16.
    4537      *     @type bool $only_friends If true, only match the current user's friends. Default: false.
    4538      *     @type string $term The suggestion service will try to find results that contain this string.
    4539      *           Mandatory.
    4540      * }
    4541      */
    4542     protected $default_args = array(
    4543         'group_id'     => 0,
    4544         'limit'        => 16,
    4545         'only_friends' => false,
    4546         'term'         => '',
    4547         'type'         => '',
    4548     );
    4549 
    4550 
    4551     /**
    4552      * Validate and sanitise the parameters for the suggestion service query.
    4553      *
    4554      * @return true|WP_Error If validation fails, return a WP_Error object. On success, return true (bool).
    4555      * @since BuddyPress (2.1.0)
    4556      */
    4557     public function validate() {
    4558         $this->args['group_id'] = (int) $this->args['group_id'];
    4559         $this->args             = apply_filters( 'bp_groups_member_suggestions_args', $this->args, $this );
    4560 
    4561         // Check for invalid or missing mandatory parameters.
    4562         if ( ! $this->args['group_id'] || ! bp_is_active( 'groups' ) ) {
    4563             return new WP_Error( 'missing_requirement' );
    4564         }
    4565 
    4566         // Check that the specified group_id exists, and that the current user can access it.
    4567         $the_group = groups_get_group( array(
    4568             'group_id'        => absint( $this->args['group_id'] ),
    4569             'populate_extras' => true,
    4570         ) );
    4571 
    4572         if ( $the_group->id === 0 || ! $the_group->user_has_access ) {
    4573             return new WP_Error( 'access_denied' );
    4574         }
    4575 
    4576         return apply_filters( 'bp_groups_member_suggestions_validate_args', parent::validate(), $this );
    4577     }
    4578 
    4579     /**
    4580      * Find and return a list of username suggestions that match the query.
    4581      *
    4582      * @return array|WP_Error Array of results. If there were problems, returns a WP_Error object.
    4583      * @since BuddyPress (2.1.0)
    4584      */
    4585     public function get_suggestions() {
    4586         $user_query = array(
    4587             'count_total'     => '',  // Prevents total count
    4588             'populate_extras' => false,
    4589             'type'            => 'alphabetical',
    4590 
    4591             'group_role'      => array( 'admin', 'member', 'mod' ),
    4592             'page'            => 1,
    4593             'per_page'        => $this->args['limit'],
    4594             'search_terms'    => $this->args['term'],
    4595             'search_wildcard' => 'right',
    4596         );
    4597 
    4598         // Only return matches of friends of this user.
    4599         if ( $this->args['only_friends'] && is_user_logged_in() ) {
    4600             $user_query['user_id'] = get_current_user_id();
    4601         }
    4602 
    4603         // Positive Group IDs will restrict the search to members in that group.
    4604         if ( $this->args['group_id'] > 0 ) {
    4605             $user_query['group_id'] = $this->args['group_id'];
    4606 
    4607         // Negative Group IDs will restrict the search to members in every other group.
    4608         } else {
    4609             $group_query = array(
    4610                 'count_total'     => '',  // Prevents total count
    4611                 'populate_extras' => false,
    4612                 'type'            => 'alphabetical',
    4613 
    4614                 'group_id'        => absint( $this->args['group_id'] ),
    4615                 'group_role'      => array( 'admin', 'member', 'mod' ),
    4616                 'page'            => 1,
    4617             );
    4618             $group_users = new BP_Group_Member_Query( $group_query );
    4619 
    4620             if ( $group_users->results ) {
    4621                 $user_query['exclude'] = wp_list_pluck( $group_users->results, 'ID' );
    4622             } else {
    4623                 $user_query['include'] = array( 0 );
    4624             }
    4625         }
    4626 
    4627         $user_query = apply_filters( 'bp_groups_member_suggestions_query_args', $user_query, $this );
    4628         if ( is_wp_error( $user_query ) ) {
    4629             return $user_query;
    4630         }
    4631 
    4632 
    4633         if ( isset( $user_query['group_id'] ) ) {
    4634             $user_query = new BP_Group_Member_Query( $user_query );
    4635         } else {
    4636             $user_query = new BP_User_Query( $user_query );
    4637         }
    4638 
    4639         $results = array();
    4640         foreach ( $user_query->results as $user ) {
    4641             $result        = new stdClass();
    4642             $result->ID    = $user->user_nicename;
    4643             $result->image = bp_core_fetch_avatar( array( 'html' => false, 'item_id' => $user->ID ) );
    4644             $result->name  = bp_core_get_user_displayname( $user->ID );
    4645 
    4646             $results[] = $result;
    4647         }
    4648 
    4649         return apply_filters( 'bp_groups_member_suggestions_get_suggestions', $results, $this );
    4650     }
    4651 }
     12require __DIR__ . '/classes/class-bp-group-extension.php';
     13require __DIR__ . '/classes/class-bp-group-member-query.php';
     14require __DIR__ . '/classes/class-bp-groups-group.php';
     15require __DIR__ . '/classes/class-bp-groups-member-suggestions.php';
     16require __DIR__ . '/classes/class-bp-groups-member.php';
  • trunk/src/bp-members/bp-members-classes.php

    r9308 r9485  
    11<?php
    2 
    32/**
    4  * Signups Management class.
     3 * BuddyPress Members Classes
    54 *
    65 * @package BuddyPress
    7  * @subpackage coreClasses
    8  *
    9  * @since BuddyPress (2.0.0)
     6 * @subpackage MembersClasses
    107 */
    11 class BP_Signup {
    128
    13     /**
    14      * ID of the signup which the object relates to.
    15      *
    16      * @var integer
    17      */
    18     public $id;
     9// Exit if accessed directly
     10defined( 'ABSPATH' ) || exit;
    1911
    20     /**
    21      * The URL to the full size of the avatar for the user.
    22      *
    23      * @var string
    24      */
    25     public $avatar;
    26 
    27     /**
    28      * The username for the user.
    29      *
    30      * @var string
    31      */
    32     public $user_login;
    33 
    34     /**
    35      * The email for the user.
    36      *
    37      * @var string
    38      */
    39     public $user_email;
    40 
    41     /**
    42      * The full name of the user.
    43      *
    44      * @var string
    45      */
    46     public $user_name;
    47 
    48     /**
    49      * Metadata associated with the signup.
    50      *
    51      * @var array
    52      */
    53     public $meta;
    54 
    55     /**
    56      * The registered date for the user.
    57      *
    58      * @var string
    59      */
    60     public $registered;
    61 
    62     /**
    63      * The activation key for the user.
    64      *
    65      * @var string
    66      */
    67     public $activation_key;
    68 
    69 
    70     /** Public Methods *******************************************************/
    71 
    72     /**
    73      * Class constructor.
    74      *
    75      * @since BuddyPress (2.0.0)
    76      *
    77      * @param integer $signup_id The ID for the signup being queried.
    78      */
    79     public function __construct( $signup_id = 0 ) {
    80         if ( !empty( $signup_id ) ) {
    81             $this->id = $signup_id;
    82             $this->populate();
    83         }
    84     }
    85 
    86     /**
    87      * Populate the instantiated class with data based on the signup_id provided.
    88      *
    89      * @since BuddyPress (2.0.0)
    90      */
    91     public function populate() {
    92         global $wpdb;
    93 
    94         $signups_table = buddypress()->members->table_name_signups;
    95         $signup        = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$signups_table} WHERE signup_id = %d AND active = 0", $this->id ) );
    96 
    97         $this->avatar         = get_avatar( $signup->user_email, 32 );
    98         $this->user_login     = $signup->user_login;
    99         $this->user_email     = $signup->user_email;
    100         $this->meta           = maybe_unserialize( $signup->meta );
    101         $this->user_name      = ! empty( $this->meta['field_1'] ) ? wp_unslash( $this->meta['field_1'] ) : '';
    102         $this->registered     = $signup->registered;
    103         $this->activation_key = $signup->activation_key;
    104     }
    105 
    106     /** Static Methods *******************************************************/
    107 
    108     /**
    109      * Fetch signups based on parameters.
    110      *
    111      * @since BuddyPress (2.0.0)
    112      *
    113      * @param array $args the argument to retrieve desired signups
    114      * @return array {
    115      *     @type array $signups Located signups.
    116      *     @type int $total Total number of signups matching params.
    117      * }
    118      */
    119     public static function get( $args = array() ) {
    120         global $wpdb;
    121 
    122         $r = bp_parse_args( $args,
    123             array(
    124                 'offset'         => 0,
    125                 'number'         => 1,
    126                 'usersearch'     => false,
    127                 'orderby'        => 'signup_id',
    128                 'order'          => 'DESC',
    129                 'include'        => false,
    130                 'activation_key' => '',
    131                 'user_login'     => '',
    132             ),
    133             'bp_core_signups_get_args'
    134         );
    135 
    136         // @todo whitelist sanitization
    137         if ( $r['orderby'] !== 'signup_id' ) {
    138             $r['orderby'] = 'user_' . $r['orderby'];
    139         }
    140 
    141         $r['orderby'] = sanitize_title( $r['orderby'] );
    142 
    143         $sql = array();
    144         $signups_table  = buddypress()->members->table_name_signups;
    145         $sql['select']  = "SELECT * FROM {$signups_table}";
    146         $sql['where']   = array();
    147         $sql['where'][] = "active = 0";
    148 
    149         if ( empty( $r['include'] ) ) {
    150 
    151             // Search terms
    152             if ( ! empty( $r['usersearch'] ) ) {
    153                 $search_terms_like = '%' . bp_esc_like( $r['usersearch'] ) . '%';
    154                 $sql['where'][]    = $wpdb->prepare( "( user_login LIKE %s OR user_email LIKE %s OR meta LIKE %s )", $search_terms_like, $search_terms_like, $search_terms_like );
    155             }
    156 
    157             // Activation key
    158             if ( ! empty( $r['activation_key'] ) ) {
    159                 $sql['where'][] = $wpdb->prepare( "activation_key = %s", $r['activation_key'] );
    160             }
    161 
    162             // User login
    163             if ( ! empty( $r['user_login'] ) ) {
    164                 $sql['where'][] = $wpdb->prepare( "user_login = %s", $r['user_login'] );
    165             }
    166 
    167             $sql['orderby'] = "ORDER BY {$r['orderby']}";
    168             $sql['order']   = bp_esc_sql_order( $r['order'] );
    169             $sql['limit']   = $wpdb->prepare( "LIMIT %d, %d", $r['offset'], $r['number'] );
    170         } else {
    171             $in = implode( ',', wp_parse_id_list( $r['include'] ) );
    172             $sql['in'] = "AND signup_id IN ({$in})";
    173         }
    174 
    175         // Implode WHERE clauses
    176         $sql['where'] = 'WHERE ' . implode( ' AND ', $sql['where'] );
    177 
    178         /**
    179          * Filters the Signups paged query.
    180          *
    181          * @since BuddyPress (2.0.0)
    182          *
    183          * @param string $value SQL statement.
    184          * @param array  $sql   Array of SQL statement parts.
    185          * @param array  $args  Array of original arguments for get() method.
    186          * @param array  $r     Array of parsed arguments for get() method.
    187          */
    188         $paged_signups = $wpdb->get_results( apply_filters( 'bp_members_signups_paged_query', join( ' ', $sql ), $sql, $args, $r ) );
    189 
    190         if ( empty( $paged_signups ) ) {
    191             return array( 'signups' => false, 'total' => false );
    192         }
    193 
    194         // Used to calculate a diff between now & last
    195         // time an activation link has been resent
    196         $now = current_time( 'timestamp', true );
    197 
    198         foreach ( (array) $paged_signups as $key => $signup ) {
    199 
    200             $signup->id   = intval( $signup->signup_id );
    201 
    202             $signup->meta = ! empty( $signup->meta ) ? maybe_unserialize( $signup->meta ) : false;
    203 
    204             $signup->user_name = '';
    205             if ( ! empty( $signup->meta['field_1'] ) ) {
    206                 $signup->user_name = wp_unslash( $signup->meta['field_1'] );
    207             }
    208 
    209             // Sent date defaults to date of registration
    210             if ( ! empty( $signup->meta['sent_date'] ) ) {
    211                 $signup->date_sent = $signup->meta['sent_date'];
    212             } else {
    213                 $signup->date_sent = $signup->registered;
    214             }
    215 
    216             $sent_at = mysql2date('U', $signup->date_sent );
    217             $diff    = $now - $sent_at;
    218 
    219             /**
    220              * add a boolean in case the last time an activation link
    221              * has been sent happened less than a day ago
    222              */
    223             if ( $diff < 1 * DAY_IN_SECONDS ) {
    224                 $signup->recently_sent = true;
    225             }
    226 
    227             if ( ! empty( $signup->meta['count_sent'] ) ) {
    228                 $signup->count_sent = absint( $signup->meta['count_sent'] );
    229             } else {
    230                 $signup->count_sent = 1;
    231             }
    232 
    233             $paged_signups[ $key ] = $signup;
    234         }
    235 
    236         unset( $sql['limit'] );
    237         $sql['select'] = preg_replace( "/SELECT.*?FROM/", "SELECT COUNT(*) FROM", $sql['select'] );
    238 
    239         /**
    240          * Filters the Signups count query.
    241          *
    242          * @since BuddyPress (2.0.0)
    243          *
    244          * @param string $value SQL statement.
    245          * @param array  $sql   Array of SQL statement parts.
    246          * @param array  $args  Array of original arguments for get() method.
    247          * @param array  $r     Array of parsed arguments for get() method.
    248          */
    249         $total_signups = $wpdb->get_var( apply_filters( 'bp_members_signups_count_query', join( ' ', $sql ), $sql, $args, $r ) );
    250 
    251         return array( 'signups' => $paged_signups, 'total' => $total_signups );
    252     }
    253 
    254     /**
    255      * Add a signup.
    256      *
    257      * @since BuddyPress (2.0.0)
    258      *
    259      * @param array $args
    260      * @return int|bool ID of newly created signup on success, false on
    261      *         failure.
    262      */
    263     public static function add( $args = array() ) {
    264         global $wpdb;
    265 
    266         $r = bp_parse_args( $args,
    267             array(
    268                 'domain'         => '',
    269                 'path'           => '',
    270                 'title'          => '',
    271                 'user_login'     => '',
    272                 'user_email'     => '',
    273                 'registered'     => current_time( 'mysql', true ),
    274                 'activation_key' => '',
    275                 'meta'           => '',
    276             ),
    277             'bp_core_signups_add_args'
    278         );
    279 
    280         $r['meta'] = maybe_serialize( $r['meta'] );
    281 
    282         $inserted = $wpdb->insert(
    283             buddypress()->members->table_name_signups,
    284             $r,
    285             array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
    286         );
    287 
    288         if ( $inserted ) {
    289             $retval = $wpdb->insert_id;
    290         } else {
    291             $retval = false;
    292         }
    293 
    294         /**
    295          * Filters the result of a signup addition.
    296          *
    297          * @since BuddyPress (2.0.0)
    298          *
    299          * @param int|bool $retval Newly added user ID on success, false on failure.
    300          */
    301         return apply_filters( 'bp_core_signups_add', $retval );
    302     }
    303 
    304     /**
    305      * Create a WP user at signup.
    306      *
    307      * Since BP 2.0, non-multisite configurations have stored signups in
    308      * the same way as Multisite configs traditionally have: in the
    309      * wp_signups table. However, because some plugins may be looking
    310      * directly in the wp_users table for non-activated signups, we
    311      * mirror signups there by creating "phantom" users, mimicking WP's
    312      * default behavior.
    313      *
    314      * @since BuddyPress (2.0.0)
    315      *
    316      * @param string $user_login User login string.
    317      * @param string $user_password User password.
    318      * @param string $user_email User email address.
    319      * @param array $usermeta Metadata associated with the signup.
    320      * @return int User id.
    321      */
    322     public static function add_backcompat( $user_login = '', $user_password = '', $user_email = '', $usermeta = array() ) {
    323         global $wpdb;
    324 
    325         $user_id = wp_insert_user( array(
    326             'user_login'   => $user_login,
    327             'user_pass'    => $user_password,
    328             'display_name' => sanitize_title( $user_login ),
    329             'user_email'   => $user_email
    330         ) );
    331 
    332         if ( is_wp_error( $user_id ) || empty( $user_id ) ) {
    333             return $user_id;
    334         }
    335 
    336         // Update the user status to '2', ie "not activated"
    337         // (0 = active, 1 = spam, 2 = not active)
    338         $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->users} SET user_status = 2 WHERE ID = %d", $user_id ) );
    339 
    340         // WordPress creates these options automatically on
    341         // wp_insert_user(), but we delete them so that inactive
    342         // signups don't appear in various user counts.
    343         delete_user_option( $user_id, 'capabilities' );
    344         delete_user_option( $user_id, 'user_level'   );
    345 
    346         // Set any profile data
    347         if ( bp_is_active( 'xprofile' ) ) {
    348             if ( ! empty( $usermeta['profile_field_ids'] ) ) {
    349                 $profile_field_ids = explode( ',', $usermeta['profile_field_ids'] );
    350 
    351                 foreach ( (array) $profile_field_ids as $field_id ) {
    352                     if ( empty( $usermeta["field_{$field_id}"] ) ) {
    353                         continue;
    354                     }
    355 
    356                     $current_field = $usermeta["field_{$field_id}"];
    357                     xprofile_set_field_data( $field_id, $user_id, $current_field );
    358 
    359                     // Save the visibility level
    360                     $visibility_level = ! empty( $usermeta['field_' . $field_id . '_visibility'] ) ? $usermeta['field_' . $field_id . '_visibility'] : 'public';
    361                     xprofile_set_field_visibility_level( $field_id, $user_id, $visibility_level );
    362                 }
    363             }
    364         }
    365 
    366         /**
    367          * Filters the user ID for the backcompat functionality.
    368          *
    369          * @since BuddyPress (2.0.0)
    370          *
    371          * @param int $user_id User ID being registered.
    372          */
    373         return apply_filters( 'bp_core_signups_add_backcompat', $user_id );
    374     }
    375 
    376     /**
    377      * Check a user status (from wp_users) on a non-multisite config.
    378      *
    379      * @since BuddyPress (2.0.0)
    380      *
    381      * @param int $user_id ID of the user being checked.
    382      * @return int|bool The status if found, otherwise false.
    383      */
    384     public static function check_user_status( $user_id = 0 ) {
    385         global $wpdb;
    386 
    387         if ( empty( $user_id ) ) {
    388             return false;
    389         }
    390 
    391         $user_status = $wpdb->get_var( $wpdb->prepare( "SELECT user_status FROM {$wpdb->users} WHERE ID = %d", $user_id ) );
    392 
    393         /**
    394          * Filters the user status of a provided user ID.
    395          *
    396          * @since BuddyPress (2.0.0)
    397          *
    398          * @param int $value User status of the provided user ID.
    399          */
    400         return apply_filters( 'bp_core_signups_check_user_status', intval( $user_status ) );
    401     }
    402 
    403     /**
    404      * Activate a signup.
    405      *
    406      * @since BuddyPress (2.0.0)
    407      *
    408      * @param string $key Activation key.
    409      * @return bool True on success, false on failure.
    410      */
    411     public static function validate( $key = '' ) {
    412         global $wpdb;
    413 
    414         if ( empty( $key ) ) {
    415             return;
    416         }
    417 
    418         $activated = $wpdb->update(
    419             // Signups table
    420             buddypress()->members->table_name_signups,
    421             array(
    422                 'active' => 1,
    423                 'activated' => current_time( 'mysql', true ),
    424             ),
    425             array(
    426                 'activation_key' => $key,
    427             ),
    428             // Data sanitization format
    429             array(
    430                 '%d',
    431                 '%s',
    432             ),
    433             // WHERE sanitization format
    434             array(
    435                 '%s',
    436             )
    437         );
    438 
    439         /**
    440          * Filters the status of the activated user.
    441          *
    442          * @since BuddyPress (2.0.0)
    443          *
    444          * @param bool $activated Whether or not the activation was successful.
    445          */
    446         return apply_filters( 'bp_core_signups_validate', $activated );
    447     }
    448 
    449     /**
    450      * How many inactive signups do we have?
    451      *
    452      * @since BuddyPress (2.0.0)
    453      *
    454      * @return int the number of signups
    455      */
    456     public static function count_signups() {
    457         global $wpdb;
    458 
    459         $signups_table = buddypress()->members->table_name_signups;
    460         $count_signups = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) AS total FROM {$signups_table} WHERE active = %d", 0 ) );
    461 
    462         /**
    463          * Filters the total inactive signups.
    464          *
    465          * @since BuddyPress (2.0.0)
    466          *
    467          * @param int $count_signups How many total signups there are.
    468          */
    469         return apply_filters( 'bp_core_signups_count', (int) $count_signups );
    470     }
    471 
    472     /**
    473      * Update the meta for a signup.
    474      *
    475      * This is the way we use to "trace" the last date an activation
    476      * email was sent and how many times activation was sent.
    477      *
    478      * @since BuddyPress (2.0.0)
    479      *
    480      * @param  array $args
    481      * @return int the signup id
    482      */
    483     public static function update( $args = array() ) {
    484         global $wpdb;
    485 
    486         $r = bp_parse_args( $args,
    487             array(
    488                 'signup_id'  => 0,
    489                 'meta'       => array(),
    490             ),
    491             'bp_core_signups_update_args'
    492         );
    493 
    494         if ( empty( $r['signup_id'] ) || empty( $r['meta'] ) ) {
    495             return false;
    496         }
    497 
    498         $wpdb->update(
    499             // Signups table
    500             buddypress()->members->table_name_signups,
    501             // Data to update
    502             array(
    503                 'meta' => serialize( $r['meta'] ),
    504             ),
    505             // WHERE
    506             array(
    507                 'signup_id' => $r['signup_id'],
    508             ),
    509             // Data sanitization format
    510             array(
    511                 '%s',
    512             ),
    513             // WHERE sanitization format
    514             array(
    515                 '%d',
    516             )
    517         );
    518 
    519         /**
    520          * Filters the signup ID which received a meta update.
    521          *
    522          * @since BuddyPress (2.0.0)
    523          *
    524          * @param int $value The signup ID.
    525          */
    526         return apply_filters( 'bp_core_signups_update', $r['signup_id'] );
    527     }
    528 
    529     /**
    530      * Resend an activation email.
    531      *
    532      * @since BuddyPress (2.0.0)
    533      *
    534      * @param array $signup_ids Single ID or list of IDs to resend.
    535      * @return array
    536      */
    537     public static function resend( $signup_ids = array() ) {
    538         if ( empty( $signup_ids ) || ! is_array( $signup_ids ) ) {
    539             return false;
    540         }
    541 
    542         $to_resend = self::get( array(
    543             'include' => $signup_ids,
    544         ) );
    545 
    546         if ( ! $signups = $to_resend['signups'] ) {
    547             return false;
    548         }
    549 
    550         $result = array();
    551 
    552         /**
    553          * Fires before activation emails are resent.
    554          *
    555          * @since BuddyPress (2.0.0)
    556          *
    557          * @param array $signup_ids Array of IDs to resend activation emails to.
    558          */
    559         do_action( 'bp_core_signup_before_resend', $signup_ids );
    560 
    561         foreach ( $signups as $signup ) {
    562 
    563             $meta               = $signup->meta;
    564             $meta['sent_date']  = current_time( 'mysql', true );
    565             $meta['count_sent'] = $signup->count_sent + 1;
    566 
    567             // Send activation email
    568             if ( is_multisite() ) {
    569                 wpmu_signup_user_notification( $signup->user_login, $signup->user_email, $signup->activation_key, serialize( $meta ) );
    570             } else {
    571 
    572                 // Check user status before sending email
    573                 $user_id = email_exists( $signup->user_email );
    574 
    575                 if ( ! empty( $user_id ) && 2 != self::check_user_status( $user_id ) ) {
    576 
    577                     // Status is not 2, so user's account has been activated
    578                     $result['errors'][ $signup->signup_id ] = array( $signup->user_login, esc_html__( 'the sign-up has already been activated.', 'buddypress' ) );
    579 
    580                     // repair signups table
    581                     self::validate( $signup->activation_key );
    582 
    583                     continue;
    584 
    585                 // Send the validation email
    586                 } else {
    587                     bp_core_signup_send_validation_email( false, $signup->user_email, $signup->activation_key );
    588                 }
    589             }
    590 
    591             // Update metas
    592             $result['resent'][] = self::update( array(
    593                 'signup_id' => $signup->signup_id,
    594                 'meta'      => $meta,
    595             ) );
    596         }
    597 
    598         /**
    599          * Fires after activation emails are resent.
    600          *
    601          * @since BuddyPress (2.0.0)
    602          *
    603          * @param array $signup_ids Array of IDs to resend activation emails to.
    604          * @param array $result     Updated metadata related to activation emails.
    605          */
    606         do_action( 'bp_core_signup_after_resend', $signup_ids, $result );
    607 
    608         /**
    609          * Filters the result of the metadata for signup activation email resends.
    610          *
    611          * @since BuddyPress (2.0.0)
    612          *
    613          * @param array $result Updated metadata related to activation emails.
    614          */
    615         return apply_filters( 'bp_core_signup_resend', $result );
    616     }
    617 
    618     /**
    619      * Activate a pending account.
    620      *
    621      * @since BuddyPress (2.0.0)
    622      *
    623      * @param array $signup_ids Single ID or list of IDs to activate.
    624      * @return array
    625      */
    626     public static function activate( $signup_ids = array() ) {
    627         if ( empty( $signup_ids ) || ! is_array( $signup_ids ) ) {
    628             return false;
    629         }
    630 
    631         $to_activate = self::get( array(
    632             'include' => $signup_ids,
    633         ) );
    634 
    635         if ( ! $signups = $to_activate['signups'] ) {
    636             return false;
    637         }
    638 
    639         $result = array();
    640 
    641         /**
    642          * Fires before activation of user accounts.
    643          *
    644          * @since BuddyPress (2.0.0)
    645          *
    646          * @param array $signup_ids Array of IDs to activate.
    647          */
    648         do_action( 'bp_core_signup_before_activate', $signup_ids );
    649 
    650         foreach ( $signups as $signup ) {
    651 
    652             $user = bp_core_activate_signup( $signup->activation_key );
    653 
    654             if ( ! empty( $user->errors ) ) {
    655 
    656                 $user_id = username_exists( $signup->user_login );
    657 
    658                 if ( 2 !== self::check_user_status( $user_id ) ) {
    659                     $user_id = false;
    660                 }
    661 
    662                 if ( empty( $user_id ) ) {
    663 
    664                     // Status is not 2, so user's account has been activated
    665                     $result['errors'][ $signup->signup_id ] = array( $signup->user_login, esc_html__( 'the sign-up has already been activated.', 'buddypress' ) );
    666 
    667                     // repair signups table
    668                     self::validate( $signup->activation_key );
    669 
    670                 // we have a user id, account is not active, let's delete it
    671                 } else {
    672                     $result['errors'][ $signup->signup_id ] = array( $signup->user_login, $user->get_error_message() );
    673                 }
    674 
    675             } else {
    676                 $result['activated'][] = $user;
    677             }
    678         }
    679 
    680         /**
    681          * Fires after activation of user accounts.
    682          *
    683          * @since BuddyPress (2.0.0)
    684          *
    685          * @param array $signup_ids Array of IDs activated activate.
    686          * @param array $result     Array of data for activated accounts.
    687          */
    688         do_action( 'bp_core_signup_after_activate', $signup_ids, $result );
    689 
    690         /**
    691          * Filters the result of the metadata after user activation.
    692          *
    693          * @since BuddyPress (2.0.0)
    694          *
    695          * @param array $result Updated metadata related to user activation.
    696          */
    697         return apply_filters( 'bp_core_signup_activate', $result );
    698     }
    699 
    700     /**
    701      * Delete a pending account.
    702      *
    703      * @since BuddyPress (2.0.0)
    704      *
    705      * @param array $signup_ids Single ID or list of IDs to delete.
    706      * @return array
    707      */
    708     public static function delete( $signup_ids = array() ) {
    709         global $wpdb;
    710 
    711         if ( empty( $signup_ids ) || ! is_array( $signup_ids ) ) {
    712             return false;
    713         }
    714 
    715         $to_delete = self::get( array(
    716             'include' => $signup_ids,
    717         ) );
    718 
    719         if ( ! $signups = $to_delete['signups'] ) {
    720             return false;
    721         }
    722 
    723         $result = array();
    724 
    725         /**
    726          * Fires before deletion of pending accounts.
    727          *
    728          * @since BuddyPress (2.0.0)
    729          *
    730          * @param array $signup_ids Array of pending IDs to delete.
    731          */
    732         do_action( 'bp_core_signup_before_delete', $signup_ids );
    733 
    734         foreach ( $signups as $signup ) {
    735             $user_id = username_exists( $signup->user_login );
    736 
    737             if ( ! empty( $user_id ) && $signup->activation_key == wp_hash( $user_id ) ) {
    738 
    739                 if ( 2 != self::check_user_status( $user_id ) ) {
    740 
    741                     // Status is not 2, so user's account has been activated
    742                     $result['errors'][ $signup->signup_id ] = array( $signup->user_login, esc_html__( 'the sign-up has already been activated.', 'buddypress' ) );
    743 
    744                     // repair signups table
    745                     self::validate( $signup->activation_key );
    746 
    747                 // we have a user id, account is not active, let's delete it
    748                 } else {
    749                     bp_core_delete_account( $user_id );
    750                 }
    751             }
    752 
    753             if ( empty( $result['errors'][ $signup->signup_id ] ) ) {
    754                 $wpdb->delete(
    755                     // Signups table
    756                     buddypress()->members->table_name_signups,
    757                     // Where
    758                     array( 'signup_id' => $signup->signup_id, ),
    759                     // WHERE sanitization format
    760                     array( '%d', )
    761                 );
    762 
    763                 $result['deleted'][] = $signup->signup_id;
    764             }
    765         }
    766 
    767         /**
    768          * Fires after deletion of pending accounts.
    769          *
    770          * @since BuddyPress (2.0.0)
    771          *
    772          * @param array $signup_ids Array of pending IDs to delete.
    773          * @param array $result     Array of data for deleted accounts.
    774          */
    775         do_action( 'bp_core_signup_after_delete', $signup_ids, $result );
    776 
    777         /**
    778          * Filters the result of the metadata for deleted pending accounts.
    779          *
    780          * @since BuddyPress (2.0.0)
    781          *
    782          * @param array $result Updated metadata related to deleted pending accounts.
    783          */
    784         return apply_filters( 'bp_core_signup_delete', $result );
    785     }
    786 }
     12require __DIR__ . '/classes/class-bp-signup.php';
  • trunk/src/bp-messages/bp-messages-classes.php

    r9482 r9485  
    11<?php
    2 
    32/**
    43 * BuddyPress Messages Classes
     
    1110defined( 'ABSPATH' ) || exit;
    1211
    13 /**
    14  * BuddyPress Message Thread class.
    15  *
    16  * @since BuddyPress (1.0.0)
    17  */
    18 class BP_Messages_Thread {
    19     /**
    20      * The message thread ID.
    21      *
    22      * @since BuddyPress (1.0.0)
    23      * @var int
    24      */
    25     public $thread_id;
    26 
    27     /**
    28      * The current messages.
    29      *
    30      * @since BuddyPress (1.0.0)
    31      * @var object
    32      */
    33     public $messages;
    34 
    35     /**
    36      * The current recipients in the message thread.
    37      *
    38      * @since BuddyPress (1.0.0)
    39      * @var object
    40      */
    41     public $recipients;
    42 
    43     /**
    44      * The user IDs of all messages in the message thread.
    45      *
    46      * @since BuddyPress (1.2.0)
    47      * @var array
    48      */
    49     public $sender_ids;
    50 
    51     /**
    52      * The unread count for the logged-in user.
    53      *
    54      * @since BuddyPress (1.2.0)
    55      * @var int
    56      */
    57     public $unread_count;
    58 
    59     /**
    60      * The content of the last message in this thread
    61      *
    62      * @since BuddyPress (1.2.0)
    63      * @var string
    64      */
    65     public $last_message_content;
    66 
    67     /**
    68      * The date of the last message in this thread
    69      *
    70      * @since BuddyPress (1.2.0)
    71      * @var string
    72      */
    73     public $last_message_date;
    74 
    75     /**
    76      * The ID of the last message in this thread
    77      *
    78      * @since BuddyPress (1.2.0)
    79      * @var int
    80      */
    81     public $last_message_id;
    82 
    83     /**
    84      * The subject of the last message in this thread
    85      *
    86      * @since BuddyPress (1.2.0)
    87      * @var string
    88      */
    89     public $last_message_subject;
    90 
    91     /**
    92      * The user ID of the author of the last message in this thread
    93      *
    94      * @since BuddyPress (1.2.0)
    95      * @var int
    96      */
    97     public $last_sender_id;
    98 
    99     /**
    100      * Sort order of the messages in this thread (ASC or DESC).
    101      *
    102      * @since BuddyPress (1.5.0)
    103      * @var string
    104      */
    105     public $messages_order;
    106 
    107     /**
    108      * Constructor.
    109      *
    110      * @since BuddyPress (1.0.0)
    111      *
    112      * @see BP_Messages_Thread::populate() for full description of parameters
    113      */
    114     public function __construct( $thread_id = false, $order = 'ASC', $args = array() ) {
    115         if ( $thread_id ) {
    116             $this->populate( $thread_id, $order, $args );
    117         }
    118     }
    119 
    120     /**
    121      * Populate method.
    122      *
    123      * Used in constructor.
    124      *
    125      * @since BuddyPress (1.0.0)
    126      *
    127      * @param int $thread_id The message thread ID.
    128      * @param string $order The order to sort the messages. Either 'ASC' or 'DESC'.
    129      * @param array $args {
    130      *     Array of arguments.
    131      *     @type bool $update_meta_cache Whether to pre-fetch metadata for
    132      *           queried message items. Default: true.
    133      * }
    134      * @return bool False on failure.
    135      */
    136     public function populate( $thread_id = 0, $order = 'ASC', $args = array() ) {
    137         global $wpdb;
    138 
    139         if( 'ASC' != $order && 'DESC' != $order ) {
    140             $order = 'ASC';
    141         }
    142 
    143         // merge $args with our defaults
    144         $r = wp_parse_args( $args, array(
    145             'update_meta_cache' => true
    146         ) );
    147 
    148         $this->messages_order = $order;
    149         $this->thread_id      = $thread_id;
    150 
    151         $bp = buddypress();
    152 
    153         if ( !$this->messages = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$bp->messages->table_name_messages} WHERE thread_id = %d ORDER BY date_sent " . $order, $this->thread_id ) ) ) {
    154             return false;
    155         }
    156 
    157         foreach ( (array) $this->messages as $key => $message ) {
    158             $this->sender_ids[$message->sender_id] = $message->sender_id;
    159         }
    160 
    161         // Fetch the recipients
    162         $this->recipients = $this->get_recipients();
    163 
    164         // Get the unread count for the logged in user
    165         if ( isset( $this->recipients[bp_loggedin_user_id()] ) ) {
    166             $this->unread_count = $this->recipients[bp_loggedin_user_id()]->unread_count;
    167         }
    168 
    169         // Grab all message meta
    170         if ( true === (bool) $r['update_meta_cache'] ) {
    171             bp_messages_update_meta_cache( wp_list_pluck( $this->messages, 'id' ) );
    172         }
    173 
    174         /**
    175          * Fires after a BP_Messages_Thread object has been populated.
    176          *
    177          * @since BuddyPress (2.2.0)
    178          *
    179          * @param BP_Messages_Thread Message thread object.
    180          */
    181         do_action( 'bp_messages_thread_post_populate', $this );
    182     }
    183 
    184     /**
    185      * Mark a thread initialized in this class as read.
    186      *
    187      * @since BuddyPress (1.0.0)
    188      *
    189      * @see BP_Messages_Thread::mark_as_read()
    190      */
    191     public function mark_read() {
    192         BP_Messages_Thread::mark_as_read( $this->thread_id );
    193     }
    194 
    195     /**
    196      * Mark a thread initialized in this class as unread.
    197      *
    198      * @since BuddyPress (1.0.0)
    199      *
    200      * @see BP_Messages_Thread::mark_as_unread()
    201      */
    202     public function mark_unread() {
    203         BP_Messages_Thread::mark_as_unread( $this->thread_id );
    204     }
    205 
    206     /**
    207      * Returns recipients for a message thread.
    208      *
    209      * @since BuddyPress (1.0.0)
    210      *
    211      * @return array
    212      */
    213     public function get_recipients() {
    214         global $wpdb;
    215 
    216         $recipients = wp_cache_get( 'thread_recipients_' . $this->thread_id, 'bp_messages' );
    217         if ( false === $recipients ) {
    218             $bp = buddypress();
    219 
    220             $recipients = array();
    221             $results    = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$bp->messages->table_name_recipients} WHERE thread_id = %d", $this->thread_id ) );
    222 
    223             foreach ( (array) $results as $recipient ) {
    224                 $recipients[ $recipient->user_id ] = $recipient;
    225             }
    226 
    227             wp_cache_set( 'thread_recipients_' . $this->thread_id, $recipients, 'bp_messages' );
    228         }
    229 
    230         /**
    231          * Filters the recipients of a message thread.
    232          *
    233          * @since BuddyPress (2.2.0)
    234          *
    235          * @param array $recipients Array of recipient objects.
    236          * @param int   $thread_id  ID of the current thread.
    237          */
    238         return apply_filters( 'bp_messages_thread_get_recipients', $recipients, $this->thread_id );
    239     }
    240 
    241     /** Static Functions ******************************************************/
    242 
    243     /**
    244      * Mark messages in a thread as deleted or delete all messages in a thread.
    245      *
    246      * Note: All messages in a thread are deleted once every recipient in a thread
    247      * has marked the thread as deleted.
    248      *
    249      * @since BuddyPress (1.0.0)
    250      *
    251      * @param int $thread_id The message thread ID
    252      * @return bool
    253      */
    254     public static function delete( $thread_id ) {
    255         global $wpdb;
    256 
    257         /**
    258          * Fires before a message thread is marked as deleted.
    259          *
    260          * @since BuddyPress (2.2.0)
    261          *
    262          * @param int $thread_id ID of the thread being deleted.
    263          */
    264         do_action( 'bp_messages_thread_before_mark_delete', $thread_id );
    265 
    266         $bp = buddypress();
    267 
    268         // Mark messages as deleted
    269         //
    270         // @todo the reliance on bp_loggedin_user_id() sucks for plugins
    271         //       refactor this method to accept a $user_id parameter
    272         $wpdb->query( $wpdb->prepare( "UPDATE {$bp->messages->table_name_recipients} SET is_deleted = 1 WHERE thread_id = %d AND user_id = %d", $thread_id, bp_loggedin_user_id() ) );
    273 
    274         // Get the message ids in order to pass to the action
    275         $message_ids = $wpdb->get_col( $wpdb->prepare( "SELECT id FROM {$bp->messages->table_name_messages} WHERE thread_id = %d", $thread_id ) );
    276 
    277         // Check to see if any more recipients remain for this message
    278         $recipients = $wpdb->get_results( $wpdb->prepare( "SELECT id FROM {$bp->messages->table_name_recipients} WHERE thread_id = %d AND is_deleted = 0", $thread_id ) );
    279 
    280         // No more recipients so delete all messages associated with the thread
    281         if ( empty( $recipients ) ) {
    282 
    283             /**
    284              * Fires before an entire message thread is deleted.
    285              *
    286              * @since BuddyPress (2.2.0)
    287              *
    288              * @param int   $thread_id   ID of the thread being deleted.
    289              * @param array $message_ids IDs of messages being deleted.
    290              */
    291             do_action( 'bp_messages_thread_before_delete', $thread_id, $message_ids );
    292 
    293             // Delete all the messages
    294             $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->messages->table_name_messages} WHERE thread_id = %d", $thread_id ) );
    295 
    296             // Do something for each message ID
    297             foreach ( $message_ids as $message_id ) {
    298                 // Delete message meta
    299                 bp_messages_delete_meta( $message_id );
    300 
    301                 /**
    302                  * Fires after a message is deleted. This hook is poorly named.
    303                  *
    304                  * @since BuddyPress (1.0.0)
    305                  *
    306                  * @param int $message_id ID of the message
    307                  */
    308                 do_action( 'messages_thread_deleted_thread', $message_id );
    309             }
    310 
    311             // Delete all the recipients
    312             $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->messages->table_name_recipients} WHERE thread_id = %d", $thread_id ) );
    313         }
    314 
    315         /**
    316          * Fires after a message thread is either marked as deleted or deleted
    317          *
    318          * @since BuddyPress (2.2.0)
    319          *
    320          * @param int   $thread_id   ID of the thread being deleted.
    321          * @param array $message_ids IDs of messages being deleted.
    322          */
    323         do_action( 'bp_messages_thread_after_delete', $thread_id, $message_ids );
    324 
    325         return true;
    326     }
    327 
    328     /**
    329      * Get current message threads for a user.
    330      *
    331      * @since BuddyPress (1.0.0)
    332      *
    333      * @param array $args {
    334      *     Array of arguments.
    335      *     @type int    $user_id      The user ID.
    336      *     @type string $box          The type of mailbox to get. Either 'inbox' or 'sentbox'.
    337      *                                Defaults to 'inbox'.
    338      *     @type string $type         The type of messages to get. Either 'all' or 'unread'
    339      *                                or 'read'. Defaults to 'all'.
    340      *     @type int    $limit        The number of messages to get. Defaults to null.
    341      *     @type int    $page         The page number to get. Defaults to null.
    342      *     @type string $search_terms The search term to use. Defaults to ''.
    343      *     @type array  $meta_query   Meta query arguments. See WP_Meta_Query for more details.
    344      * }
    345      * @return array|bool Array on success. Boolean false on failure.
    346      */
    347     public static function get_current_threads_for_user( $args = array() ) {
    348         global $wpdb;
    349 
    350         // Backward compatibility with old method of passing arguments
    351         if ( ! is_array( $args ) || func_num_args() > 1 ) {
    352             _deprecated_argument( __METHOD__, '2.2.0', 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__ ) );
    353 
    354             $old_args_keys = array(
    355                 0 => 'user_id',
    356                 1 => 'box',
    357                 2 => 'type',
    358                 3 => 'limit',
    359                 4 => 'page',
    360                 5 => 'search_terms',
    361             );
    362 
    363             $func_args = func_get_args();
    364             $args      = bp_core_parse_args_array( $old_args_keys, $func_args );
    365         }
    366 
    367         $defaults = array(
    368             'user_id'      => false,
    369             'box'          => 'inbox',
    370             'type'         => 'all',
    371             'limit'        => null,
    372             'page'         => null,
    373             'search_terms' => '',
    374             'meta_query'   => array()
    375         );
    376         $r = wp_parse_args( $args, $defaults );
    377 
    378         $pag_sql = $type_sql = $search_sql = $user_id_sql = $sender_sql = '';
    379         $meta_query_sql = array(
    380             'join'  => '',
    381             'where' => ''
    382         );
    383 
    384         if ( $r['limit'] && $r['page'] ) {
    385             $pag_sql = $wpdb->prepare( " LIMIT %d, %d", intval( ( $r['page'] - 1 ) * $r['limit'] ), intval( $r['limit'] ) );
    386         }
    387 
    388         if ( $r['type'] == 'unread' ) {
    389             $type_sql = " AND r.unread_count != 0 ";
    390         } elseif ( $r['type'] == 'read' ) {
    391             $type_sql = " AND r.unread_count = 0 ";
    392         }
    393 
    394         if ( ! empty( $r['search_terms'] ) ) {
    395             $search_terms_like = '%' . bp_esc_like( $r['search_terms'] ) . '%';
    396             $search_sql        = $wpdb->prepare( "AND ( subject LIKE %s OR message LIKE %s )", $search_terms_like, $search_terms_like );
    397         }
    398 
    399         if ( ! empty( $r['user_id'] ) ) {
    400             if ( 'sentbox' == $r['box'] ) {
    401                 $user_id_sql = 'AND ' . $wpdb->prepare( 'm.sender_id = %d', $r['user_id'] );
    402                 $sender_sql  = ' AND m.sender_id = r.user_id';
    403             } else {
    404                 $user_id_sql = 'AND ' . $wpdb->prepare( 'r.user_id = %d', $r['user_id'] );
    405                 $sender_sql  = ' AND r.sender_only = 0';
    406             }
    407         }
    408 
    409         // Process meta query into SQL
    410         $meta_query = self::get_meta_query_sql( $r['meta_query'] );
    411         if ( ! empty( $meta_query['join'] ) ) {
    412             $meta_query_sql['join'] = $meta_query['join'];
    413         }
    414         if ( ! empty( $meta_query['where'] ) ) {
    415             $meta_query_sql['where'] = $meta_query['where'];
    416         }
    417 
    418         $bp = buddypress();
    419 
    420         // set up SQL array
    421         $sql = array();
    422         $sql['select'] = 'SELECT m.thread_id, MAX(m.date_sent) AS date_sent';
    423         $sql['from']   = "FROM {$bp->messages->table_name_recipients} r INNER JOIN {$bp->messages->table_name_messages} m ON m.thread_id = r.thread_id {$meta_query_sql['join']}";
    424         $sql['where']  = "WHERE r.is_deleted = 0 {$user_id_sql} {$sender_sql} {$type_sql} {$search_sql} {$meta_query_sql['where']}";
    425         $sql['misc']   = "GROUP BY m.thread_id ORDER BY date_sent DESC {$pag_sql}";
    426 
    427         // get thread IDs
    428         $thread_ids = $wpdb->get_results( implode( ' ', $sql ) );
    429         if ( empty( $thread_ids ) ) {
    430             return false;
    431         }
    432 
    433         // adjust $sql to work for thread total
    434         $sql['select'] = 'SELECT COUNT( DISTINCT m.thread_id )';
    435         unset( $sql['misc'] );
    436         $total_threads = $wpdb->get_var( implode( ' ', $sql ) );
    437 
    438         // Sort threads by date_sent
    439         foreach( (array) $thread_ids as $thread ) {
    440             $sorted_threads[$thread->thread_id] = strtotime( $thread->date_sent );
    441         }
    442 
    443         arsort( $sorted_threads );
    444 
    445         $threads = false;
    446         foreach ( (array) $sorted_threads as $thread_id => $date_sent ) {
    447             $threads[] = new BP_Messages_Thread( $thread_id, 'ASC', array(
    448                 'update_meta_cache' => false
    449             ) );
    450         }
    451 
    452         /**
    453          * Filters the results of the query for a user's message threads.
    454          *
    455          * @since BuddyPress (2.2.0)
    456          *
    457          * @param array $value {
    458          *     @type array $threads       Array of threads. Passed by reference.
    459          *     @type int   $total_threads Number of threads found by the query.
    460          * }
    461          */
    462         return apply_filters( 'bp_messages_thread_current_threads', array( 'threads' => &$threads, 'total' => (int) $total_threads ) );
    463     }
    464 
    465     /**
    466      * Get the SQL for the 'meta_query' param in BP_Messages_Thread::get_current_threads_for_user().
    467      *
    468      * We use WP_Meta_Query to do the heavy lifting of parsing the meta_query array
    469      * and creating the necessary SQL clauses.
    470      *
    471      * @since BuddyPress (2.2.0)
    472      *
    473      * @param array $meta_query An array of meta_query filters. See the
    474      *   documentation for WP_Meta_Query for details.
    475      * @return array $sql_array 'join' and 'where' clauses.
    476      */
    477     public static function get_meta_query_sql( $meta_query = array() ) {
    478         global $wpdb;
    479 
    480         $sql_array = array(
    481             'join'  => '',
    482             'where' => '',
    483         );
    484 
    485         if ( ! empty( $meta_query ) ) {
    486             $meta_query = new WP_Meta_Query( $meta_query );
    487 
    488             // WP_Meta_Query expects the table name at
    489             // $wpdb->messagemeta
    490             $wpdb->messagemeta = buddypress()->messages->table_name_meta;
    491 
    492             return $meta_query->get_sql( 'message', 'm', 'id' );
    493         }
    494 
    495         return $sql_array;
    496     }
    497 
    498     /**
    499      * Mark a thread as read.
    500      *
    501      * @since BuddyPress (1.0.0)
    502      *
    503      * @param int $thread_id The message thread ID.
    504      */
    505     public static function mark_as_read( $thread_id ) {
    506         global $wpdb;
    507 
    508         $bp  = buddypress();
    509         $sql = $wpdb->prepare( "UPDATE {$bp->messages->table_name_recipients} SET unread_count = 0 WHERE user_id = %d AND thread_id = %d", bp_loggedin_user_id(), $thread_id );
    510         $wpdb->query($sql);
    511 
    512         wp_cache_delete( bp_loggedin_user_id(), 'bp_messages_unread_count' );
    513     }
    514 
    515     /**
    516      * Mark a thread as unread.
    517      *
    518      * @since BuddyPress (1.0.0)
    519      *
    520      * @param int $thread_id The message thread ID.
    521      */
    522     public static function mark_as_unread( $thread_id ) {
    523         global $wpdb;
    524 
    525         $bp  = buddypress();
    526         $sql = $wpdb->prepare( "UPDATE {$bp->messages->table_name_recipients} SET unread_count = 1 WHERE user_id = %d AND thread_id = %d", bp_loggedin_user_id(), $thread_id );
    527         $wpdb->query($sql);
    528 
    529         wp_cache_delete( bp_loggedin_user_id(), 'bp_messages_unread_count' );
    530     }
    531 
    532     /**
    533      * Returns the total number of message threads for a user.
    534      *
    535      * @since BuddyPress (1.0.0)
    536      *
    537      * @param int    $user_id The user ID.
    538      * @param string $box  The type of mailbox to get. Either 'inbox' or 'sentbox'.
    539      *                     Defaults to 'inbox'.
    540      * @param string $type The type of messages to get. Either 'all' or 'unread'
    541      *                     or 'read'. Defaults to 'all'.
    542      * @return int
    543      */
    544     public static function get_total_threads_for_user( $user_id, $box = 'inbox', $type = 'all' ) {
    545         global $wpdb;
    546 
    547         $exclude_sender = '';
    548         if ( $box != 'sentbox' )
    549             $exclude_sender = ' AND sender_only != 1';
    550 
    551         if ( $type == 'unread' )
    552             $type_sql = " AND unread_count != 0 ";
    553         elseif ( $type == 'read' )
    554             $type_sql = " AND unread_count = 0 ";
    555 
    556         $bp = buddypress();
    557 
    558         return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(thread_id) FROM {$bp->messages->table_name_recipients} WHERE user_id = %d AND is_deleted = 0{$exclude_sender} {$type_sql}", $user_id ) );
    559     }
    560 
    561     /**
    562      * Determine if the logged-in user is a sender of any message in a thread.
    563      *
    564      * @since BuddyPress (1.0.0)
    565      *
    566      * @param int $thread_id The message thread ID.
    567      * @return bool
    568      */
    569     public static function user_is_sender( $thread_id ) {
    570         global $wpdb;
    571 
    572         $bp = buddypress();
    573 
    574         $sender_ids = $wpdb->get_col( $wpdb->prepare( "SELECT sender_id FROM {$bp->messages->table_name_messages} WHERE thread_id = %d", $thread_id ) );
    575 
    576         if ( ! $sender_ids ) {
    577             return false;
    578         }
    579 
    580         return in_array( bp_loggedin_user_id(), $sender_ids );
    581     }
    582 
    583     /**
    584      * Returns the userlink of the last sender in a message thread.
    585      *
    586      * @since BuddyPress (1.0.0)
    587      *
    588      * @param int $thread_id The message thread ID.
    589      * @return string|bool The user link on success. Boolean false on failure.
    590      */
    591     public static function get_last_sender( $thread_id ) {
    592         global $wpdb;
    593 
    594         $bp = buddypress();
    595 
    596         if ( ! $sender_id = $wpdb->get_var( $wpdb->prepare( "SELECT sender_id FROM {$bp->messages->table_name_messages} WHERE thread_id = %d GROUP BY sender_id ORDER BY date_sent LIMIT 1", $thread_id ) ) ) {
    597             return false;
    598         }
    599 
    600         return bp_core_get_userlink( $sender_id, true );
    601     }
    602 
    603     /**
    604      * Gets the unread message count for a user.
    605      *
    606      * @since BuddyPress (1.0.0)
    607      *
    608      * @param int $user_id The user ID.
    609      * @return int
    610      */
    611     public static function get_inbox_count( $user_id = 0 ) {
    612         global $wpdb;
    613 
    614         if ( empty( $user_id ) ) {
    615             $user_id = bp_loggedin_user_id();
    616         }
    617 
    618         $unread_count = wp_cache_get( $user_id, 'bp_messages_unread_count' );
    619 
    620         if ( false === $unread_count ) {
    621             $bp = buddypress();
    622 
    623             $unread_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT SUM(unread_count) FROM {$bp->messages->table_name_recipients} WHERE user_id = %d AND is_deleted = 0 AND sender_only = 0", $user_id ) );
    624 
    625             wp_cache_set( $user_id, $unread_count, 'bp_messages_unread_count' );
    626         }
    627 
    628         /**
    629          * Filters a user's unread message count.
    630          *
    631          * @since BuddyPress (2.2.0)
    632          *
    633          * @param int $unread_count Unread message count.
    634          * @param int $user_id      ID of the user.
    635          */
    636         return apply_filters( 'messages_thread_get_inbox_count', (int) $unread_count, $user_id );
    637     }
    638 
    639     /**
    640      * Checks whether a user is a part of a message thread discussion.
    641      *
    642      * @since BuddyPress (1.0.0)
    643      *
    644      * @param int $thread_id The message thread ID.
    645      * @param int $user_id The user ID.
    646      * @return int The message ID on success.
    647      */
    648     public static function check_access( $thread_id, $user_id = 0 ) {
    649         global $wpdb;
    650 
    651         if ( empty( $user_id ) )
    652             $user_id = bp_loggedin_user_id();
    653 
    654         $bp = buddypress();
    655 
    656         return $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$bp->messages->table_name_recipients} WHERE thread_id = %d AND is_deleted = 0 AND user_id = %d", $thread_id, $user_id ) );
    657     }
    658 
    659     /**
    660      * Checks whether a message thread exists.
    661      *
    662      * @since BuddyPress (1.0.0)
    663      *
    664      * @param int $thread_id The message thread ID.
    665      * @return int The message thread ID on success.
    666      */
    667     public static function is_valid( $thread_id = 0 ) {
    668         global $wpdb;
    669 
    670         // Bail if no thread ID is passed
    671         if ( empty( $thread_id ) ) {
    672             return false;
    673         }
    674 
    675         $bp = buddypress();
    676 
    677         return $wpdb->get_var( $wpdb->prepare( "SELECT thread_id FROM {$bp->messages->table_name_messages} WHERE thread_id = %d LIMIT 1", $thread_id ) );
    678     }
    679 
    680     /**
    681      * Returns a string containing all the message recipient userlinks.
    682      *
    683      * String is comma-delimited.
    684      *
    685      * If a message thread has more than four users, the returned string is simply
    686      * "X Recipients" where "X" is the number of recipients in the message thread.
    687      *
    688      * @since BuddyPress (1.0.0)
    689      *
    690      * @param array $recipients Array containing the message recipients (array of objects).
    691      * @return string
    692      */
    693     public static function get_recipient_links( $recipients ) {
    694         if ( count( $recipients ) >= 5 )
    695             return sprintf( __( '%s Recipients', 'buddypress' ), number_format_i18n( count( $recipients ) ) );
    696 
    697         $recipient_links = array();
    698 
    699         foreach ( (array) $recipients as $recipient ) {
    700             $recipient_link = bp_core_get_userlink( $recipient->user_id );
    701 
    702             if ( empty( $recipient_link ) ) {
    703                 $recipient_link = __( 'Deleted User', 'buddypress' );
    704             }
    705 
    706             $recipient_links[] = $recipient_link;
    707         }
    708 
    709         return implode( ', ', (array) $recipient_links );
    710     }
    711 
    712     /**
    713      * Upgrade method for the older BP message thread DB table.
    714      *
    715      * @since BuddyPress (1.2.0)
    716      *
    717      * @todo We should remove this.  No one is going to upgrade from v1.1, right?
    718      * @return bool
    719      */
    720     public static function update_tables() {
    721         global $wpdb;
    722 
    723         $bp_prefix = bp_core_get_table_prefix();
    724         $errors    = false;
    725         $threads   = $wpdb->get_results( "SELECT * FROM {$bp_prefix}bp_messages_threads" );
    726 
    727         // Nothing to update, just return true to remove the table
    728         if ( empty( $threads ) ) {
    729             return true;
    730         }
    731 
    732         $bp = buddypress();
    733 
    734         foreach( (array) $threads as $thread ) {
    735             $message_ids = maybe_unserialize( $thread->message_ids );
    736 
    737             if ( !empty( $message_ids ) ) {
    738                 $message_ids = implode( ',', $message_ids );
    739 
    740                 // Add the thread_id to the messages table
    741                 if ( ! $wpdb->query( $wpdb->prepare( "UPDATE {$bp->messages->table_name_messages} SET thread_id = %d WHERE id IN ({$message_ids})", $thread->id ) ) )
    742                     $errors = true;
    743             }
    744         }
    745 
    746         if ( $errors ) {
    747             return false;
    748         }
    749 
    750         return true;
    751     }
    752 }
    753 
    754 /**
    755  * Single message class.
    756  */
    757 class BP_Messages_Message {
    758     /**
    759      * ID of the message.
    760      *
    761      * @var int
    762      */
    763     public $id;
    764 
    765     /**
    766      * ID of the message thread.
    767      *
    768      * @var int
    769      */
    770     public $thread_id;
    771 
    772     /**
    773      * ID of the sender.
    774      *
    775      * @var int
    776      */
    777     public $sender_id;
    778 
    779     /**
    780      * Subject line of the message.
    781      *
    782      * @var string
    783      */
    784     public $subject;
    785 
    786     /**
    787      * Content of the message.
    788      *
    789      * @var string
    790      */
    791     public $message;
    792 
    793     /**
    794      * Date the message was sent.
    795      *
    796      * @var string
    797      */
    798     public $date_sent;
    799 
    800     /**
    801      * Message recipients.
    802      *
    803      * @var bool|array
    804      */
    805     public $recipients = false;
    806 
    807     /**
    808      * Constructor.
    809      *
    810      * @param int $id Optional. ID of the message.
    811      */
    812     public function __construct( $id = null ) {
    813         $this->date_sent = bp_core_current_time();
    814         $this->sender_id = bp_loggedin_user_id();
    815 
    816         if ( !empty( $id ) ) {
    817             $this->populate( $id );
    818         }
    819     }
    820 
    821     /**
    822      * Set up data related to a specific message object.
    823      *
    824      * @param int $id ID of the message.
    825      */
    826     public function populate( $id ) {
    827         global $wpdb;
    828 
    829         $bp = buddypress();
    830 
    831         if ( $message = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$bp->messages->table_name_messages} WHERE id = %d", $id ) ) ) {
    832             $this->id        = $message->id;
    833             $this->thread_id = $message->thread_id;
    834             $this->sender_id = $message->sender_id;
    835             $this->subject   = $message->subject;
    836             $this->message   = $message->message;
    837             $this->date_sent = $message->date_sent;
    838         }
    839     }
    840 
    841     /**
    842      * Send a message.
    843      *
    844      * @return int|bool ID of the newly created message on success, false
    845      *         on failure.
    846      */
    847     public function send() {
    848         global $wpdb;
    849 
    850         $bp = buddypress();
    851 
    852         $this->sender_id = apply_filters( 'messages_message_sender_id_before_save', $this->sender_id, $this->id );
    853         $this->thread_id = apply_filters( 'messages_message_thread_id_before_save', $this->thread_id, $this->id );
    854         $this->subject   = apply_filters( 'messages_message_subject_before_save',   $this->subject,   $this->id );
    855         $this->message   = apply_filters( 'messages_message_content_before_save',   $this->message,   $this->id );
    856         $this->date_sent = apply_filters( 'messages_message_date_sent_before_save', $this->date_sent, $this->id );
    857 
    858         /**
    859          * Fires before the current message item gets saved.
    860          *
    861          * Please use this hook to filter the properties above. Each part will be passed in.
    862          *
    863          * @since BuddyPress (1.0.0)
    864          *
    865          * @param BP_Messages_Message Current instance of the message item being saved. Passed by reference.
    866          */
    867         do_action_ref_array( 'messages_message_before_save', array( &$this ) );
    868 
    869         // Make sure we have at least one recipient before sending.
    870         if ( empty( $this->recipients ) )
    871             return false;
    872 
    873         $new_thread = false;
    874 
    875         // If we have no thread_id then this is the first message of a new thread.
    876         if ( empty( $this->thread_id ) ) {
    877             $this->thread_id = (int) $wpdb->get_var( "SELECT MAX(thread_id) FROM {$bp->messages->table_name_messages}" ) + 1;
    878             $new_thread = true;
    879         }
    880 
    881         // First insert the message into the messages table
    882         if ( !$wpdb->query( $wpdb->prepare( "INSERT INTO {$bp->messages->table_name_messages} ( thread_id, sender_id, subject, message, date_sent ) VALUES ( %d, %d, %s, %s, %s )", $this->thread_id, $this->sender_id, $this->subject, $this->message, $this->date_sent ) ) )
    883             return false;
    884 
    885         $this->id = $wpdb->insert_id;
    886 
    887         $recipient_ids = array();
    888 
    889         if ( $new_thread ) {
    890             // Add an recipient entry for all recipients
    891             foreach ( (array) $this->recipients as $recipient ) {
    892                 $wpdb->query( $wpdb->prepare( "INSERT INTO {$bp->messages->table_name_recipients} ( user_id, thread_id, unread_count ) VALUES ( %d, %d, 1 )", $recipient->user_id, $this->thread_id ) );
    893                 $recipient_ids[] = $recipient->user_id;
    894             }
    895 
    896             // Add a sender recipient entry if the sender is not in the list of recipients
    897             if ( !in_array( $this->sender_id, $recipient_ids ) )
    898                 $wpdb->query( $wpdb->prepare( "INSERT INTO {$bp->messages->table_name_recipients} ( user_id, thread_id, sender_only ) VALUES ( %d, %d, 1 )", $this->sender_id, $this->thread_id ) );
    899         } else {
    900             // Update the unread count for all recipients
    901             $wpdb->query( $wpdb->prepare( "UPDATE {$bp->messages->table_name_recipients} SET unread_count = unread_count + 1, sender_only = 0, is_deleted = 0 WHERE thread_id = %d AND user_id != %d", $this->thread_id, $this->sender_id ) );
    902         }
    903 
    904         messages_remove_callback_values();
    905 
    906         /**
    907          * Fires after the current message item has been saved.
    908          *
    909          * @since BuddyPress (1.0.0)
    910          *
    911          * @param BP_Messages_Message Current instance of the message item being saved. Passed by reference.
    912          */
    913         do_action_ref_array( 'messages_message_after_save', array( &$this ) );
    914 
    915         return $this->id;
    916     }
    917 
    918     /**
    919      * Get a list of recipients for a message.
    920      *
    921      * @return array
    922      */
    923     public function get_recipients() {
    924         global $wpdb;
    925 
    926         $bp = buddypress();
    927 
    928         return $wpdb->get_results( $wpdb->prepare( "SELECT user_id FROM {$bp->messages->table_name_recipients} WHERE thread_id = %d", $this->thread_id ) );
    929     }
    930 
    931     /** Static Functions **************************************************/
    932 
    933     /**
    934      * Get list of recipient IDs from their usernames.
    935      *
    936      * @param array $recipient_usernames Usernames of recipients.
    937      * @return array
    938      */
    939     public static function get_recipient_ids( $recipient_usernames ) {
    940         if ( !$recipient_usernames )
    941             return false;
    942 
    943         if ( is_array( $recipient_usernames ) ) {
    944             for ( $i = 0, $count = count( $recipient_usernames ); $i < $count; ++$i ) {
    945                 if ( $rid = bp_core_get_userid( trim($recipient_usernames[$i]) ) ) {
    946                     $recipient_ids[] = $rid;
    947                 }
    948             }
    949         }
    950 
    951         return $recipient_ids;
    952     }
    953 
    954     /**
    955      * Get the ID of the message last sent by the logged-in user for a given thread.
    956      *
    957      * @param int $thread_id ID of the thread.
    958      * @return int|null ID of the message if found, otherwise null.
    959      */
    960     public static function get_last_sent_for_user( $thread_id ) {
    961         global $wpdb;
    962 
    963         $bp = buddypress();
    964 
    965         return $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$bp->messages->table_name_messages} WHERE sender_id = %d AND thread_id = %d ORDER BY date_sent DESC LIMIT 1", bp_loggedin_user_id(), $thread_id ) );
    966     }
    967 
    968     /**
    969      * Check whether a user is the sender of a message.
    970      *
    971      * @param int $user_id ID of the user.
    972      * @param int $message_id ID of the message.
    973      * @return int|null Returns the ID of the message if the user is the
    974      *         sender, otherwise null.
    975      */
    976     public static function is_user_sender( $user_id, $message_id ) {
    977         global $wpdb;
    978 
    979         $bp = buddypress();
    980 
    981         return $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$bp->messages->table_name_messages} WHERE sender_id = %d AND id = %d", $user_id, $message_id ) );
    982     }
    983 
    984     /**
    985      * Get the ID of the sender of a message.
    986      *
    987      * @param int $message_id ID of the message.
    988      * @return int|null The ID of the sender if found, otherwise null.
    989      */
    990     public static function get_message_sender( $message_id ) {
    991         global $wpdb;
    992 
    993         $bp = buddypress();
    994 
    995         return $wpdb->get_var( $wpdb->prepare( "SELECT sender_id FROM {$bp->messages->table_name_messages} WHERE id = %d", $message_id ) );
    996     }
    997 }
    998 
    999 /**
    1000  * BuddyPress Notices class.
    1001  *
    1002  * Use this class to create, activate, deactivate or delete notices.
    1003  *
    1004  * @since BuddyPress (1.0.0)
    1005  */
    1006 class BP_Messages_Notice {
    1007     /**
    1008      * The notice ID.
    1009      *
    1010      * @var int
    1011      */
    1012     public $id = null;
    1013 
    1014     /**
    1015      * The subject line for the notice.
    1016      *
    1017      * @var string
    1018      */
    1019     public $subject;
    1020 
    1021     /**
    1022      * The content of the notice.
    1023      *
    1024      * @var string
    1025      */
    1026     public $message;
    1027 
    1028     /**
    1029      * The date the notice was created.
    1030      *
    1031      * @var string
    1032      */
    1033     public $date_sent;
    1034 
    1035     /**
    1036      * Whether the notice is active or not.
    1037      *
    1038      * @var int
    1039      */
    1040     public $is_active;
    1041 
    1042     /**
    1043      * Constructor.
    1044      *
    1045      * @since BuddyPress (1.0.0)
    1046      * @param int $id Optional. The ID of the current notice.
    1047      */
    1048     public function __construct( $id = null ) {
    1049         if ( $id ) {
    1050             $this->id = $id;
    1051             $this->populate();
    1052         }
    1053     }
    1054 
    1055     /**
    1056      * Populate method.
    1057      *
    1058      * Runs during constructor.
    1059      *
    1060      * @since BuddyPress (1.0.0)
    1061      */
    1062     public function populate() {
    1063         global $wpdb;
    1064 
    1065         $bp = buddypress();
    1066 
    1067         $notice = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$bp->messages->table_name_notices} WHERE id = %d", $this->id ) );
    1068 
    1069         if ( $notice ) {
    1070             $this->subject   = $notice->subject;
    1071             $this->message   = $notice->message;
    1072             $this->date_sent = $notice->date_sent;
    1073             $this->is_active = $notice->is_active;
    1074         }
    1075     }
    1076 
    1077     /**
    1078      * Saves a notice.
    1079      *
    1080      * @since BuddyPress (1.0.0)
    1081      *
    1082      * @return bool
    1083      */
    1084     public function save() {
    1085         global $wpdb;
    1086 
    1087         $bp = buddypress();
    1088 
    1089         $this->subject = apply_filters( 'messages_notice_subject_before_save', $this->subject, $this->id );
    1090         $this->message = apply_filters( 'messages_notice_message_before_save', $this->message, $this->id );
    1091 
    1092         /**
    1093          * Fires before the current message notice item gets saved.
    1094          *
    1095          * Please use this hook to filter the properties above. Each part will be passed in.
    1096          *
    1097          * @since BuddyPress (1.0.0)
    1098          *
    1099          * @param BP_Messages_Notice Current instance of the message notice item being saved. Passed by reference.
    1100          */
    1101         do_action_ref_array( 'messages_notice_before_save', array( &$this ) );
    1102 
    1103         if ( empty( $this->id ) ) {
    1104             $sql = $wpdb->prepare( "INSERT INTO {$bp->messages->table_name_notices} (subject, message, date_sent, is_active) VALUES (%s, %s, %s, %d)", $this->subject, $this->message, $this->date_sent, $this->is_active );
    1105         } else {
    1106             $sql = $wpdb->prepare( "UPDATE {$bp->messages->table_name_notices} SET subject = %s, message = %s, is_active = %d WHERE id = %d", $this->subject, $this->message, $this->is_active, $this->id );
    1107         }
    1108 
    1109         if ( ! $wpdb->query( $sql ) ) {
    1110             return false;
    1111         }
    1112 
    1113         if ( ! $id = $this->id ) {
    1114             $id = $wpdb->insert_id;
    1115         }
    1116 
    1117         // Now deactivate all notices apart from the new one.
    1118         $wpdb->query( $wpdb->prepare( "UPDATE {$bp->messages->table_name_notices} SET is_active = 0 WHERE id != %d", $id ) );
    1119 
    1120         bp_update_user_last_activity( bp_loggedin_user_id(), bp_core_current_time() );
    1121 
    1122         /**
    1123          * Fires after the current message notice item has been saved.
    1124          *
    1125          * @since BuddyPress (1.0.0)
    1126          *
    1127          * @param BP_Messages_Notice Current instance of the message item being saved. Passed by reference.
    1128          */
    1129         do_action_ref_array( 'messages_notice_after_save', array( &$this ) );
    1130 
    1131         return true;
    1132     }
    1133 
    1134     /**
    1135      * Activates a notice.
    1136      *
    1137      * @since BuddyPress (1.0.0)
    1138      *
    1139      * @return bool
    1140      */
    1141     public function activate() {
    1142         $this->is_active = 1;
    1143         return (bool) $this->save();
    1144     }
    1145 
    1146     /**
    1147      * Deactivates a notice.
    1148      *
    1149      * @since BuddyPress (1.0.0)
    1150      *
    1151      * @return bool
    1152      */
    1153     public function deactivate() {
    1154         $this->is_active = 0;
    1155         return (bool) $this->save();
    1156     }
    1157 
    1158     /**
    1159      * Deletes a notice.
    1160      *
    1161      * @since BuddyPress (1.0.0)
    1162      *
    1163      * @return bool
    1164      */
    1165     public function delete() {
    1166         global $wpdb;
    1167 
    1168         /**
    1169          * Fires before the current message item has been deleted.
    1170          *
    1171          * @since BuddyPress (1.0.0)
    1172          *
    1173          * @param BP_Messages_Notice Current instance of the message notice item being deleted.
    1174          */
    1175         do_action( 'messages_notice_before_delete', $this );
    1176 
    1177         $bp  = buddypress();
    1178         $sql = $wpdb->prepare( "DELETE FROM {$bp->messages->table_name_notices} WHERE id = %d", $this->id );
    1179 
    1180         if ( ! $wpdb->query( $sql ) ) {
    1181             return false;
    1182         }
    1183 
    1184         return true;
    1185     }
    1186 
    1187     /** Static Methods ********************************************************/
    1188 
    1189     /**
    1190      * Pulls up a list of notices.
    1191      *
    1192      * To get all notices, pass a value of -1 to pag_num
    1193      *
    1194      * @since BuddyPress (1.0.0)
    1195      *
    1196      * @param array $args {
    1197      *     Array of parameters.
    1198      *     @type int $pag_num Number of notices per page. Defaults to 20.
    1199      *     @type int $pag_page The page number.  Defaults to 1.
    1200      * }
    1201      * @return array
    1202      */
    1203     public static function get_notices( $args = array() ) {
    1204         global $wpdb;
    1205 
    1206         $r = wp_parse_args( $args, array(
    1207             'pag_num'  => 20, // Number of notices per page
    1208             'pag_page' => 1   // Page number
    1209         ) );
    1210 
    1211         $limit_sql = '';
    1212         if ( (int) $r['pag_num'] >= 0 ) {
    1213             $limit_sql = $wpdb->prepare( "LIMIT %d, %d", (int) ( ( $r['pag_page'] - 1 ) * $r['pag_num'] ), (int) $r['pag_num'] );
    1214         }
    1215 
    1216         $bp = buddypress();
    1217 
    1218         $notices = $wpdb->get_results( "SELECT * FROM {$bp->messages->table_name_notices} ORDER BY date_sent DESC {$limit_sql}" );
    1219 
    1220         return $notices;
    1221     }
    1222 
    1223     /**
    1224      * Returns the total number of recorded notices.
    1225      *
    1226      * @since BuddyPress (1.0.0)
    1227      *
    1228      * @return int
    1229      */
    1230     public static function get_total_notice_count() {
    1231         global $wpdb;
    1232 
    1233         $bp = buddypress();
    1234 
    1235         $notice_count = $wpdb->get_var( "SELECT COUNT(id) FROM {$bp->messages->table_name_notices}" );
    1236 
    1237         return $notice_count;
    1238     }
    1239 
    1240     /**
    1241      * Returns the active notice that should be displayed on the frontend.
    1242      *
    1243      * @since BuddyPress (1.0.0)
    1244      *
    1245      * @return object The BP_Messages_Notice object
    1246      */
    1247     public static function get_active() {
    1248         $notice = wp_cache_get( 'active_notice', 'bp_messages' );
    1249 
    1250         if ( false === $notice ) {
    1251             global $wpdb;
    1252 
    1253             $bp = buddypress();
    1254 
    1255             $notice_id = $wpdb->get_var( "SELECT id FROM {$bp->messages->table_name_notices} WHERE is_active = 1" );
    1256             $notice    = new BP_Messages_Notice( $notice_id );
    1257 
    1258             wp_cache_set( 'active_notice', $notice, 'bp_messages' );
    1259         }
    1260 
    1261         return $notice;
    1262     }
    1263 }
     12require __DIR__ . '/classes/class-bp_messages-thread.php';
     13require __DIR__ . '/classes/class-bp-messages-message.php';
     14require __DIR__ . '/classes/class-bp-messages-notice.php';
  • trunk/src/bp-notifications/bp-notifications-classes.php

    r9352 r9485  
    11<?php
    2 
    32/**
    43 * BuddyPress Notifications Classes
     
    1514defined( 'ABSPATH' ) || exit;
    1615
    17 /**
    18  * BuddyPress Notification items.
    19  *
    20  * Use this class to create, access, edit, or delete BuddyPress Notifications.
    21  *
    22  * @since BuddyPress (1.9.0)
    23  */
    24 class BP_Notifications_Notification {
    25 
    26     /**
    27      * The notification ID.
    28      *
    29      * @since BuddyPress (1.9.0)
    30      * @access public
    31      * @var int
    32      */
    33     public $id;
    34 
    35     /**
    36      * The ID of the item associated with the notification.
    37      *
    38      * @since BuddyPress (1.9.0)
    39      * @access public
    40      * @var int
    41      */
    42     public $item_id;
    43 
    44     /**
    45      * The ID of the secondary item associated with the notification.
    46      *
    47      * @since BuddyPress (1.9.0)
    48      * @access public
    49      * @var int
    50      */
    51     public $secondary_item_id = null;
    52 
    53     /**
    54      * The ID of the user the notification is associated with.
    55      *
    56      * @since BuddyPress (1.9.0)
    57      * @access public
    58      * @var int
    59      */
    60     public $user_id;
    61 
    62     /**
    63      * The name of the component that the notification is for.
    64      *
    65      * @since BuddyPress (1.9.0)
    66      * @access public
    67      * @var string
    68      */
    69     public $component_name;
    70 
    71     /**
    72      * The component action which the notification is related to.
    73      *
    74      * @since BuddyPress (1.9.0)
    75      * @access public
    76      * @var string
    77      */
    78     public $component_action;
    79 
    80     /**
    81      * The date the notification was created.
    82      *
    83      * @since BuddyPress (1.9.0)
    84      * @access public
    85      * @var string
    86      */
    87     public $date_notified;
    88 
    89     /**
    90      * Is the notification new, or has it already been read.
    91      *
    92      * @since BuddyPress (1.9.0)
    93      * @access public
    94      * @var bool
    95      */
    96     public $is_new;
    97 
    98     /** Public Methods ****************************************************/
    99 
    100     /**
    101      * Constructor method.
    102      *
    103      * @since BuddyPress (1.9.0)
    104      *
    105      * @param int $id Optional. Provide an ID to access an existing
    106      *        notification item.
    107      */
    108     public function __construct( $id = 0 ) {
    109         if ( ! empty( $id ) ) {
    110             $this->id = $id;
    111             $this->populate();
    112         }
    113     }
    114 
    115     /**
    116      * Update or insert notification details into the database.
    117      *
    118      * @since BuddyPress (1.9.0)
    119      *
    120      * @global wpdb $wpdb WordPress database object.
    121      *
    122      * @return bool True on success, false on failure.
    123      */
    124     public function save() {
    125 
    126         // Return value
    127         $retval = false;
    128 
    129         // Default data and format
    130         $data = array(
    131             'user_id'           => $this->user_id,
    132             'item_id'           => $this->item_id,
    133             'secondary_item_id' => $this->secondary_item_id,
    134             'component_name'    => $this->component_name,
    135             'component_action'  => $this->component_action,
    136             'date_notified'     => $this->date_notified,
    137             'is_new'            => $this->is_new,
    138         );
    139         $data_format = array( '%d', '%d', '%d', '%s', '%s', '%s', '%d' );
    140 
    141         /**
    142          * Fires before the current notification item gets saved.
    143          *
    144          * Please use this hook to filter the properties above. Each part will be passed in.
    145          *
    146          * @since BuddyPress (2.0.0)
    147          *
    148          * @param BP_Notifications_Notification Current instance of the notification item being saved. Passed by reference.
    149          */
    150         do_action_ref_array( 'bp_notification_before_save', array( &$this ) );
    151 
    152         // Update
    153         if ( ! empty( $this->id ) ) {
    154             $result = self::_update( $data, array( 'ID' => $this->id ), $data_format, array( '%d' ) );
    155 
    156         // Insert
    157         } else {
    158             $result = self::_insert( $data, $data_format );
    159         }
    160 
    161         // Set the notification ID if successful
    162         if ( ! empty( $result ) && ! is_wp_error( $result ) ) {
    163             global $wpdb;
    164 
    165             $this->id = $wpdb->insert_id;
    166             $retval   = $wpdb->insert_id;
    167         }
    168 
    169         /**
    170          * Fires after the current notification item gets saved.
    171          *
    172          * @since BuddyPress (2.0.0)
    173          *
    174          * @param BP_Notifications_Notification Current instance of the notification item being saved. Passed by reference.
    175          */
    176         do_action_ref_array( 'bp_notification_after_save', array( &$this ) );
    177 
    178         // Return the result
    179         return $retval;
    180     }
    181 
    182     /**
    183      * Fetch data for an existing notification from the database.
    184      *
    185      * @since BuddyPress (1.9.0)
    186      *
    187      * @global BuddyPress $bp The one true BuddyPress instance.
    188      * @global wpdb $wpdb WordPress database object.
    189      */
    190     public function populate() {
    191         global $wpdb;
    192 
    193         $bp = buddypress();
    194 
    195         // Look for a notification
    196         $notification = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$bp->notifications->table_name} WHERE id = %d", $this->id ) );
    197 
    198         // Setup the notification data
    199         if ( ! empty( $notification ) && ! is_wp_error( $notification ) ) {
    200             $this->item_id           = $notification->item_id;
    201             $this->secondary_item_id = $notification->secondary_item_id;
    202             $this->user_id           = $notification->user_id;
    203             $this->component_name    = $notification->component_name;
    204             $this->component_action  = $notification->component_action;
    205             $this->date_notified     = $notification->date_notified;
    206             $this->is_new            = $notification->is_new;
    207         }
    208     }
    209 
    210     /** Protected Static Methods ******************************************/
    211 
    212     /**
    213      * Create a notification entry.
    214      *
    215      * @since BuddyPress (1.9.0)
    216      *
    217      * @param array $data {
    218      *     Array of notification data, passed to {@link wpdb::insert()}.
    219      *     @type int $user_id ID of the associated user.
    220      *     @type int $item_id ID of the associated item.
    221      *     @type int $secondary_item_id ID of the secondary associated item.
    222      *     @type string $component_name Name of the associated component.
    223      *     @type string $component_action Name of the associated component
    224      *           action.
    225      *     @type string $date_notified Timestamp of the notification.
    226      *     @type bool $is_new True if the notification is unread, otherwise
    227      *           false.
    228      * }
    229      * @param array $data_format See {@link wpdb::insert()}.
    230      * @return int|false The number of rows inserted, or false on error.
    231      */
    232     protected static function _insert( $data = array(), $data_format = array() ) {
    233         global $wpdb;
    234         return $wpdb->insert( buddypress()->notifications->table_name, $data, $data_format );
    235     }
    236 
    237     /**
    238      * Update notifications.
    239      *
    240      * @since BuddyPress (1.9.0)
    241      *
    242      * @see wpdb::update() for further description of paramater formats.
    243      *
    244      * @param array $data Array of notification data to update, passed to
    245      *        {@link wpdb::update()}. Accepts any property of a
    246      *        BP_Notification_Notification object.
    247      * @param array $where The WHERE params as passed to wpdb::update().
    248      *        Typically consists of array( 'ID' => $id ) to specify the ID
    249      *        of the item being updated. See {@link wpdb::update()}.
    250      * @param array $data_format See {@link wpdb::insert()}.
    251      * @param array $where_format See {@link wpdb::insert()}.
    252      * @return int|false The number of rows updated, or false on error.
    253      */
    254     protected static function _update( $data = array(), $where = array(), $data_format = array(), $where_format = array() ) {
    255         global $wpdb;
    256         return $wpdb->update( buddypress()->notifications->table_name, $data, $where, $data_format, $where_format );
    257     }
    258 
    259     /**
    260      * Delete notifications.
    261      *
    262      * @since BuddyPress (1.9.0)
    263      *
    264      * @see wpdb::update() for further description of paramater formats.
    265      *
    266      * @param array $where Array of WHERE clauses to filter by, passed to
    267      *        {@link wpdb::delete()}. Accepts any property of a
    268      *        BP_Notification_Notification object.
    269      * @param array $where_format See {@link wpdb::insert()}.
    270      * @return int|false The number of rows updated, or false on error.
    271      */
    272     protected static function _delete( $where = array(), $where_format = array() ) {
    273         global $wpdb;
    274         return $wpdb->delete( buddypress()->notifications->table_name, $where, $where_format );
    275     }
    276 
    277     /**
    278      * Assemble the WHERE clause of a get() SQL statement.
    279      *
    280      * Used by BP_Notifications_Notification::get() to create its WHERE
    281      * clause.
    282      *
    283      * @since BuddyPress (1.9.0)
    284      *
    285      * @param array $args See {@link BP_Notifications_Notification::get()}
    286      *        for more details.
    287      * @return string WHERE clause.
    288      */
    289     protected static function get_where_sql( $args = array() ) {
    290         global $wpdb;
    291 
    292         $where_conditions = array();
    293         $where            = '';
    294 
    295         // id
    296         if ( ! empty( $args['id'] ) ) {
    297             $id_in = implode( ',', wp_parse_id_list( $args['id'] ) );
    298             $where_conditions['id'] = "id IN ({$id_in})";
    299         }
    300 
    301         // user_id
    302         if ( ! empty( $args['user_id'] ) ) {
    303             $user_id_in = implode( ',', wp_parse_id_list( $args['user_id'] ) );
    304             $where_conditions['user_id'] = "user_id IN ({$user_id_in})";
    305         }
    306 
    307         // item_id
    308         if ( ! empty( $args['item_id'] ) ) {
    309             $item_id_in = implode( ',', wp_parse_id_list( $args['item_id'] ) );
    310             $where_conditions['item_id'] = "item_id IN ({$item_id_in})";
    311         }
    312 
    313         // secondary_item_id
    314         if ( ! empty( $args['secondary_item_id'] ) ) {
    315             $secondary_item_id_in = implode( ',', wp_parse_id_list( $args['secondary_item_id'] ) );
    316             $where_conditions['secondary_item_id'] = "secondary_item_id IN ({$secondary_item_id_in})";
    317         }
    318 
    319         // component_name
    320         if ( ! empty( $args['component_name'] ) ) {
    321             if ( ! is_array( $args['component_name'] ) ) {
    322                 $component_names = explode( ',', $args['component_name'] );
    323             } else {
    324                 $component_names = $args['component_name'];
    325             }
    326 
    327             $cn_clean = array();
    328             foreach ( $component_names as $cn ) {
    329                 $cn_clean[] = $wpdb->prepare( '%s', $cn );
    330             }
    331 
    332             $cn_in = implode( ',', $cn_clean );
    333             $where_conditions['component_name'] = "component_name IN ({$cn_in})";
    334         }
    335 
    336         // component_action
    337         if ( ! empty( $args['component_action'] ) ) {
    338             if ( ! is_array( $args['component_action'] ) ) {
    339                 $component_actions = explode( ',', $args['component_action'] );
    340             } else {
    341                 $component_actions = $args['component_action'];
    342             }
    343 
    344             $ca_clean = array();
    345             foreach ( $component_actions as $ca ) {
    346                 $ca_clean[] = $wpdb->prepare( '%s', $ca );
    347             }
    348 
    349             $ca_in = implode( ',', $ca_clean );
    350             $where_conditions['component_action'] = "component_action IN ({$ca_in})";
    351         }
    352 
    353         // is_new
    354         if ( ! empty( $args['is_new'] ) && 'both' !== $args['is_new'] ) {
    355             $where_conditions['is_new'] = "is_new = 1";
    356         } elseif ( isset( $args['is_new'] ) && ( 0 === $args['is_new'] || false === $args['is_new'] ) ) {
    357             $where_conditions['is_new'] = "is_new = 0";
    358         }
    359 
    360         // search_terms
    361         if ( ! empty( $args['search_terms'] ) ) {
    362             $search_terms_like = '%' . bp_esc_like( $args['search_terms'] ) . '%';
    363             $where_conditions['search_terms'] = $wpdb->prepare( "( component_name LIKE %s OR component_action LIKE %s )", $search_terms_like, $search_terms_like );
    364         }
    365 
    366         // Custom WHERE
    367         if ( ! empty( $where_conditions ) ) {
    368             $where = 'WHERE ' . implode( ' AND ', $where_conditions );
    369         }
    370 
    371         return $where;
    372     }
    373 
    374     /**
    375      * Assemble the ORDER BY clause of a get() SQL statement.
    376      *
    377      * Used by BP_Notifications_Notification::get() to create its ORDER BY
    378      * clause.
    379      *
    380      * @since BuddyPress (1.9.0)
    381      *
    382      * @param array $args See {@link BP_Notifications_Notification::get()}
    383      *        for more details.
    384      * @return string ORDER BY clause.
    385      */
    386     protected static function get_order_by_sql( $args = array() ) {
    387 
    388         // Setup local variable
    389         $conditions = array();
    390         $retval     = '';
    391 
    392         // Order by
    393         if ( ! empty( $args['order_by'] ) ) {
    394             $order_by               = implode( ', ', (array) $args['order_by'] );
    395             $conditions['order_by'] = "{$order_by}";
    396         }
    397 
    398         // Sort order direction
    399         if ( ! empty( $args['sort_order'] ) && in_array( $args['sort_order'], array( 'ASC', 'DESC' ) ) ) {
    400             $sort_order               = $args['sort_order'];
    401             $conditions['sort_order'] = "{$sort_order}";
    402         }
    403 
    404         // Custom ORDER BY
    405         if ( ! empty( $conditions ) ) {
    406             $retval = 'ORDER BY ' . implode( ' ', $conditions );
    407         }
    408 
    409         return $retval;
    410     }
    411 
    412     /**
    413      * Assemble the LIMIT clause of a get() SQL statement.
    414      *
    415      * Used by BP_Notifications_Notification::get() to create its LIMIT clause.
    416      *
    417      * @since BuddyPress (1.9.0)
    418      *
    419      * @param array $args See {@link BP_Notifications_Notification::get()}
    420      *        for more details.
    421      * @return string LIMIT clause.
    422      */
    423     protected static function get_paged_sql( $args = array() ) {
    424         global $wpdb;
    425 
    426         // Setup local variable
    427         $retval = '';
    428 
    429         // Custom LIMIT
    430         if ( ! empty( $args['page'] ) && ! empty( $args['per_page'] ) ) {
    431             $page     = absint( $args['page']     );
    432             $per_page = absint( $args['per_page'] );
    433             $offset   = $per_page * ( $page - 1 );
    434             $retval   = $wpdb->prepare( "LIMIT %d, %d", $offset, $per_page );
    435         }
    436 
    437         return $retval;
    438     }
    439 
    440     /**
    441      * Assemble query clauses, based on arguments, to pass to $wpdb methods.
    442      *
    443      * The insert(), update(), and delete() methods of {@link wpdb} expect
    444      * arguments of the following forms:
    445      *
    446      * - associative arrays whose key/value pairs are column => value, to
    447      *   be used in WHERE, SET, or VALUES clauses
    448      * - arrays of "formats", which tell $wpdb->prepare() which type of
    449      *   value to expect when sanitizing (eg, array( '%s', '%d' ))
    450      *
    451      * This utility method can be used to assemble both kinds of params,
    452      * out of a single set of associative array arguments, such as:
    453      *
    454      *     $args = array(
    455      *         'user_id' => 4,
    456      *         'component_name' => 'groups',
    457      *     );
    458      *
    459      * This will be converted to:
    460      *
    461      *     array(
    462      *         'data' => array(
    463      *             'user_id' => 4,
    464      *             'component_name' => 'groups',
    465      *         ),
    466      *         'format' => array(
    467      *             '%d',
    468      *             '%s',
    469      *         ),
    470      *     )
    471      *
    472      * which can easily be passed as arguments to the $wpdb methods.
    473      *
    474      * @since BuddyPress (1.9.0)
    475      *
    476      * @param $args Associative array of filter arguments.
    477      *        See {@BP_Notifications_Notification::get()} for a breakdown.
    478      * @return array Associative array of 'data' and 'format' args.
    479      */
    480     protected static function get_query_clauses( $args = array() ) {
    481         $where_clauses = array(
    482             'data'   => array(),
    483             'format' => array(),
    484         );
    485 
    486         // id
    487         if ( ! empty( $args['id'] ) ) {
    488             $where_clauses['data']['id'] = absint( $args['id'] );
    489             $where_clauses['format'][] = '%d';
    490         }
    491 
    492         // user_id
    493         if ( ! empty( $args['user_id'] ) ) {
    494             $where_clauses['data']['user_id'] = absint( $args['user_id'] );
    495             $where_clauses['format'][] = '%d';
    496         }
    497 
    498         // item_id
    499         if ( ! empty( $args['item_id'] ) ) {
    500             $where_clauses['data']['item_id'] = absint( $args['item_id'] );
    501             $where_clauses['format'][] = '%d';
    502         }
    503 
    504         // secondary_item_id
    505         if ( ! empty( $args['secondary_item_id'] ) ) {
    506             $where_clauses['data']['secondary_item_id'] = absint( $args['secondary_item_id'] );
    507             $where_clauses['format'][] = '%d';
    508         }
    509 
    510         // component_name
    511         if ( ! empty( $args['component_name'] ) ) {
    512             $where_clauses['data']['component_name'] = $args['component_name'];
    513             $where_clauses['format'][] = '%s';
    514         }
    515 
    516         // component_action
    517         if ( ! empty( $args['component_action'] ) ) {
    518             $where_clauses['data']['component_action'] = $args['component_action'];
    519             $where_clauses['format'][] = '%s';
    520         }
    521 
    522         // is_new
    523         if ( isset( $args['is_new'] ) ) {
    524             $where_clauses['data']['is_new'] = ! empty( $args['is_new'] ) ? 1 : 0;
    525             $where_clauses['format'][] = '%d';
    526         }
    527 
    528         return $where_clauses;
    529     }
    530 
    531     /** Public Static Methods *********************************************/
    532 
    533     /**
    534      * Check that a specific notification is for a specific user.
    535      *
    536      * @since BuddyPress (1.9.0)
    537      *
    538      * @param int $user_id ID of the user being checked.
    539      * @param int $notification_id ID of the notification being checked.
    540      * @return bool True if the notification belongs to the user, otherwise
    541      *         false.
    542      */
    543     public static function check_access( $user_id, $notification_id ) {
    544         global $wpdb;
    545 
    546         $bp = buddypress();
    547 
    548         return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM {$bp->core->table_name_notifications} WHERE id = %d AND user_id = %d", $notification_id, $user_id ) );
    549     }
    550 
    551     /**
    552      * Get notifications, based on provided filter parameters.
    553      *
    554      * @since BuddyPress (1.9.0)
    555      *
    556      * @param array $args {
    557      *     Associative array of arguments. All arguments but $page and
    558      *     $per_page can be treated as filter values for get_where_sql()
    559      *     and get_query_clauses(). All items are optional.
    560      *     @type int|array $id ID of notification being updated. Can be an
    561      *           array of IDs.
    562      *     @type int|array $user_id ID of user being queried. Can be an
    563      *           array of user IDs.
    564      *     @type int|array $item_id ID of associated item. Can be an array
    565      *           of multiple item IDs.
    566      *     @type int|array $secondary_item_id ID of secondary associated
    567      *           item. Can be an array of multiple IDs.
    568      *     @type string|array $component_name Name of the component to
    569      *           filter by. Can be an array of component names.
    570      *     @type string|array $component_action Name of the action to
    571      *           filter by. Can be an array of actions.
    572      *     @type bool $is_new Whether to limit to new notifications. True
    573      *           returns only new notifications, false returns only non-new
    574      *           notifications. 'both' returns all. Default: true.
    575      *     @type string $search_terms Term to match against component_name
    576      *           or component_action fields.
    577      *     @type string $order_by Database column to order notifications by.
    578      *     @type string $sort_order Either 'ASC' or 'DESC'.
    579      *     @type string $order_by Field to order results by.
    580      *     @type string $sort_order ASC or DESC.
    581      *     @type int $page Number of the current page of results. Default:
    582      *           false (no pagination - all items).
    583      *     @type int $per_page Number of items to show per page. Default:
    584      *           false (no pagination - all items).
    585      * }
    586      * @return array Located notifications.
    587      */
    588     public static function get( $args = array() ) {
    589         global $wpdb;
    590 
    591         // Parse the arguments
    592         $r  = wp_parse_args( $args, array(
    593             'id'                => false,
    594             'user_id'           => false,
    595             'item_id'           => false,
    596             'secondary_item_id' => false,
    597             'component_name'    => bp_notifications_get_registered_components(),
    598             'component_action'  => false,
    599             'is_new'            => true,
    600             'search_terms'      => '',
    601             'order_by'          => false,
    602             'sort_order'        => false,
    603             'page'              => false,
    604             'per_page'          => false,
    605         ) );
    606 
    607         // SELECT
    608         $select_sql = "SELECT *";
    609 
    610         // FROM
    611         $from_sql   = "FROM " . buddypress()->notifications->table_name;
    612 
    613         // WHERE
    614         $where_sql  = self::get_where_sql( array(
    615             'id'                => $r['id'],
    616             'user_id'           => $r['user_id'],
    617             'item_id'           => $r['item_id'],
    618             'secondary_item_id' => $r['secondary_item_id'],
    619             'component_name'    => $r['component_name'],
    620             'component_action'  => $r['component_action'],
    621             'is_new'            => $r['is_new'],
    622             'search_terms'      => $r['search_terms'],
    623         ) );
    624 
    625         // ORDER BY
    626         $order_sql  = self::get_order_by_sql( array(
    627             'order_by'   => $r['order_by'],
    628             'sort_order' => $r['sort_order']
    629         ) );
    630 
    631         // LIMIT %d, %d
    632         $pag_sql    = self::get_paged_sql( array(
    633             'page'     => $r['page'],
    634             'per_page' => $r['per_page'],
    635         ) );
    636 
    637         $sql = "{$select_sql} {$from_sql} {$where_sql} {$order_sql} {$pag_sql}";
    638 
    639         return $wpdb->get_results( $sql );
    640     }
    641 
    642     /**
    643      * Get a count of total notifications matching a set of arguments.
    644      *
    645      * @since BuddyPress (1.9.0)
    646      *
    647      * @see BP_Notifications_Notification::get() for a description of
    648      *      arguments.
    649      *
    650      * @param array $args See {@link BP_Notifications_Notification::get()}.
    651      * @return int Count of located items.
    652      */
    653     public static function get_total_count( $args ) {
    654         global $wpdb;
    655 
    656         /**
    657          * Default component_name to active_components
    658          *
    659          * @see http://buddypress.trac.wordpress.org/ticket/5300
    660          */
    661         $args = wp_parse_args( $args, array(
    662             'component_name' => bp_notifications_get_registered_components()
    663         ) );
    664 
    665         // Load BuddyPress
    666         $bp = buddypress();
    667 
    668         // Build the query
    669         $select_sql = "SELECT COUNT(*)";
    670         $from_sql   = "FROM {$bp->notifications->table_name}";
    671         $where_sql  = self::get_where_sql( $args );
    672         $sql        = "{$select_sql} {$from_sql} {$where_sql}";
    673 
    674         // Return the queried results
    675         return $wpdb->get_var( $sql );
    676     }
    677 
    678     /**
    679      * Update notifications.
    680      *
    681      * @since BuddyPress (1.9.0)
    682      *
    683      * @see BP_Notifications_Notification::get() for a description of
    684      *      accepted update/where arguments.
    685      *
    686      * @param array $update_args Associative array of fields to update,
    687      *        and the values to update them to. Of the format
    688      *            array( 'user_id' => 4, 'component_name' => 'groups', )
    689      * @param array $where_args Associative array of columns/values, to
    690      *        determine which rows should be updated. Of the format
    691      *            array( 'item_id' => 7, 'component_action' => 'members', )
    692      * @return int|bool Number of rows updated on success, false on failure.
    693      */
    694     public static function update( $update_args = array(), $where_args = array() ) {
    695         $update = self::get_query_clauses( $update_args );
    696         $where  = self::get_query_clauses( $where_args  );
    697 
    698         // make sure we delete the notification cache for the user on update
    699         if ( ! empty( $where_args['user_id'] ) ) {
    700             wp_cache_delete( 'all_for_user_' . $where_args['user_id'], 'bp_notifications' );
    701         }
    702 
    703         return self::_update( $update['data'], $where['data'], $update['format'], $where['format'] );
    704     }
    705 
    706     /**
    707      * Delete notifications.
    708      *
    709      * @since BuddyPress (1.9.0)
    710      *
    711      * @see BP_Notifications_Notification::get() for a description of
    712      *      accepted update/where arguments.
    713      *
    714      * @param array $args Associative array of columns/values, to determine
    715      *        which rows should be deleted.  Of the format
    716      *            array( 'item_id' => 7, 'component_action' => 'members', )
    717      * @return int|bool Number of rows deleted on success, false on failure.
    718      */
    719     public static function delete( $args = array() ) {
    720         $where = self::get_query_clauses( $args );
    721 
    722         /**
    723          * Fires before the deletion of a notification item.
    724          *
    725          * @since BuddyPress (2.0.0)
    726          *
    727          * @param array $args Associative array of columns/values, to determine
    728          *                    which rows should be deleted. Of the format
    729          *                    array( 'item_id' => 7, 'component_action' => 'members' ).
    730          */
    731         do_action( 'bp_notification_before_delete', $args );
    732 
    733         return self::_delete( $where['data'], $where['format'] );
    734     }
    735 
    736     /** Convenience methods ***********************************************/
    737 
    738     /**
    739      * Delete a single notification by ID.
    740      *
    741      * @since BuddyPress (1.9.0)
    742      *
    743      * @see BP_Notifications_Notification::delete() for explanation of
    744      *      return value.
    745      *
    746      * @param int $id ID of the notification item to be deleted.
    747      * @return bool True on success, false on failure.
    748      */
    749     public static function delete_by_id( $id ) {
    750         return self::delete( array(
    751             'id' => $id,
    752         ) );
    753     }
    754 
    755     /**
    756      * Fetch all the notifications in the database for a specific user.
    757      *
    758      * @since BuddyPress (1.9.0)
    759      *
    760      * @param int $user_id ID of the user whose notifications are being
    761      *        fetched.
    762      * @param string $status Optional. Status of notifications to fetch.
    763      *        'is_new' to get only unread items, 'all' to get all.
    764      * @return array Associative array of notification items.
    765      */
    766     public static function get_all_for_user( $user_id, $status = 'is_new' ) {
    767         return self::get( array(
    768             'user_id' => $user_id,
    769             'is_new'  => 'is_new' === $status,
    770         ) );
    771     }
    772 
    773     /**
    774      * Fetch all the unread notifications in the database for a specific user.
    775      *
    776      * @since BuddyPress (1.9.0)
    777      *
    778      * @param int $user_id ID of the user whose notifications are being
    779      *        fetched.
    780      * @return array Associative array of unread notification items.
    781      */
    782     public static function get_unread_for_user( $user_id = 0 ) {
    783         return self::get( array(
    784             'user_id' => $user_id,
    785             'is_new'  => true,
    786         ) );
    787     }
    788 
    789     /**
    790      * Fetch all the read notifications in the database for a specific user.
    791      *
    792      * @since BuddyPress (1.9.0)
    793      *
    794      * @param int $user_id ID of the user whose notifications are being
    795      *        fetched.
    796      * @return array Associative array of unread notification items.
    797      */
    798     public static function get_read_for_user( $user_id = 0 ) {
    799         return self::get( array(
    800             'user_id' => $user_id,
    801             'is_new'  => false,
    802         ) );
    803     }
    804 
    805     /**
    806      * Get unread notifications for a user, in a pagination-friendly format.
    807      *
    808      * @since BuddyPress (1.9.0)
    809      *
    810      * @param array $args {
    811      *     Array of arguments.
    812      *     @type int $user_id ID of the user for whom the notifications are
    813      *           being fetched. Default: logged-in user ID.
    814      *     @type bool $is_new Whether to limit the query to unread
    815      *           notifications. Default: true.
    816      *     @type int $page Number of the page to return. Default: 1.
    817      *     @type int $per_page Number of results to display per page.
    818      *           Default: 10.
    819      *     @type string $search_terms Optional. A term to search against in
    820      *           the 'component_name' and 'component_action' columns.
    821      * }
    822      * @return array {
    823      *     @type array $notifications Array of notification results.
    824      *     @type int $total Count of all located notifications matching
    825      *           the query params.
    826      * }
    827      */
    828     public static function get_current_notifications_for_user( $args = array() ) {
    829         $r = wp_parse_args( $args, array(
    830             'user_id'      => bp_loggedin_user_id(),
    831             'is_new'       => true,
    832             'page'         => 1,
    833             'per_page'     => 25,
    834             'search_terms' => '',
    835         ) );
    836 
    837         $notifications = self::get( $r );
    838 
    839         // Bail if no notifications
    840         if ( empty( $notifications ) ) {
    841             return false;
    842         }
    843 
    844         $total_count = self::get_total_count( $r );
    845 
    846         return array( 'notifications' => &$notifications, 'total' => $total_count );
    847     }
    848 
    849     /** Mark **************************************************************/
    850 
    851     /**
    852      * Mark all user notifications as read.
    853      *
    854      * @since BuddyPress (1.9.0)
    855      *
    856      * @param int $user_id The ID of the user who the notifications
    857      *        are for.
    858      * @param int $is_new Mark as read (1) or unread (0).
    859      */
    860     public static function mark_all_for_user( $user_id, $is_new = 0, $item_id = 0, $component_name = '', $component_action = '', $secondary_item_id = 0 ) {
    861 
    862         // Values to be updated
    863         $update_args = array(
    864             'is_new' => $is_new,
    865         );
    866 
    867         // WHERE clauses
    868         $where_args = array(
    869             'user_id' => $user_id,
    870         );
    871 
    872         if ( ! empty( $item_id ) ) {
    873             $where_args['item_id'] = $item_id;
    874         }
    875 
    876         if ( ! empty( $component_name ) ) {
    877             $where_args['component_name'] = $component_name;
    878         }
    879 
    880         if ( ! empty( $component_action ) ) {
    881             $where_args['component_action'] = $component_action;
    882         }
    883 
    884         if ( ! empty( $secondary_item_id ) ) {
    885             $where_args['secondary_item_id'] = $secondary_item_id;
    886         }
    887 
    888         return self::update( $update_args, $where_args );
    889     }
    890 
    891     /**
    892      * Mark all notifications from a user as read.
    893      *
    894      * @since BuddyPress (1.9.0)
    895      *
    896      * @param int $user_id The ID of the user who the notifications are from.
    897      * @param int $is_new Mark as read (1) or unread (0).
    898      */
    899     public static function mark_all_from_user( $user_id, $is_new = 0, $component_name = '', $component_action = '', $secondary_item_id = 0 ) {
    900 
    901         // Values to be updated
    902         $update_args = array(
    903             'is_new' => $is_new,
    904         );
    905 
    906         // WHERE clauses
    907         $where_args = array(
    908             'item_id' => $user_id,
    909         );
    910 
    911         if ( ! empty( $component_name ) ) {
    912             $where_args['component_name'] = $component_name;
    913         }
    914 
    915         if ( ! empty( $component_action ) ) {
    916             $where_args['component_action'] = $component_action;
    917         }
    918 
    919         if ( ! empty( $secondary_item_id ) ) {
    920             $where_args['secondary_item_id'] = $secondary_item_id;
    921         }
    922 
    923         return self::update( $update_args, $where_args );
    924     }
    925 
    926     /**
    927      * Mark all notifications for all users as read by item id, and optional
    928      * secondary item id, and component name and action.
    929      *
    930      * @since BuddyPress (1.9.0)
    931      *
    932      * @param int $item_id The ID of the item associated with the
    933      *        notifications.
    934      * @param string $component_name The component that the notifications
    935      *        are associated with.
    936      * @param string $component_action The action that the notifications
    937      *        are associated with.
    938      * @param string $secondary_item_id Optional. ID of the secondary
    939      *        associated item.
    940      */
    941     public static function mark_all_by_type( $item_id, $is_new = 0, $component_name = '', $component_action = '', $secondary_item_id = 0 ) {
    942 
    943         // Values to be updated
    944         $update_args = array(
    945             'is_new' => $is_new,
    946         );
    947 
    948         // WHERE clauses
    949         $where_args = array(
    950             'item_id' => $item_id,
    951         );
    952 
    953         if ( ! empty( $component_name ) ) {
    954             $where_args['component_name'] = $component_name;
    955         }
    956 
    957         if ( ! empty( $component_action ) ) {
    958             $where_args['component_action'] = $component_action;
    959         }
    960 
    961         if ( ! empty( $secondary_item_id ) ) {
    962             $where_args['secondary_item_id'] = $secondary_item_id;
    963         }
    964 
    965         return self::update( $update_args, $where_args );
    966     }
    967 }
     16require __DIR__ . '/classes/class-bp-notifications-notification.php';
  • trunk/src/bp-xprofile/bp-xprofile-classes.php

    r9471 r9485  
    11<?php
    2 
    32/**
    43 * BuddyPress XProfile Classes
     
    1110defined( 'ABSPATH' ) || exit;
    1211
    13 class BP_XProfile_Group {
    14     public $id = null;
    15     public $name;
    16     public $description;
    17     public $can_delete;
    18     public $group_order;
    19     public $fields;
    20 
    21     public function __construct( $id = null ) {
    22         if ( !empty( $id ) )
    23             $this->populate( $id );
    24     }
    25 
    26     public function populate( $id ) {
    27         global $wpdb;
    28 
    29         $group = wp_cache_get( 'xprofile_group_' . $this->id, 'bp' );
    30 
    31         if ( false === $group ) {
    32             $bp = buddypress();
    33             $group = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$bp->profile->table_name_groups} WHERE id = %d", $id ) );
    34         }
    35 
    36         if ( empty( $group ) ) {
    37             return false;
    38         }
    39 
    40         $this->id          = $group->id;
    41         $this->name        = stripslashes( $group->name );
    42         $this->description = stripslashes( $group->description );
    43         $this->can_delete  = $group->can_delete;
    44         $this->group_order = $group->group_order;
    45     }
    46 
    47     public function save() {
    48         global $wpdb;
    49 
    50         $this->name        = apply_filters( 'xprofile_group_name_before_save',        $this->name,        $this->id );
    51         $this->description = apply_filters( 'xprofile_group_description_before_save', $this->description, $this->id );
    52 
    53         /**
    54          * Fires before the current group instance gets saved.
    55          *
    56          * Please use this hook to filter the properties above. Each part will be passed in.
    57          *
    58          * @since BuddyPress (1.0.0)
    59          *
    60          * @param BP_XProfile_Group Current instance of the group being saved. Passed by reference.
    61          */
    62         do_action_ref_array( 'xprofile_group_before_save', array( &$this ) );
    63 
    64         $bp = buddypress();
    65 
    66         if ( $this->id )
    67             $sql = $wpdb->prepare( "UPDATE {$bp->profile->table_name_groups} SET name = %s, description = %s WHERE id = %d", $this->name, $this->description, $this->id );
    68         else
    69             $sql = $wpdb->prepare( "INSERT INTO {$bp->profile->table_name_groups} (name, description, can_delete) VALUES (%s, %s, 1)", $this->name, $this->description );
    70 
    71         if ( is_wp_error( $wpdb->query( $sql ) ) )
    72             return false;
    73 
    74         // If not set, update the ID in the group object
    75         if ( ! $this->id )
    76             $this->id = $wpdb->insert_id;
    77 
    78         /**
    79          * Fires after the current group instance gets saved.
    80          *
    81          * @since BuddyPress (1.0.0)
    82          *
    83          * @param BP_XProfile_Group Current instance of the group being saved. Passed by reference.
    84          */
    85         do_action_ref_array( 'xprofile_group_after_save', array( &$this ) );
    86 
    87         return $this->id;
    88     }
    89 
    90     public function delete() {
    91         global $wpdb;
    92 
    93         if ( empty( $this->can_delete ) )
    94             return false;
    95 
    96         /**
    97          * Fires before the current group instance gets deleted.
    98          *
    99          * @since BuddyPress (2.0.0)
    100          *
    101          * @param BP_XProfile_Group Current instance of the group being deleted. Passed by reference.
    102          */
    103         do_action_ref_array( 'xprofile_group_before_delete', array( &$this ) );
    104 
    105         $bp = buddypress();
    106 
    107         // Delete field group
    108         if ( !$wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->profile->table_name_groups} WHERE id = %d", $this->id ) ) ) {
    109             return false;
    110         } else {
    111 
    112             // Remove the group's fields.
    113             if ( BP_XProfile_Field::delete_for_group( $this->id ) ) {
    114 
    115                 // Remove profile data for the groups fields
    116                 for ( $i = 0, $count = count( $this->fields ); $i < $count; ++$i ) {
    117                     BP_XProfile_ProfileData::delete_for_field( $this->fields[$i]->id );
    118                 }
    119             }
    120 
    121             /**
    122              * Fires after the current group instance gets deleted.
    123              *
    124              * @since BuddyPress (2.0.0)
    125              *
    126              * @param BP_XProfile_Group Current instance of the group being deleted. Passed by reference.
    127              */
    128             do_action_ref_array( 'xprofile_group_after_delete', array( &$this ) );
    129 
    130             return true;
    131         }
    132     }
    133 
    134     /** Static Methods ********************************************************/
    135 
    136     /**
    137      * get()
    138      *
    139      * Populates the BP_XProfile_Group object with profile field groups, fields, and field data
    140      *
    141      * @package BuddyPress XProfile
    142      *
    143      * @global $wpdb WordPress DB access object.
    144      * @global BuddyPress $bp The one true BuddyPress instance
    145      *
    146      * @param array $args {
    147      *  Array of optional arguments:
    148      *  @type int $profile_group_id Limit results to a single profile
    149      *        group.
    150      *      @type int $user_id Required if you want to load a specific
    151      *            user's data. Default: displayed user's ID.
    152      *      @type bool $hide_empty_groups True to hide groups that don't
    153      *            have any fields. Default: false.
    154      *  @type bool $hide_empty_fields True to hide fields where the
    155      *        user has not provided data. Default: false.
    156      *      @type bool $fetch_fields Whether to fetch each group's fields.
    157      *            Default: false.
    158      *      @type bool $fetch_field_data Whether to fetch data for each
    159      *            field. Requires a $user_id. Default: false.
    160      *      @type array $exclude_groups Comma-separated list or array of
    161      *            group IDs to exclude.
    162      *      @type array $exclude_fields Comma-separated list or array of
    163      *            field IDs to exclude.
    164      *      @type bool $update_meta_cache Whether to pre-fetch xprofilemeta
    165      *            for all retrieved groups, fields, and data. Default: true.
    166      * }
    167      * @return array $groups
    168      */
    169     public static function get( $args = array() ) {
    170         global $wpdb;
    171 
    172         $defaults = array(
    173             'profile_group_id'       => false,
    174             'user_id'                => bp_displayed_user_id(),
    175             'hide_empty_groups'      => false,
    176             'hide_empty_fields'      => false,
    177             'fetch_fields'           => false,
    178             'fetch_field_data'       => false,
    179             'fetch_visibility_level' => false,
    180             'exclude_groups'         => false,
    181             'exclude_fields'         => false,
    182             'update_meta_cache'      => true,
    183         );
    184 
    185         $r = wp_parse_args( $args, $defaults );
    186         extract( $r, EXTR_SKIP );
    187 
    188         // Keep track of object IDs for cache-priming
    189         $object_ids = array(
    190             'group' => array(),
    191             'field' => array(),
    192             'data'  => array(),
    193         );
    194 
    195         $where_sql = '';
    196 
    197         if ( ! empty( $profile_group_id ) ) {
    198             $where_sql = $wpdb->prepare( 'WHERE g.id = %d', $profile_group_id );
    199         } elseif ( $exclude_groups ) {
    200             $exclude_groups = join( ',', wp_parse_id_list( $exclude_groups ) );
    201             $where_sql = "WHERE g.id NOT IN ({$exclude_groups})";
    202         }
    203 
    204         $bp = buddypress();
    205 
    206         if ( ! empty( $hide_empty_groups ) ) {
    207             $group_ids = $wpdb->get_col( "SELECT DISTINCT g.id FROM {$bp->profile->table_name_groups} g INNER JOIN {$bp->profile->table_name_fields} f ON g.id = f.group_id {$where_sql} ORDER BY g.group_order ASC" );
    208         } else {
    209             $group_ids = $wpdb->get_col( "SELECT DISTINCT g.id FROM {$bp->profile->table_name_groups} g {$where_sql} ORDER BY g.group_order ASC" );
    210         }
    211 
    212         $groups = self::get_group_data( $group_ids );
    213 
    214         if ( empty( $fetch_fields ) )
    215             return $groups;
    216 
    217         // Get the group ids
    218         $group_ids = array();
    219         foreach( (array) $groups as $group ) {
    220             $group_ids[] = $group->id;
    221         }
    222 
    223         // Store for meta cache priming
    224         $object_ids['group'] = $group_ids;
    225 
    226         $group_ids = implode( ',', (array) $group_ids );
    227 
    228         if ( empty( $group_ids ) )
    229             return $groups;
    230 
    231         // Support arrays and comma-separated strings
    232         $exclude_fields_cs = wp_parse_id_list( $exclude_fields );
    233 
    234         // Visibility - Handled here so as not to be overridden by sloppy use of the
    235         // exclude_fields parameter. See bp_xprofile_get_hidden_fields_for_user()
    236         $exclude_fields_cs = array_merge( $exclude_fields_cs, bp_xprofile_get_hidden_fields_for_user( $user_id ) );
    237         $exclude_fields_cs = implode( ',', $exclude_fields_cs );
    238 
    239         if ( !empty( $exclude_fields_cs ) ) {
    240             $exclude_fields_sql = "AND id NOT IN ({$exclude_fields_cs})";
    241         } else {
    242             $exclude_fields_sql = '';
    243         }
    244 
    245         // Fetch the fields
    246         $fields = $wpdb->get_results( "SELECT id, name, description, type, group_id, is_required FROM {$bp->profile->table_name_fields} WHERE group_id IN ( {$group_ids} ) AND parent_id = 0 {$exclude_fields_sql} ORDER BY field_order" );
    247 
    248         // Store field IDs for meta cache priming
    249         $object_ids['field'] = wp_list_pluck( $fields, 'id' );
    250 
    251         if ( empty( $fields ) )
    252             return $groups;
    253 
    254         // Maybe fetch field data
    255         if ( ! empty( $fetch_field_data ) ) {
    256 
    257             // Fetch the field data for the user.
    258             foreach( (array) $fields as $field ) {
    259                 $field_ids[] = $field->id;
    260             }
    261 
    262             $field_ids_sql = implode( ',', (array) $field_ids );
    263 
    264             if ( ! empty( $field_ids ) && ! empty( $user_id ) ) {
    265                 $field_data = BP_XProfile_ProfileData::get_data_for_user( $user_id, $field_ids );
    266             }
    267 
    268             // Remove data-less fields, if necessary
    269             if ( !empty( $hide_empty_fields ) && ! empty( $field_ids ) && ! empty( $field_data ) ) {
    270 
    271                 // Loop through the results and find the fields that have data.
    272                 foreach( (array) $field_data as $data ) {
    273 
    274                     // Empty fields may contain a serialized empty array
    275                     $maybe_value = maybe_unserialize( $data->value );
    276 
    277                     // Valid field values of 0 or '0' get caught by empty(), so we have an extra check for these. See #BP5731
    278                     if ( ( ! empty( $maybe_value ) || '0' == $maybe_value ) && false !== $key = array_search( $data->field_id, $field_ids ) ) {
    279 
    280                         // Fields that have data get removed from the list
    281                         unset( $field_ids[$key] );
    282                     }
    283                 }
    284 
    285                 // The remaining members of $field_ids are empty. Remove them.
    286                 foreach( $fields as $field_key => $field ) {
    287                     if ( in_array( $field->id, $field_ids ) ) {
    288                         unset( $fields[$field_key] );
    289                     }
    290                 }
    291 
    292                 // Reset indexes
    293                 $fields = array_values( $fields );
    294             }
    295 
    296             // Field data was found
    297             if ( ! empty( $fields ) && !empty( $field_data ) && !is_wp_error( $field_data ) ) {
    298 
    299                 // Loop through fields
    300                 foreach( (array) $fields as $field_key => $field ) {
    301 
    302                     // Loop through the data in each field
    303                     foreach( (array) $field_data as $data ) {
    304 
    305                         // Assign correct data value to the field
    306                         if ( $field->id == $data->field_id ) {
    307                             $fields[$field_key]->data        = new stdClass;
    308                             $fields[$field_key]->data->value = $data->value;
    309                             $fields[$field_key]->data->id    = $data->id;
    310                         }
    311 
    312                         // Store for meta cache priming
    313                         $object_ids['data'][] = $data->id;
    314                     }
    315                 }
    316             }
    317         }
    318 
    319         // Prime the meta cache, if necessary
    320         if ( $update_meta_cache ) {
    321             bp_xprofile_update_meta_cache( $object_ids );
    322         }
    323 
    324         // Maybe fetch visibility levels
    325         if ( !empty( $fetch_visibility_level ) ) {
    326             $fields = self::fetch_visibility_level( $user_id, $fields );
    327         }
    328 
    329         // Merge the field array back in with the group array
    330         foreach( (array) $groups as $group ) {
    331 
    332             // Indexes may have been shifted after previous deletions, so we get a
    333             // fresh one each time through the loop
    334             $index = array_search( $group, $groups );
    335 
    336             foreach( (array) $fields as $field ) {
    337                 if ( $group->id == $field->group_id ) {
    338                     $groups[$index]->fields[] = $field;
    339                 }
    340             }
    341 
    342             // When we unset fields above, we may have created empty groups.
    343             // Remove them, if necessary.
    344             if ( empty( $group->fields ) && $hide_empty_groups ) {
    345                 unset( $groups[$index] );
    346             }
    347 
    348             // Reset indexes
    349             $groups = array_values( $groups );
    350         }
    351 
    352         return $groups;
    353     }
    354 
    355     /**
    356      * Get data about a set of groups, based on IDs.
    357      *
    358      * @since BuddyPress (2.0.0)
    359      *
    360      * @param array $group_ids Array of IDs.
    361      * @return array
    362      */
    363     protected static function get_group_data( $group_ids ) {
    364         global $wpdb;
    365 
    366         // Bail if no group IDs are passed
    367         if ( empty( $group_ids ) ) {
    368             return array();
    369         }
    370 
    371         $groups        = array();
    372         $uncached_gids = array();
    373 
    374         foreach ( $group_ids as $group_id ) {
    375 
    376             // If cached data is found, use it
    377             if ( $group_data = wp_cache_get( 'xprofile_group_' . $group_id, 'bp' ) ) {
    378                 $groups[ $group_id ] = $group_data;
    379 
    380             // Otherwise leave a placeholder so we don't lose the order
    381             } else {
    382                 $groups[ $group_id ] = '';
    383 
    384                 // Add to the list of items to be queried
    385                 $uncached_gids[] = $group_id;
    386             }
    387         }
    388 
    389         // Fetch uncached data from the DB if necessary
    390         if ( ! empty( $uncached_gids ) ) {
    391             $uncached_gids_sql = implode( ',', wp_parse_id_list( $uncached_gids ) );
    392 
    393             $bp = buddypress();
    394 
    395             // Fetch data, preserving order
    396             $queried_gdata = $wpdb->get_results( "SELECT * FROM {$bp->profile->table_name_groups} WHERE id IN ({$uncached_gids_sql}) ORDER BY FIELD( id, {$uncached_gids_sql} )");
    397 
    398             // Put queried data into the placeholders created earlier,
    399             // and add it to the cache
    400             foreach ( (array) $queried_gdata as $gdata ) {
    401                 $groups[ $gdata->id ] = $gdata;
    402                 wp_cache_set( 'xprofile_group_' . $gdata->id, $gdata, 'bp' );
    403             }
    404         }
    405 
    406         // Reset indexes
    407         $groups = array_values( $groups );
    408 
    409         return $groups;
    410     }
    411 
    412     public static function admin_validate() {
    413         global $message;
    414 
    415         /* Validate Form */
    416         if ( empty( $_POST['group_name'] ) ) {
    417             $message = __( 'Please make sure you give the group a name.', 'buddypress' );
    418             return false;
    419         } else {
    420             return true;
    421         }
    422     }
    423 
    424     public static function update_position( $field_group_id, $position ) {
    425         global $wpdb;
    426 
    427         if ( !is_numeric( $position ) ) {
    428             return false;
    429         }
    430 
    431         // purge profile field group cache
    432         wp_cache_delete( 'xprofile_groups_inc_empty', 'bp' );
    433 
    434         $bp = buddypress();
    435 
    436         return $wpdb->query( $wpdb->prepare( "UPDATE {$bp->profile->table_name_groups} SET group_order = %d WHERE id = %d", $position, $field_group_id ) );
    437     }
    438 
    439     /**
    440      * Fetch the field visibility level for the fields returned by the query
    441      *
    442      * @since BuddyPress (1.6.0)
    443      *
    444      * @param int $user_id The profile owner's user_id
    445      * @param array $fields The database results returned by the get() query
    446      * @return array $fields The database results, with field_visibility added
    447      */
    448     public static function fetch_visibility_level( $user_id = 0, $fields = array() ) {
    449 
    450         // Get the user's visibility level preferences
    451         $visibility_levels = bp_get_user_meta( $user_id, 'bp_xprofile_visibility_levels', true );
    452 
    453         foreach( (array) $fields as $key => $field ) {
    454 
    455             // Does the admin allow this field to be customized?
    456             $allow_custom = 'disabled' !== bp_xprofile_get_meta( $field->id, 'field', 'allow_custom_visibility' );
    457 
    458             // Look to see if the user has set the visibility for this field
    459             if ( $allow_custom && isset( $visibility_levels[$field->id] ) ) {
    460                 $field_visibility = $visibility_levels[$field->id];
    461 
    462             // If no admin-set default is saved, fall back on a global default
    463             } else {
    464                 $fallback_visibility = bp_xprofile_get_meta( $field->id, 'field', 'default_visibility' );
    465 
    466                 /**
    467                  * Filters the XProfile default visibility level for a field.
    468                  *
    469                  * @since BuddyPress (1.6.0)
    470                  *
    471                  * @param string $value Default visibility value.
    472                  */
    473                 $field_visibility = ! empty( $fallback_visibility ) ? $fallback_visibility : apply_filters( 'bp_xprofile_default_visibility_level', 'public' );
    474             }
    475 
    476             $fields[$key]->visibility_level = $field_visibility;
    477         }
    478 
    479         return $fields;
    480     }
    481 
    482     /**
    483      * Fetch the admin-set preferences for all fields.
    484      *
    485      * @since BuddyPress (1.6.0)
    486      *
    487      * @return array $default_visibility_levels An array, keyed by
    488      *         field_id, of default visibility level + allow_custom
    489      *         (whether the admin allows this field to be set by user)
    490      */
    491     public static function fetch_default_visibility_levels() {
    492         global $wpdb;
    493 
    494         $default_visibility_levels = wp_cache_get( 'xprofile_default_visibility_levels', 'bp' );
    495 
    496         if ( false === $default_visibility_levels ) {
    497             $bp = buddypress();
    498 
    499             $levels = $wpdb->get_results( "SELECT object_id, meta_key, meta_value FROM {$bp->profile->table_name_meta} WHERE object_type = 'field' AND ( meta_key = 'default_visibility' OR meta_key = 'allow_custom_visibility' )" );
    500 
    501             // Arrange so that the field id is the key and the visibility level the value
    502             $default_visibility_levels = array();
    503             foreach ( $levels as $level ) {
    504                 if ( 'default_visibility' == $level->meta_key ) {
    505                     $default_visibility_levels[ $level->object_id ]['default'] = $level->meta_value;
    506                 } elseif ( 'allow_custom_visibility' == $level->meta_key ) {
    507                     $default_visibility_levels[ $level->object_id ]['allow_custom'] = $level->meta_value;
    508                 }
    509             }
    510 
    511             wp_cache_set( 'xprofile_default_visibility_levels', $default_visibility_levels, 'bp' );
    512         }
    513 
    514         return $default_visibility_levels;
    515     }
    516 
    517     public function render_admin_form() {
    518         global $message;
    519 
    520         if ( empty( $this->id ) ) {
    521             $title  = __( 'Add New Field Group', 'buddypress' );
    522             $action = "users.php?page=bp-profile-setup&amp;mode=add_group";
    523             $button = __( 'Create Field Group', 'buddypress' );
    524         } else {
    525             $title  = __( 'Edit Field Group', 'buddypress' );
    526             $action = "users.php?page=bp-profile-setup&amp;mode=edit_group&amp;group_id=" . $this->id;
    527             $button = __( 'Save Changes', 'buddypress' );
    528         } ?>
    529 
    530         <div class="wrap">
    531 
    532             <?php screen_icon( 'users' ); ?>
    533 
    534             <h2><?php echo esc_html( $title ); ?></h2>
    535 
    536             <?php if ( !empty( $message ) ) :
    537                     $type = ( 'error' == $type ) ? 'error' : 'updated'; ?>
    538 
    539                 <div id="message" class="<?php echo esc_attr( $type ); ?> fade">
    540                     <p><?php echo esc_html( $message ); ?></p>
    541                 </div>
    542 
    543             <?php endif; ?>
    544 
    545             <form id="bp-xprofile-add-field-group" action="<?php echo esc_url( $action ); ?>" method="post">
    546                 <div id="poststuff">
    547                     <div id="post-body" class="metabox-holder columns-2">
    548                         <div id="post-body-content">
    549                             <div id="titlediv">
    550                                 <div id="titlewrap">
    551                                     <label id="title-prompt-text" for="title"><?php esc_html_e( 'Field Group Name', 'buddypress') ?></label>
    552                                     <input type="text" name="group_name" id="title" value="<?php echo esc_attr( $this->name ); ?>" autocomplete="off" />
    553                                 </div>
    554                             </div>
    555 
    556                             <div id="postdiv">
    557                                 <div class="postbox">
    558                                     <div id="titlediv"><h3 class="hndle"><?php _e( 'Group Description', 'buddypress' ); ?></h3></div>
    559                                     <div class="inside">
    560                                         <textarea name="group_description" id="group_description" rows="8" cols="60"><?php echo esc_textarea( $this->description ); ?></textarea>
    561                                     </div>
    562                                 </div>
    563                             </div>
    564                         </div>
    565                         <div id="postbox-container-1" class="postbox-container">
    566                             <div id="side-sortables" class="meta-box-sortables ui-sortable">
    567 
    568                                 <?php
    569 
    570                                 /**
    571                                  * Fires before XProfile Group submit metabox.
    572                                  *
    573                                  * @since BuddyPress (2.1.0)
    574                                  *
    575                                  * @param BP_XProfile_Group $this Current XProfile group.
    576                                  */
    577                                 do_action( 'xprofile_group_before_submitbox', $this );
    578                                 ?>
    579 
    580                                 <div id="submitdiv" class="postbox">
    581                                     <div id="handlediv"><h3 class="hndle"><?php _e( 'Save', 'buddypress' ); ?></h3></div>
    582                                     <div class="inside">
    583                                         <div id="submitcomment" class="submitbox">
    584                                             <div id="major-publishing-actions">
    585 
    586                                                 <?php
    587 
    588                                                 /**
    589                                                  * Fires at the beginning of the XProfile Group publishing actions section.
    590                                                  *
    591                                                  * @since BuddyPress (2.1.0)
    592                                                  *
    593                                                  * @param BP_XProfile_Group $this Current XProfile group.
    594                                                  */
    595                                                 do_action( 'xprofile_group_submitbox_start', $this );
    596                                                 ?>
    597 
    598                                                 <div id="delete-action">
    599                                                     <a href="users.php?page=bp-profile-setup" class="submitdelete deletion"><?php _e( 'Cancel', 'buddypress' ); ?></a>
    600                                                 </div>
    601                                                 <div id="publishing-action">
    602                                                     <input type="submit" name="save_group" value="<?php echo esc_attr( $button ); ?>" class="button-primary"/>
    603                                                 </div>
    604                                                 <input type="hidden" name="group_order" id="group_order" value="<?php echo esc_attr( $this->group_order ); ?>" />
    605                                                 <div class="clear"></div>
    606                                             </div>
    607                                         </div>
    608                                     </div>
    609                                 </div>
    610 
    611                                 <?php
    612 
    613                                 /**
    614                                  * Fires after XProfile Group submit metabox.
    615                                  *
    616                                  * @since BuddyPress (2.1.0)
    617                                  *
    618                                  * @param BP_XProfile_Group $this Current XProfile group.
    619                                  */
    620                                 do_action( 'xprofile_group_after_submitbox', $this );
    621                                 ?>
    622                             </div>
    623                         </div>
    624                     </div>
    625                 </div>
    626             </form>
    627         </div>
    628 
    629 <?php
    630     }
    631 }
    632 
    633 class BP_XProfile_Field {
    634     public $id;
    635     public $group_id;
    636     public $parent_id;
    637     public $type;
    638     public $name;
    639     public $description;
    640     public $is_required;
    641     public $can_delete = '1';
    642     public $field_order;
    643     public $option_order;
    644     public $order_by;
    645     public $is_default_option;
    646     public $default_visibility = 'public';
    647     public $allow_custom_visibility = 'allowed';
    648 
    649     /**
    650      * @since BuddyPress (2.0.0)
    651      * @var BP_XProfile_Field_Type Field type object used for validation
    652      */
    653     public $type_obj = null;
    654 
    655     public $data;
    656     public $message = null;
    657     public $message_type = 'err';
    658 
    659     public function __construct( $id = null, $user_id = null, $get_data = true ) {
    660         if ( !empty( $id ) ) {
    661             $this->populate( $id, $user_id, $get_data );
    662 
    663         // Initialise the type obj to prevent fatals when creating new profile fields
    664         } else {
    665             $this->type_obj            = bp_xprofile_create_field_type( 'textbox' );
    666             $this->type_obj->field_obj = $this;
    667         }
    668     }
    669 
    670     public function populate( $id, $user_id, $get_data ) {
    671         global $wpdb, $userdata;
    672 
    673         if ( empty( $user_id ) ) {
    674             $user_id = isset( $userdata->ID ) ? $userdata->ID : 0;
    675         }
    676 
    677         $bp  = buddypress();
    678         $sql = $wpdb->prepare( "SELECT * FROM {$bp->profile->table_name_fields} WHERE id = %d", $id );
    679 
    680         if ( $field = $wpdb->get_row( $sql ) ) {
    681             $this->id               = $field->id;
    682             $this->group_id          = $field->group_id;
    683             $this->parent_id         = $field->parent_id;
    684             $this->type              = $field->type;
    685             $this->name              = stripslashes( $field->name );
    686             $this->description       = stripslashes( $field->description );
    687             $this->is_required       = $field->is_required;
    688             $this->can_delete        = $field->can_delete;
    689             $this->field_order       = $field->field_order;
    690             $this->option_order      = $field->option_order;
    691             $this->order_by          = $field->order_by;
    692             $this->is_default_option = $field->is_default_option;
    693 
    694             // Create the field type and store a reference back to this object.
    695             $this->type_obj            = bp_xprofile_create_field_type( $field->type );
    696             $this->type_obj->field_obj = $this;
    697 
    698             if ( $get_data && $user_id ) {
    699                 $this->data          = $this->get_field_data( $user_id );
    700             }
    701 
    702             $this->default_visibility = bp_xprofile_get_meta( $id, 'field', 'default_visibility' );
    703 
    704             if ( empty( $this->default_visibility ) ) {
    705                 $this->default_visibility = 'public';
    706             }
    707 
    708             $this->allow_custom_visibility = 'disabled' == bp_xprofile_get_meta( $id, 'field', 'allow_custom_visibility' ) ? 'disabled' : 'allowed';
    709         }
    710     }
    711 
    712     public function delete( $delete_data = false ) {
    713         global $wpdb;
    714 
    715         // Prevent deletion if no ID is present
    716         // Prevent deletion by url when can_delete is false.
    717         // Prevent deletion of option 1 since this invalidates fields with options.
    718         if ( empty( $this->id ) || empty( $this->can_delete ) || ( $this->parent_id && $this->option_order == 1 ) )
    719             return false;
    720 
    721         $bp = buddypress();
    722 
    723         if ( !$wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->profile->table_name_fields} WHERE id = %d OR parent_id = %d", $this->id, $this->id ) ) )
    724             return false;
    725 
    726         // delete the data in the DB for this field
    727         if ( true === $delete_data )
    728             BP_XProfile_ProfileData::delete_for_field( $this->id );
    729 
    730         return true;
    731     }
    732 
    733     public function save() {
    734         global $wpdb;
    735 
    736         $bp = buddypress();
    737 
    738         $this->group_id    = apply_filters( 'xprofile_field_group_id_before_save',    $this->group_id,    $this->id );
    739         $this->parent_id   = apply_filters( 'xprofile_field_parent_id_before_save',   $this->parent_id,   $this->id );
    740         $this->type        = apply_filters( 'xprofile_field_type_before_save',        $this->type,        $this->id );
    741         $this->name        = apply_filters( 'xprofile_field_name_before_save',        $this->name,        $this->id );
    742         $this->description = apply_filters( 'xprofile_field_description_before_save', $this->description, $this->id );
    743         $this->is_required = apply_filters( 'xprofile_field_is_required_before_save', $this->is_required, $this->id );
    744         $this->order_by    = apply_filters( 'xprofile_field_order_by_before_save',    $this->order_by,    $this->id );
    745         $this->field_order = apply_filters( 'xprofile_field_field_order_before_save', $this->field_order, $this->id );
    746         $this->option_order = apply_filters( 'xprofile_field_option_order_before_save', $this->option_order, $this->id );
    747         $this->can_delete  = apply_filters( 'xprofile_field_can_delete_before_save',  $this->can_delete,  $this->id );
    748         $this->type_obj    = bp_xprofile_create_field_type( $this->type );
    749 
    750         /**
    751          * Fires before the current field instance gets saved.
    752          *
    753          * Please use this hook to filter the properties above. Each part will be passed in.
    754          *
    755          * @since BuddyPress (1.0.0)
    756          *
    757          * @param BP_XProfile_Field Current instance of the field being saved.
    758          */
    759         do_action_ref_array( 'xprofile_field_before_save', array( $this ) );
    760 
    761         if ( $this->id != null ) {
    762             $sql = $wpdb->prepare( "UPDATE {$bp->profile->table_name_fields} SET group_id = %d, parent_id = 0, type = %s, name = %s, description = %s, is_required = %d, order_by = %s, field_order = %d, option_order = %d, can_delete = %d WHERE id = %d", $this->group_id, $this->type, $this->name, $this->description, $this->is_required, $this->order_by, $this->field_order, $this->option_order, $this->can_delete, $this->id );
    763         } else {
    764             $sql = $wpdb->prepare( "INSERT INTO {$bp->profile->table_name_fields} (group_id, parent_id, type, name, description, is_required, order_by, field_order, option_order, can_delete ) VALUES (%d, %d, %s, %s, %s, %d, %s, %d, %d, %d )", $this->group_id, $this->parent_id, $this->type, $this->name, $this->description, $this->is_required, $this->order_by, $this->field_order, $this->option_order, $this->can_delete );
    765         }
    766 
    767         /**
    768          * Check for null so field options can be changed without changing any other part of the field.
    769          * The described situation will return 0 here.
    770          */
    771         if ( $wpdb->query( $sql ) !== null ) {
    772 
    773             if ( !empty( $this->id ) ) {
    774                 $field_id = $this->id;
    775             } else {
    776                 $field_id = $wpdb->insert_id;
    777             }
    778 
    779             // Only do this if we are editing an existing field
    780             if ( $this->id != null ) {
    781 
    782                 /**
    783                  * Remove any radio or dropdown options for this
    784                  * field. They will be re-added if needed.
    785                  * This stops orphan options if the user changes a
    786                  * field from a radio button field to a text box.
    787                  */
    788                 $this->delete_children();
    789             }
    790 
    791             /**
    792              * Check to see if this is a field with child options.
    793              * We need to add the options to the db, if it is.
    794              */
    795             if ( $this->type_obj->supports_options ) {
    796 
    797                 if ( !empty( $this->id ) ) {
    798                     $parent_id = $this->id;
    799                 } else {
    800                     $parent_id = $wpdb->insert_id;
    801                 }
    802 
    803                 // Allow plugins to filter the field's child options (i.e. the items in a selectbox).
    804                 $post_option  = ! empty( $_POST["{$this->type}_option"] ) ? $_POST["{$this->type}_option"] : '';
    805                 $post_default = ! empty( $_POST["isDefault_{$this->type}_option"] ) ? $_POST["isDefault_{$this->type}_option"] : '';
    806 
    807                 /**
    808                  * Filters the submitted field option value before saved.
    809                  *
    810                  * @since BuddyPress (1.5.0)
    811                  *
    812                  * @param string            $post_option Submitted option value.
    813                  * @param BP_XProfile_Field $type        Current field type being saved for.
    814                  */
    815                 $options      = apply_filters( 'xprofile_field_options_before_save', $post_option,  $this->type );
    816 
    817                 /**
    818                  * Filters the default field option value before saved.
    819                  *
    820                  * @since BuddyPress (1.5.0)
    821                  *
    822                  * @param string            $post_default Default option value.
    823                  * @param BP_XProfile_Field $type         Current field type being saved for.
    824                  */
    825                 $defaults     = apply_filters( 'xprofile_field_default_before_save', $post_default, $this->type );
    826 
    827                 $counter = 1;
    828                 if ( !empty( $options ) ) {
    829                     foreach ( (array) $options as $option_key => $option_value ) {
    830                         $is_default = 0;
    831 
    832                         if ( is_array( $defaults ) ) {
    833                             if ( isset( $defaults[$option_key] ) )
    834                                 $is_default = 1;
    835                         } else {
    836                             if ( (int) $defaults == $option_key )
    837                                 $is_default = 1;
    838                         }
    839 
    840                         if ( '' != $option_value ) {
    841                             if ( !$wpdb->query( $wpdb->prepare( "INSERT INTO {$bp->profile->table_name_fields} (group_id, parent_id, type, name, description, is_required, option_order, is_default_option) VALUES (%d, %d, 'option', %s, '', 0, %d, %d)", $this->group_id, $parent_id, $option_value, $counter, $is_default ) ) ) {
    842                                 return false;
    843                             }
    844                         }
    845 
    846                         $counter++;
    847                     }
    848                 }
    849             }
    850 
    851             /**
    852              * Fires after the current field instance gets saved.
    853              *
    854              * @since BuddyPress (1.0.0)
    855              *
    856              * @param BP_XProfile_Field Current instance of the field being saved.
    857              */
    858             do_action_ref_array( 'xprofile_field_after_save', array( $this ) );
    859 
    860             // Recreate type_obj in case someone changed $this->type via a filter
    861             $this->type_obj            = bp_xprofile_create_field_type( $this->type );
    862             $this->type_obj->field_obj = $this;
    863 
    864             return $field_id;
    865         } else {
    866             return false;
    867         }
    868     }
    869 
    870     public function get_field_data( $user_id ) {
    871         return new BP_XProfile_ProfileData( $this->id, $user_id );
    872     }
    873 
    874     public function get_children( $for_editing = false ) {
    875         global $wpdb;
    876 
    877         // This is done here so we don't have problems with sql injection
    878         if ( 'asc' == $this->order_by && empty( $for_editing ) ) {
    879             $sort_sql = 'ORDER BY name ASC';
    880         } elseif ( 'desc' == $this->order_by && empty( $for_editing ) ) {
    881             $sort_sql = 'ORDER BY name DESC';
    882         } else {
    883             $sort_sql = 'ORDER BY option_order ASC';
    884         }
    885 
    886         // This eliminates a problem with getting all fields when there is no id for the object
    887         if ( empty( $this->id ) ) {
    888             $parent_id = -1;
    889         } else {
    890             $parent_id = $this->id;
    891         }
    892 
    893         $bp  = buddypress();
    894         $sql = $wpdb->prepare( "SELECT * FROM {$bp->profile->table_name_fields} WHERE parent_id = %d AND group_id = %d $sort_sql", $parent_id, $this->group_id );
    895 
    896         $children = $wpdb->get_results( $sql );
    897 
    898         /**
    899          * Filters the found children for a field.
    900          *
    901          * @since BuddyPress (1.2.5)
    902          *
    903          * @param object $children    Found children for a field.
    904          * @param bool   $for_editing Whether or not the field is for editing.
    905          */
    906         return apply_filters( 'bp_xprofile_field_get_children', $children, $for_editing );
    907     }
    908 
    909     public function delete_children() {
    910         global $wpdb;
    911 
    912         $bp  = buddypress();
    913         $sql = $wpdb->prepare( "DELETE FROM {$bp->profile->table_name_fields} WHERE parent_id = %d", $this->id );
    914 
    915         $wpdb->query( $sql );
    916     }
    917 
    918     /** Static Methods ********************************************************/
    919 
    920     public static function get_type( $field_id ) {
    921         global $wpdb;
    922 
    923         if ( !empty( $field_id ) ) {
    924             $bp  = buddypress();
    925             $sql = $wpdb->prepare( "SELECT type FROM {$bp->profile->table_name_fields} WHERE id = %d", $field_id );
    926 
    927             if ( !$field_type = $wpdb->get_var( $sql ) ) {
    928                 return false;
    929             }
    930 
    931             return $field_type;
    932         }
    933 
    934         return false;
    935     }
    936 
    937     public static function delete_for_group( $group_id ) {
    938         global $wpdb;
    939 
    940         if ( !empty( $group_id ) ) {
    941             $bp  = buddypress();
    942             $sql = $wpdb->prepare( "DELETE FROM {$bp->profile->table_name_fields} WHERE group_id = %d", $group_id );
    943 
    944             if ( $wpdb->get_var( $sql ) === false ) {
    945                 return false;
    946             }
    947 
    948             return true;
    949         }
    950 
    951         return false;
    952     }
    953 
    954     public static function get_id_from_name( $field_name ) {
    955         global $wpdb;
    956 
    957         $bp = buddypress();
    958 
    959         if ( empty( $bp->profile->table_name_fields ) || !isset( $field_name ) )
    960             return false;
    961 
    962         return $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$bp->profile->table_name_fields} WHERE name = %s AND parent_id = 0", $field_name ) );
    963     }
    964 
    965     public static function update_position( $field_id, $position, $field_group_id ) {
    966         global $wpdb;
    967 
    968         if ( !is_numeric( $position ) || !is_numeric( $field_group_id ) )
    969             return false;
    970 
    971         $bp = buddypress();
    972 
    973         // Update $field_id with new $position and $field_group_id
    974         if ( $parent = $wpdb->query( $wpdb->prepare( "UPDATE {$bp->profile->table_name_fields} SET field_order = %d, group_id = %d WHERE id = %d", $position, $field_group_id, $field_id ) ) ) {;
    975 
    976             // Update any children of this $field_id
    977             $children = $wpdb->query( $wpdb->prepare( "UPDATE {$bp->profile->table_name_fields} SET group_id = %d WHERE parent_id = %d", $field_group_id, $field_id ) );
    978 
    979             return $parent;
    980         }
    981 
    982         return false;
    983     }
    984 
    985     /**
    986      * This function populates the items for radio buttons checkboxes and drop down boxes
    987      */
    988     public function render_admin_form_children() {
    989         foreach ( array_keys( bp_xprofile_get_field_types() ) as $field_type ) {
    990             $type_obj = bp_xprofile_create_field_type( $field_type );
    991             $type_obj->admin_new_field_html( $this );
    992         }
    993     }
    994 
    995     public function render_admin_form( $message = '' ) {
    996         if ( empty( $this->id ) ) {
    997             $title  = __( 'Add Field', 'buddypress' );
    998             $action = "users.php?page=bp-profile-setup&amp;group_id=" . $this->group_id . "&amp;mode=add_field#tabs-" . $this->group_id;
    999 
    1000             if ( !empty( $_POST['saveField'] ) ) {
    1001                 $this->name        = $_POST['title'];
    1002                 $this->description = $_POST['description'];
    1003                 $this->is_required = $_POST['required'];
    1004                 $this->type        = $_POST['fieldtype'];
    1005                 $this->order_by    = $_POST["sort_order_{$this->type}"];
    1006                 $this->field_order = $_POST['field_order'];
    1007             }
    1008         } else {
    1009             $title  = __( 'Edit Field', 'buddypress' );
    1010             $action = "users.php?page=bp-profile-setup&amp;mode=edit_field&amp;group_id=" . $this->group_id . "&amp;field_id=" . $this->id . "#tabs-" . $this->group_id;
    1011         } ?>
    1012 
    1013         <div class="wrap">
    1014             <div id="icon-users" class="icon32"><br /></div>
    1015             <h2><?php echo esc_html( $title ); ?></h2>
    1016 
    1017             <?php if ( !empty( $message ) ) : ?>
    1018 
    1019                 <div id="message" class="error fade">
    1020                     <p><?php echo esc_html( $message ); ?></p>
    1021                 </div>
    1022 
    1023             <?php endif; ?>
    1024 
    1025             <form id="bp-xprofile-add-field" action="<?php echo esc_url( $action ); ?>" method="post">
    1026                 <div id="poststuff">
    1027                     <div id="post-body" class="metabox-holder columns-<?php echo ( 1 == get_current_screen()->get_columns() ) ? '1' : '2'; ?>">
    1028                         <div id="post-body-content">
    1029                             <div id="titlediv">
    1030                                 <div class="titlewrap">
    1031                                     <label id="title-prompt-text" for="title"><?php echo esc_attr_x( 'Field Name', 'XProfile admin edit field', 'buddypress' ); ?></label>
    1032                                     <input type="text" name="title" id="title" value="<?php echo esc_attr( $this->name ); ?>" autocomplete="off" />
    1033                                 </div>
    1034                             </div>
    1035                             <div class="postbox">
    1036                                 <h3><?php _e( 'Field Description', 'buddypress' ); ?></h3>
    1037                                 <div class="inside">
    1038                                     <textarea name="description" id="description" rows="8" cols="60"><?php echo esc_textarea( $this->description ); ?></textarea>
    1039                                 </div>
    1040                             </div>
    1041                         </div><!-- #post-body-content -->
    1042 
    1043                         <div id="postbox-container-1" class="postbox-container">
    1044 
    1045                             <?php
    1046 
    1047                             /**
    1048                              * Fires before XProfile Field submit metabox.
    1049                              *
    1050                              * @since BuddyPress (2.1.0)
    1051                              *
    1052                              * @param BP_XProfile_Field $this Current XProfile field.
    1053                              */
    1054                             do_action( 'xprofile_field_before_submitbox', $this );
    1055                             ?>
    1056 
    1057                             <div id="submitdiv" class="postbox">
    1058                                 <h3><?php _e( 'Submit', 'buddypress' ); ?></h3>
    1059                                 <div class="inside">
    1060                                     <div id="submitcomment" class="submitbox">
    1061                                         <div id="major-publishing-actions">
    1062 
    1063                                             <?php
    1064 
    1065                                             /**
    1066                                              * Fires at the beginning of the XProfile Field publishing actions section.
    1067                                              *
    1068                                              * @since BuddyPress (2.1.0)
    1069                                              *
    1070                                              * @param BP_XProfile_Field $this Current XProfile field.
    1071                                              */
    1072                                             do_action( 'xprofile_field_submitbox_start', $this );
    1073                                             ?>
    1074 
    1075                                             <input type="hidden" name="field_order" id="field_order" value="<?php echo esc_attr( $this->field_order ); ?>" />
    1076                                             <div id="publishing-action">
    1077                                                 <input type="submit" value="<?php esc_attr_e( 'Save', 'buddypress' ); ?>" name="saveField" id="saveField" style="font-weight: bold" class="button-primary" />
    1078                                             </div>
    1079                                             <div id="delete-action">
    1080                                                 <a href="users.php?page=bp-profile-setup" class="deletion"><?php _e( 'Cancel', 'buddypress' ); ?></a>
    1081                                             </div>
    1082                                             <?php wp_nonce_field( 'xprofile_delete_option' ); ?>
    1083                                             <div class="clear"></div>
    1084                                         </div>
    1085                                     </div>
    1086                                 </div>
    1087                             </div>
    1088 
    1089                             <?php
    1090 
    1091                             /**
    1092                              * Fires after XProfile Field submit metabox.
    1093                              *
    1094                              * @since BuddyPress (2.1.0)
    1095                              *
    1096                              * @param BP_XProfile_Field $this Current XProfile field.
    1097                              */
    1098                             do_action( 'xprofile_field_after_submitbox', $this );
    1099                             ?>
    1100 
    1101                             <?php /* Field 1 is the fullname field, which cannot have custom visibility */ ?>
    1102                             <?php if ( 1 != $this->id ) : ?>
    1103 
    1104                                 <div class="postbox">
    1105                                     <h3><label for="default-visibility"><?php _e( 'Default Visibility', 'buddypress' ); ?></label></h3>
    1106                                     <div class="inside">
    1107                                         <ul>
    1108 
    1109                                             <?php foreach( bp_xprofile_get_visibility_levels() as $level ) : ?>
    1110 
    1111                                                 <li>
    1112                                                     <input type="radio" id="default-visibility[<?php echo esc_attr( $level['id'] ) ?>]" name="default-visibility" value="<?php echo esc_attr( $level['id'] ) ?>" <?php checked( $this->default_visibility, $level['id'] ); ?> />
    1113                                                     <label for="default-visibility[<?php echo esc_attr( $level['id'] ) ?>]"><?php echo esc_html( $level['label'] ) ?></label>
    1114                                                 </li>
    1115 
    1116                                             <?php endforeach ?>
    1117 
    1118                                         </ul>
    1119                                     </div>
    1120                                 </div>
    1121 
    1122                                 <div class="postbox">
    1123                                     <h3><label for="allow-custom-visibility"><?php _e( 'Per-Member Visibility', 'buddypress' ); ?></label></h3>
    1124                                     <div class="inside">
    1125                                         <ul>
    1126                                             <li>
    1127                                                 <input type="radio" id="allow-custom-visibility-allowed" name="allow-custom-visibility" value="allowed" <?php checked( $this->allow_custom_visibility, 'allowed' ); ?> />
    1128                                                 <label for="allow-custom-visibility-allowed"><?php _e( "Let members change this field's visibility", 'buddypress' ); ?></label>
    1129                                             </li>
    1130                                             <li>
    1131                                                 <input type="radio" id="allow-custom-visibility-disabled" name="allow-custom-visibility" value="disabled" <?php checked( $this->allow_custom_visibility, 'disabled' ); ?> />
    1132                                                 <label for="allow-custom-visibility-disabled"><?php _e( 'Enforce the default visibility for all members', 'buddypress' ); ?></label>
    1133                                             </li>
    1134                                         </ul>
    1135                                     </div>
    1136                                 </div>
    1137 
    1138                             <?php endif ?>
    1139 
    1140                             <?php
    1141 
    1142                             /**
    1143                              * Fires after XProfile Field sidebar metabox.
    1144                              *
    1145                              * @since BuddyPress (2.2.0)
    1146                              *
    1147                              * @param BP_XProfile_Field $this Current XProfile field.
    1148                              */
    1149                             do_action( 'xprofile_field_after_sidebarbox', $this ); ?>
    1150                         </div>
    1151 
    1152                         <div id="postbox-container-2" class="postbox-container">
    1153 
    1154                             <?php /* Field 1 is the fullname field, which cannot be altered */ ?>
    1155                             <?php if ( 1 != $this->id ) : ?>
    1156 
    1157                                 <div class="postbox">
    1158                                     <h3><label for="required"><?php _e( "Field Requirement", 'buddypress' ); ?></label></h3>
    1159                                     <div class="inside">
    1160                                         <select name="required" id="required" style="width: 30%">
    1161                                             <option value="0"<?php selected( $this->is_required, '0' ); ?>><?php _e( 'Not Required', 'buddypress' ); ?></option>
    1162                                             <option value="1"<?php selected( $this->is_required, '1' ); ?>><?php _e( 'Required',     'buddypress' ); ?></option>
    1163                                         </select>
    1164                                     </div>
    1165                                 </div>
    1166 
    1167                                 <div class="postbox">
    1168                                     <h3><label for="fieldtype"><?php _e( 'Field Type', 'buddypress'); ?></label></h3>
    1169                                     <div class="inside">
    1170                                         <select name="fieldtype" id="fieldtype" onchange="show_options(this.value)" style="width: 30%">
    1171                                             <?php bp_xprofile_admin_form_field_types( $this->type ); ?>
    1172                                         </select>
    1173 
    1174                                         <?php
    1175                                         // Deprecated filter, don't use. Go look at {@link BP_XProfile_Field_Type::admin_new_field_html()}.
    1176                                         do_action( 'xprofile_field_additional_options', $this );
    1177 
    1178                                         $this->render_admin_form_children();
    1179                                         ?>
    1180                                     </div>
    1181                                 </div>
    1182 
    1183                             <?php else : ?>
    1184 
    1185                                 <input type="hidden" name="required"  id="required"  value="1"       />
    1186                                 <input type="hidden" name="fieldtype" id="fieldtype" value="textbox" />
    1187 
    1188                             <?php endif; ?>
    1189 
    1190                             <?php
    1191 
    1192                             /**
    1193                              * Fires after XProfile Field content metabox.
    1194                              *
    1195                              * @since BuddyPress (2.2.0)
    1196                              *
    1197                              * @param BP_XProfile_Field $this Current XProfile field.
    1198                              */
    1199                             do_action( 'xprofile_field_after_contentbox', $this ); ?>
    1200                         </div>
    1201                     </div><!-- #post-body -->
    1202 
    1203                 </div><!-- #poststuff -->
    1204 
    1205             </form>
    1206         </div>
    1207 
    1208 <?php
    1209     }
    1210 
    1211     public static function admin_validate() {
    1212         global $message;
    1213 
    1214         // Validate Form
    1215         if ( '' == $_POST['title'] || '' == $_POST['required'] || '' == $_POST['fieldtype'] ) {
    1216             $message = __( 'Please make sure you fill out all required fields.', 'buddypress' );
    1217             return false;
    1218 
    1219         } elseif ( empty( $_POST['field_file'] ) ) {
    1220             $field_type  = bp_xprofile_create_field_type( $_POST['fieldtype'] );
    1221             $option_name = "{$_POST['fieldtype']}_option";
    1222 
    1223             if ( $field_type->supports_options && isset( $_POST[$option_name] ) && empty( $_POST[$option_name][1] ) ) {
    1224                 $message = __( 'This field type require at least one option. Please add options below.', 'buddypress' );
    1225                 return false;
    1226             }
    1227         }
    1228 
    1229         return true;
    1230     }
    1231 }
    1232 
    1233 class BP_XProfile_ProfileData {
    1234     public $id;
    1235     public $user_id;
    1236     public $field_id;
    1237     public $value;
    1238     public $last_updated;
    1239 
    1240     public function __construct( $field_id = null, $user_id = null ) {
    1241         if ( !empty( $field_id ) ) {
    1242             $this->populate( $field_id, $user_id );
    1243         }
    1244     }
    1245 
    1246     public function populate( $field_id, $user_id )  {
    1247         global $wpdb;
    1248 
    1249         $cache_key   = "{$user_id}:{$field_id}";
    1250         $profiledata = wp_cache_get( $cache_key, 'bp_xprofile_data' );
    1251 
    1252         if ( false === $profiledata ) {
    1253             $bp = buddypress();
    1254 
    1255             $sql         = $wpdb->prepare( "SELECT * FROM {$bp->profile->table_name_data} WHERE field_id = %d AND user_id = %d", $field_id, $user_id );
    1256             $profiledata = $wpdb->get_row( $sql );
    1257 
    1258             if ( $profiledata ) {
    1259                 wp_cache_set( $cache_key, $profiledata, 'bp_xprofile_data' );
    1260             }
    1261         }
    1262 
    1263         if ( $profiledata ) {
    1264             $this->id           = $profiledata->id;
    1265             $this->user_id      = $profiledata->user_id;
    1266             $this->field_id     = $profiledata->field_id;
    1267             $this->value        = stripslashes( $profiledata->value );
    1268             $this->last_updated = $profiledata->last_updated;
    1269 
    1270         } else {
    1271             // When no row is found, we'll need to set these properties manually
    1272             $this->field_id     = $field_id;
    1273             $this->user_id      = $user_id;
    1274         }
    1275     }
    1276 
    1277     /**
    1278      * Check if there is data already for the user.
    1279      *
    1280      * @global object $wpdb
    1281      * @global array $bp
    1282      * @return bool
    1283      */
    1284     public function exists() {
    1285         global $wpdb;
    1286 
    1287         // Check cache first
    1288         $cache_key = "{$this->user_id}:{$this->field_id}";
    1289         $cached    = wp_cache_get( $cache_key, 'bp_xprofile_data' );
    1290 
    1291         if ( $cached && ! empty( $cached->id ) ) {
    1292             $retval = true;
    1293         } else {
    1294             $bp = buddypress();
    1295             $retval = $wpdb->get_row( $wpdb->prepare( "SELECT id FROM {$bp->profile->table_name_data} WHERE user_id = %d AND field_id = %d", $this->user_id, $this->field_id ) );
    1296         }
    1297 
    1298         /**
    1299          * Filters whether or not data already exists for the user.
    1300          *
    1301          * @since BuddyPress (1.2.7)
    1302          *
    1303          * @param bool                    $retval Whether or not data already exists.
    1304          * @param BP_XProfile_ProfileData $this   Instance of the current BP_XProfile_ProfileData class.
    1305          */
    1306         return apply_filters_ref_array( 'xprofile_data_exists', array( (bool)$retval, $this ) );
    1307     }
    1308 
    1309     /**
    1310      * Check if this data is for a valid field.
    1311      *
    1312      * @global object $wpdb
    1313      * @return bool
    1314      */
    1315     public function is_valid_field() {
    1316         global $wpdb;
    1317 
    1318         $bp = buddypress();
    1319 
    1320         $retval = $wpdb->get_row( $wpdb->prepare( "SELECT id FROM {$bp->profile->table_name_fields} WHERE id = %d", $this->field_id ) );
    1321 
    1322         /**
    1323          * Filters whether or not data is for a valid field
    1324          *
    1325          * @since BuddyPress (1.2.7)
    1326          *
    1327          * @param bool                    $retval Whether or not data is valid.
    1328          * @param BP_XProfile_ProfileData $this   Instance of the current BP_XProfile_ProfileData class.
    1329          */
    1330         return apply_filters_ref_array( 'xprofile_data_is_valid_field', array( (bool)$retval, $this ) );
    1331     }
    1332 
    1333     public function save() {
    1334         global $wpdb;
    1335 
    1336         $bp = buddypress();
    1337 
    1338         $this->user_id      = apply_filters( 'xprofile_data_user_id_before_save',      $this->user_id,         $this->id );
    1339         $this->field_id     = apply_filters( 'xprofile_data_field_id_before_save',     $this->field_id,        $this->id );
    1340         $this->value        = apply_filters( 'xprofile_data_value_before_save',        $this->value,           $this->id, true, $this );
    1341         $this->last_updated = apply_filters( 'xprofile_data_last_updated_before_save', bp_core_current_time(), $this->id );
    1342 
    1343         /**
    1344          * Fires before the current profile data instance gets saved.
    1345          *
    1346          * Please use this hook to filter the properties above. Each part will be passed in.
    1347          *
    1348          * @since BuddyPress (1.0.0)
    1349          *
    1350          * @param BP_XProfile_ProfileData $this Current instance of the profile data being saved.
    1351          */
    1352         do_action_ref_array( 'xprofile_data_before_save', array( $this ) );
    1353 
    1354         if ( $this->is_valid_field() ) {
    1355             if ( $this->exists() && strlen( trim( $this->value ) ) ) {
    1356                 $result   = $wpdb->query( $wpdb->prepare( "UPDATE {$bp->profile->table_name_data} SET value = %s, last_updated = %s WHERE user_id = %d AND field_id = %d", $this->value, $this->last_updated, $this->user_id, $this->field_id ) );
    1357 
    1358             } elseif ( $this->exists() && empty( $this->value ) ) {
    1359                 // Data removed, delete the entry.
    1360                 $result   = $this->delete();
    1361 
    1362             } else {
    1363                 $result   = $wpdb->query( $wpdb->prepare("INSERT INTO {$bp->profile->table_name_data} (user_id, field_id, value, last_updated) VALUES (%d, %d, %s, %s)", $this->user_id, $this->field_id, $this->value, $this->last_updated ) );
    1364                 $this->id = $wpdb->insert_id;
    1365             }
    1366 
    1367             if ( false === $result )
    1368                 return false;
    1369 
    1370             /**
    1371              * Fires after the current profile data instance gets saved.
    1372              *
    1373              * @since BuddyPress (1.0.0)
    1374              *
    1375              * @param BP_XProfile_ProfileData $this Current instance of the profile data being saved.
    1376              */
    1377             do_action_ref_array( 'xprofile_data_after_save', array( $this ) );
    1378 
    1379             return true;
    1380         }
    1381 
    1382         return false;
    1383     }
    1384 
    1385     /**
    1386      * Delete specific XProfile field data
    1387      *
    1388      * @global object $wpdb
    1389      * @return boolean
    1390      */
    1391     public function delete() {
    1392         global $wpdb;
    1393 
    1394         $bp = buddypress();
    1395 
    1396         /**
    1397          * Fires before the current profile data instance gets deleted.
    1398          *
    1399          * @since BuddyPress (1.9.0)
    1400          *
    1401          * @param BP_XProfile_ProfileData $this Current instance of the profile data being deleted.
    1402          */
    1403         do_action_ref_array( 'xprofile_data_before_delete', array( $this ) );
    1404 
    1405         if ( !$wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->profile->table_name_data} WHERE field_id = %d AND user_id = %d", $this->field_id, $this->user_id ) ) )
    1406             return false;
    1407 
    1408         /**
    1409          * Fires after the current profile data instance gets deleted.
    1410          *
    1411          * @since BuddyPress (1.9.0)
    1412          *
    1413          * @param BP_XProfile_ProfileData $this Current instance of the profile data being deleted.
    1414          */
    1415         do_action_ref_array( 'xprofile_data_after_delete', array( $this ) );
    1416 
    1417         return true;
    1418     }
    1419 
    1420     /** Static Methods ********************************************************/
    1421 
    1422     /**
    1423      * Get a user's profile data for a set of fields.
    1424      *
    1425      * @param int $user_id
    1426      * @param array $field_ids
    1427      * @return array
    1428      */
    1429     public static function get_data_for_user( $user_id, $field_ids ) {
    1430         global $wpdb;
    1431 
    1432         $data = array();
    1433 
    1434         $uncached_field_ids = bp_xprofile_get_non_cached_field_ids( $user_id, $field_ids, 'bp_xprofile_data' );
    1435 
    1436         // Prime the cache
    1437         if ( ! empty( $uncached_field_ids ) ) {
    1438             $bp = buddypress();
    1439             $uncached_field_ids_sql = implode( ',', wp_parse_id_list( $uncached_field_ids ) );
    1440             $uncached_data = $wpdb->get_results( $wpdb->prepare( "SELECT id, user_id, field_id, value, last_updated FROM {$bp->profile->table_name_data} WHERE field_id IN ({$uncached_field_ids_sql}) AND user_id = %d", $user_id ) );
    1441 
    1442             // Rekey
    1443             $queried_data = array();
    1444             foreach ( $uncached_data as $ud ) {
    1445                 $d               = new stdClass;
    1446                 $d->id           = $ud->id;
    1447                 $d->user_id      = $ud->user_id;
    1448                 $d->field_id     = $ud->field_id;
    1449                 $d->value        = $ud->value;
    1450                 $d->last_updated = $ud->last_updated;
    1451 
    1452                 $queried_data[ $ud->field_id ] = $d;
    1453             }
    1454 
    1455             // Set caches
    1456             foreach ( $uncached_field_ids as $field_id ) {
    1457 
    1458                 $cache_key = "{$user_id}:{$field_id}";
    1459 
    1460                 // If a value was found, cache it
    1461                 if ( isset( $queried_data[ $field_id ] ) ) {
    1462                     wp_cache_set( $cache_key, $queried_data[ $field_id ], 'bp_xprofile_data' );
    1463 
    1464                 // If no value was found, cache an empty item
    1465                 // to avoid future cache misses
    1466                 } else {
    1467                     $d               = new stdClass;
    1468                     $d->id           = '';
    1469                     $d->user_id      = '';
    1470                     $d->field_id     = $field_id;
    1471                     $d->value        = '';
    1472                     $d->last_updated = '';
    1473 
    1474                     wp_cache_set( $cache_key, $d, 'bp_xprofile_data' );
    1475                 }
    1476             }
    1477         }
    1478 
    1479         // Now that all items are cached, fetch them
    1480         foreach ( $field_ids as $field_id ) {
    1481             $cache_key = "{$user_id}:{$field_id}";
    1482             $data[]    = wp_cache_get( $cache_key, 'bp_xprofile_data' );
    1483         }
    1484 
    1485         return $data;
    1486     }
    1487 
    1488     /**
    1489      * Get all of the profile information for a specific user.
    1490      *
    1491      * @param int $user_id ID of the user.
    1492      * @return array
    1493      */
    1494     public static function get_all_for_user( $user_id ) {
    1495 
    1496         $groups = bp_xprofile_get_groups( array(
    1497             'user_id'                => $user_id,
    1498             'hide_empty_groups'      => true,
    1499             'hide_empty_fields'      => true,
    1500             'fetch_fields'           => true,
    1501             'fetch_field_data'       => true,
    1502         ) );
    1503 
    1504         $profile_data = array();
    1505 
    1506         if ( ! empty( $groups ) ) {
    1507             $user = new WP_User( $user_id );
    1508 
    1509             $profile_data['user_login']    = $user->user_login;
    1510             $profile_data['user_nicename'] = $user->user_nicename;
    1511             $profile_data['user_email']    = $user->user_email;
    1512 
    1513             foreach ( (array) $groups as $group ) {
    1514                 if ( empty( $group->fields ) ) {
    1515                     continue;
    1516                 }
    1517 
    1518                 foreach ( (array) $group->fields as $field ) {
    1519                     $profile_data[ $field->name ] = array(
    1520                         'field_group_id'   => $group->id,
    1521                         'field_group_name' => $group->name,
    1522                         'field_id'         => $field->id,
    1523                         'field_type'       => $field->type,
    1524                         'field_data'       => $field->data->value,
    1525                     );
    1526                 }
    1527             }
    1528         }
    1529 
    1530         return $profile_data;
    1531     }
    1532 
    1533     /**
    1534      * Get the user's field data id by the id of the xprofile field.
    1535      *
    1536      * @param int $field_id
    1537      * @param int $user_id
    1538      * @return int $fielddata_id
    1539      */
    1540     public static function get_fielddataid_byid( $field_id, $user_id ) {
    1541         global $wpdb;
    1542 
    1543         if ( empty( $field_id ) || empty( $user_id ) ) {
    1544             $fielddata_id = 0;
    1545         } else {
    1546             $bp = buddypress();
    1547 
    1548             // Check cache first
    1549             $cache_key = "{$user_id}:{$field_id}";
    1550             $fielddata = wp_cache_get( $cache_key, 'bp_xprofile_data' );
    1551             if ( false === $fielddata || empty( $fielddata->id ) ) {
    1552                 $fielddata_id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$bp->profile->table_name_data} WHERE field_id = %d AND user_id = %d", $field_id, $user_id ) );
    1553             } else {
    1554                 $fielddata_id = $fielddata->id;
    1555             }
    1556         }
    1557 
    1558         return $fielddata_id;
    1559     }
    1560 
    1561     /**
    1562      * Get profile field values by field ID and user IDs.
    1563      *
    1564      * Supports multiple user IDs.
    1565      *
    1566      * @param int $field_id ID of the field.
    1567      * @param int|array $user_ids ID or IDs of user(s).
    1568      * @return string|array Single value if a single user is queried,
    1569      *         otherwise an array of results.
    1570      */
    1571     public static function get_value_byid( $field_id, $user_ids = null ) {
    1572         global $wpdb;
    1573 
    1574         if ( empty( $user_ids ) ) {
    1575             $user_ids = bp_displayed_user_id();
    1576         }
    1577 
    1578         $is_single = false;
    1579         if ( ! is_array( $user_ids ) ) {
    1580             $user_ids  = array( $user_ids );
    1581             $is_single = true;
    1582         }
    1583 
    1584         // Assemble uncached IDs
    1585         $uncached_ids = array();
    1586         foreach ( $user_ids as $user_id ) {
    1587             $cache_key = "{$user_id}:{$field_id}";
    1588             if ( false === wp_cache_get( $cache_key, 'bp_xprofile_data' ) ) {
    1589                 $uncached_ids[] = $user_id;
    1590             }
    1591         }
    1592 
    1593         // Prime caches
    1594         if ( ! empty( $uncached_ids ) ) {
    1595             $bp = buddypress();
    1596             $uncached_ids_sql = implode( ',', $uncached_ids );
    1597             $queried_data = $wpdb->get_results( $wpdb->prepare( "SELECT id, user_id, field_id, value, last_updated FROM {$bp->profile->table_name_data} WHERE field_id = %d AND user_id IN ({$uncached_ids_sql})", $field_id ) );
    1598 
    1599             // Rekey
    1600             $qd = array();
    1601             foreach ( $queried_data as $data ) {
    1602                 $qd[ $data->user_id ] = $data;
    1603             }
    1604 
    1605             foreach ( $uncached_ids as $id ) {
    1606                 // The value was successfully fetched
    1607                 if ( isset( $qd[ $id ] ) ) {
    1608                     $d = $qd[ $id ];
    1609 
    1610                 // No data found for the user, so we fake it to
    1611                 // avoid cache misses and PHP notices
    1612                 } else {
    1613                     $d = new stdClass;
    1614                     $d->id           = '';
    1615                     $d->user_id      = $id;
    1616                     $d->field_id     = '';
    1617                     $d->value        = '';
    1618                     $d->last_updated = '';
    1619                 }
    1620 
    1621                 $cache_key = "{$d->user_id}:{$field_id}";
    1622                 wp_cache_set( $cache_key, $d, 'bp_xprofile_data' );
    1623             }
    1624         }
    1625 
    1626         // Now that the cache is primed with all data, fetch it
    1627         $data = array();
    1628         foreach ( $user_ids as $user_id ) {
    1629             $cache_key = "{$user_id}:{$field_id}";
    1630             $data[]    = wp_cache_get( $cache_key, 'bp_xprofile_data' );
    1631         }
    1632 
    1633         // If a single ID was passed, just return the value
    1634         if ( $is_single ) {
    1635             return $data[0]->value;
    1636 
    1637         // Otherwise return the whole array
    1638         } else {
    1639             return $data;
    1640         }
    1641     }
    1642 
    1643     public static function get_value_byfieldname( $fields, $user_id = null ) {
    1644         global $wpdb;
    1645 
    1646         if ( empty( $fields ) )
    1647             return false;
    1648 
    1649         $bp = buddypress();
    1650 
    1651         if ( empty( $user_id ) )
    1652             $user_id = bp_displayed_user_id();
    1653 
    1654         $field_sql = '';
    1655 
    1656         if ( is_array( $fields ) ) {
    1657             for ( $i = 0, $count = count( $fields ); $i < $count; ++$i ) {
    1658                 if ( $i == 0 ) {
    1659                     $field_sql .= $wpdb->prepare( "AND ( f.name = %s ", $fields[$i] );
    1660                 } else {
    1661                     $field_sql .= $wpdb->prepare( "OR f.name = %s ", $fields[$i] );
    1662                 }
    1663             }
    1664 
    1665             $field_sql .= ')';
    1666         } else {
    1667             $field_sql .= $wpdb->prepare( "AND f.name = %s", $fields );
    1668         }
    1669 
    1670         $sql = $wpdb->prepare( "SELECT d.value, f.name FROM {$bp->profile->table_name_data} d, {$bp->profile->table_name_fields} f WHERE d.field_id = f.id AND d.user_id = %d AND f.parent_id = 0 $field_sql", $user_id );
    1671 
    1672         if ( !$values = $wpdb->get_results( $sql ) )
    1673             return false;
    1674 
    1675         $new_values = array();
    1676 
    1677         if ( is_array( $fields ) ) {
    1678             for ( $i = 0, $count = count( $values ); $i < $count; ++$i ) {
    1679                 for ( $j = 0; $j < count( $fields ); $j++ ) {
    1680                     if ( $values[$i]->name == $fields[$j] ) {
    1681                         $new_values[$fields[$j]] = $values[$i]->value;
    1682                     } elseif ( !array_key_exists( $fields[$j], $new_values ) ) {
    1683                         $new_values[$fields[$j]] = NULL;
    1684                     }
    1685                 }
    1686             }
    1687         } else {
    1688             $new_values = $values[0]->value;
    1689         }
    1690 
    1691         return $new_values;
    1692     }
    1693 
    1694     public static function delete_for_field( $field_id ) {
    1695         global $wpdb;
    1696 
    1697         $bp = buddypress();
    1698 
    1699         if ( !$wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->profile->table_name_data} WHERE field_id = %d", $field_id ) ) )
    1700             return false;
    1701 
    1702         return true;
    1703     }
    1704 
    1705     public static function get_last_updated( $user_id ) {
    1706         global $wpdb;
    1707 
    1708         $bp = buddypress();
    1709 
    1710         $last_updated = $wpdb->get_var( $wpdb->prepare( "SELECT last_updated FROM {$bp->profile->table_name_data} WHERE user_id = %d ORDER BY last_updated LIMIT 1", $user_id ) );
    1711 
    1712         return $last_updated;
    1713     }
    1714 
    1715     public static function delete_data_for_user( $user_id ) {
    1716         global $wpdb;
    1717 
    1718         $bp = buddypress();
    1719 
    1720         return $wpdb->query( $wpdb->prepare( "DELETE FROM {$bp->profile->table_name_data} WHERE user_id = %d", $user_id ) );
    1721     }
    1722 
    1723     public static function get_random( $user_id, $exclude_fullname ) {
    1724         global $wpdb;
    1725 
    1726         $exclude_sql = ! empty( $exclude_fullname ) ? ' AND pf.id != 1' : '';
    1727 
    1728         $bp = buddypress();
    1729 
    1730         return $wpdb->get_results( $wpdb->prepare( "SELECT pf.type, pf.name, pd.value FROM {$bp->profile->table_name_data} pd INNER JOIN {$bp->profile->table_name_fields} pf ON pd.field_id = pf.id AND pd.user_id = %d {$exclude_sql} ORDER BY RAND() LIMIT 1", $user_id ) );
    1731     }
    1732 
    1733     public static function get_fullname( $user_id = 0 ) {
    1734 
    1735         if ( empty( $user_id ) )
    1736             $user_id = bp_displayed_user_id();
    1737 
    1738         $data = xprofile_get_field_data( bp_xprofile_fullname_field_id(), $user_id );
    1739 
    1740         return $data[$field_name];
    1741     }
    1742 }
    1743 
    1744 /** Field Types ***************************************************************/
    1745 
    1746 /**
    1747  * Datebox xprofile field type.
    1748  *
    1749  * @since BuddyPress (2.0.0)
    1750  */
    1751 class BP_XProfile_Field_Type_Datebox extends BP_XProfile_Field_Type {
    1752 
    1753     /**
    1754      * Constructor for the datebox field type
    1755      *
    1756      * @since BuddyPress (2.0.0)
    1757      */
    1758     public function __construct() {
    1759         parent::__construct();
    1760 
    1761         $this->category = _x( 'Single Fields', 'xprofile field type category', 'buddypress' );
    1762         $this->name     = _x( 'Date Selector', 'xprofile field type', 'buddypress' );
    1763 
    1764         $this->set_format( '/^\d{4}-\d{1,2}-\d{1,2} 00:00:00$/', 'replace' ); // "Y-m-d 00:00:00"
    1765 
    1766         /**
    1767          * Fires inside __construct() method for BP_XProfile_Field_Type_Datebox class.
    1768          *
    1769          * @since BuddyPress (2.0.0)
    1770          *
    1771          * @param BP_XProfile_Field_Type_Datebox $this Current instance of
    1772          *                                             the field type datebox.
    1773          */
    1774         do_action( 'bp_xprofile_field_type_datebox', $this );
    1775     }
    1776 
    1777     /**
    1778      * Output the edit field HTML for this field type.
    1779      *
    1780      * Must be used inside the {@link bp_profile_fields()} template loop.
    1781      *
    1782      * @param array $raw_properties Optional key/value array of {@link http://dev.w3.org/html5/markup/input.html permitted attributes} that you want to add.
    1783      * @since BuddyPress (2.0.0)
    1784      */
    1785     public function edit_field_html( array $raw_properties = array() ) {
    1786 
    1787         // user_id is a special optional parameter that we pass to
    1788         // {@link bp_the_profile_field_options()}.
    1789         if ( isset( $raw_properties['user_id'] ) ) {
    1790             $user_id = (int) $raw_properties['user_id'];
    1791             unset( $raw_properties['user_id'] );
    1792         } else {
    1793             $user_id = bp_displayed_user_id();
    1794         }
    1795 
    1796         $day_r = bp_parse_args( $raw_properties, array(
    1797             'id'   => bp_get_the_profile_field_input_name() . '_day',
    1798             'name' => bp_get_the_profile_field_input_name() . '_day'
    1799         ) );
    1800 
    1801         $month_r = bp_parse_args( $raw_properties, array(
    1802             'id'   => bp_get_the_profile_field_input_name() . '_month',
    1803             'name' => bp_get_the_profile_field_input_name() . '_month'
    1804         ) );
    1805 
    1806         $year_r = bp_parse_args( $raw_properties, array(
    1807             'id'   => bp_get_the_profile_field_input_name() . '_year',
    1808             'name' => bp_get_the_profile_field_input_name() . '_year'
    1809         ) ); ?>
    1810 
    1811         <div class="datebox">
    1812 
    1813             <label for="<?php bp_the_profile_field_input_name(); ?>_day"><?php bp_the_profile_field_name(); ?> <?php if ( bp_get_the_profile_field_is_required() ) : ?><?php esc_html_e( '(required)', 'buddypress' ); ?><?php endif; ?></label>
    1814             <?php
    1815 
    1816             /**
    1817              * Fires after field label and displays associated errors for the field.
    1818              *
    1819              * This is a dynamic hook that is dependent on the associated
    1820              * field ID. The hooks will be similar to `bp_field_12_errors`
    1821              * where the 12 is the field ID. Simply replace the 12 with
    1822              * your needed target ID.
    1823              *
    1824              * @since BuddyPress (1.8.0)
    1825              */
    1826             do_action( bp_get_the_profile_field_errors_action() ); ?>
    1827 
    1828             <select <?php echo $this->get_edit_field_html_elements( $day_r ); ?>>
    1829                 <?php bp_the_profile_field_options( array(
    1830                     'type'    => 'day',
    1831                     'user_id' => $user_id
    1832                 ) ); ?>
    1833             </select>
    1834 
    1835             <select <?php echo $this->get_edit_field_html_elements( $month_r ); ?>>
    1836                 <?php bp_the_profile_field_options( array(
    1837                     'type'    => 'month',
    1838                     'user_id' => $user_id
    1839                 ) ); ?>
    1840             </select>
    1841 
    1842             <select <?php echo $this->get_edit_field_html_elements( $year_r ); ?>>
    1843                 <?php bp_the_profile_field_options( array(
    1844                     'type'    => 'year',
    1845                     'user_id' => $user_id
    1846                 ) ); ?>
    1847             </select>
    1848 
    1849         </div>
    1850     <?php
    1851     }
    1852 
    1853     /**
    1854      * Output the edit field options HTML for this field type.
    1855      *
    1856      * BuddyPress considers a field's "options" to be, for example, the items in a selectbox.
    1857      * These are stored separately in the database, and their templating is handled separately.
    1858      *
    1859      * This templating is separate from {@link BP_XProfile_Field_Type::edit_field_html()} because
    1860      * it's also used in the wp-admin screens when creating new fields, and for backwards compatibility.
    1861      *
    1862      * Must be used inside the {@link bp_profile_fields()} template loop.
    1863      *
    1864      * @param array $args Optional. The arguments passed to {@link bp_the_profile_field_options()}.
    1865      * @since BuddyPress (2.0.0)
    1866      */
    1867     public function edit_field_options_html( array $args = array() ) {
    1868 
    1869         $date       = BP_XProfile_ProfileData::get_value_byid( $this->field_obj->id, $args['user_id'] );
    1870         $day        = 0;
    1871         $month      = 0;
    1872         $year       = 0;
    1873         $html       = '';
    1874         $eng_months = array( 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' );
    1875 
    1876         // Set day, month, year defaults
    1877         if ( ! empty( $date ) ) {
    1878 
    1879             // If Unix timestamp
    1880             if ( is_numeric( $date ) ) {
    1881                 $day   = date( 'j', $date );
    1882                 $month = date( 'F', $date );
    1883                 $year  = date( 'Y', $date );
    1884 
    1885             // If MySQL timestamp
    1886             } else {
    1887                 $day   = mysql2date( 'j', $date );
    1888                 $month = mysql2date( 'F', $date, false ); // Not localized, so that selected() works below
    1889                 $year  = mysql2date( 'Y', $date );
    1890             }
    1891         }
    1892 
    1893         // Check for updated posted values, and errors preventing them from
    1894         // being saved first time.
    1895         if ( ! empty( $_POST['field_' . $this->field_obj->id . '_day'] ) ) {
    1896             $new_day = (int) $_POST['field_' . $this->field_obj->id . '_day'];
    1897             $day     = ( $day != $new_day ) ? $new_day : $day;
    1898         }
    1899 
    1900         if ( ! empty( $_POST['field_' . $this->field_obj->id . '_month'] ) ) {
    1901             if ( in_array( $_POST['field_' . $this->field_obj->id . '_month'], $eng_months ) ) {
    1902                 $new_month = $_POST['field_' . $this->field_obj->id . '_month'];
    1903             } else {
    1904                 $new_month = $month;
    1905             }
    1906 
    1907             $month = ( $month !== $new_month ) ? $new_month : $month;
    1908         }
    1909 
    1910         if ( ! empty( $_POST['field_' . $this->field_obj->id . '_year'] ) ) {
    1911             $new_year = (int) $_POST['field_' . $this->field_obj->id . '_year'];
    1912             $year     = ( $year != $new_year ) ? $new_year : $year;
    1913         }
    1914 
    1915         // $type will be passed by calling function when needed
    1916         switch ( $args['type'] ) {
    1917             case 'day':
    1918                 $html = sprintf( '<option value="" %1$s>%2$s</option>', selected( $day, 0, false ), /* translators: no option picked in select box */ __( '----', 'buddypress' ) );
    1919 
    1920                 for ( $i = 1; $i < 32; ++$i ) {
    1921                     $html .= sprintf( '<option value="%1$s" %2$s>%3$s</option>', (int) $i, selected( $day, $i, false ), (int) $i );
    1922                 }
    1923             break;
    1924 
    1925             case 'month':
    1926                 $months = array(
    1927                     __( 'January',   'buddypress' ),
    1928                     __( 'February',  'buddypress' ),
    1929                     __( 'March',     'buddypress' ),
    1930                     __( 'April',     'buddypress' ),
    1931                     __( 'May',       'buddypress' ),
    1932                     __( 'June',      'buddypress' ),
    1933                     __( 'July',      'buddypress' ),
    1934                     __( 'August',    'buddypress' ),
    1935                     __( 'September', 'buddypress' ),
    1936                     __( 'October',   'buddypress' ),
    1937                     __( 'November',  'buddypress' ),
    1938                     __( 'December',  'buddypress' )
    1939                 );
    1940 
    1941                 $html = sprintf( '<option value="" %1$s>%2$s</option>', selected( $month, 0, false ), /* translators: no option picked in select box */ __( '----', 'buddypress' ) );
    1942 
    1943                 for ( $i = 0; $i < 12; ++$i ) {
    1944                     $html .= sprintf( '<option value="%1$s" %2$s>%3$s</option>', esc_attr( $eng_months[$i] ), selected( $month, $eng_months[$i], false ), $months[$i] );
    1945                 }
    1946             break;
    1947 
    1948             case 'year':
    1949                 $html = sprintf( '<option value="" %1$s>%2$s</option>', selected( $year, 0, false ), /* translators: no option picked in select box */ __( '----', 'buddypress' ) );
    1950 
    1951                 for ( $i = 2037; $i > 1901; $i-- ) {
    1952                     $html .= sprintf( '<option value="%1$s" %2$s>%3$s</option>', (int) $i, selected( $year, $i, false ), (int) $i );
    1953                 }
    1954             break;
    1955         }
    1956 
    1957         /**
    1958          * Filters the output for the profile field datebox.
    1959          *
    1960          * @since BuddyPress (1.1.0)
    1961          *
    1962          * @param string $html  HTML output for the field.
    1963          * @param string $value Which date type is being rendered for.
    1964          * @param string $day   Date formatted for the current day.
    1965          * @param string $month Date formatted for the current month.
    1966          * @param string $year  Date formatted for the current year.
    1967          * @param int    $id    ID of the field object being rendered.
    1968          * @param string $date  Current date.
    1969          */
    1970         echo apply_filters( 'bp_get_the_profile_field_datebox', $html, $args['type'], $day, $month, $year, $this->field_obj->id, $date );
    1971     }
    1972 
    1973     /**
    1974      * Output HTML for this field type on the wp-admin Profile Fields screen.
    1975      *
    1976      * Must be used inside the {@link bp_profile_fields()} template loop.
    1977      *
    1978      * @param array $raw_properties Optional key/value array of permitted attributes that you want to add.
    1979      * @since BuddyPress (2.0.0)
    1980      */
    1981     public function admin_field_html( array $raw_properties = array() ) {
    1982 
    1983         $day_r = bp_parse_args( $raw_properties, array(
    1984             'id'   => bp_get_the_profile_field_input_name() . '_day',
    1985             'name' => bp_get_the_profile_field_input_name() . '_day'
    1986         ) );
    1987 
    1988         $month_r = bp_parse_args( $raw_properties, array(
    1989             'id'   => bp_get_the_profile_field_input_name() . '_month',
    1990             'name' => bp_get_the_profile_field_input_name() . '_month'
    1991         ) );
    1992 
    1993         $year_r = bp_parse_args( $raw_properties, array(
    1994             'id'   => bp_get_the_profile_field_input_name() . '_year',
    1995             'name' => bp_get_the_profile_field_input_name() . '_year'
    1996         ) ); ?>
    1997 
    1998         <select <?php echo $this->get_edit_field_html_elements( $day_r ); ?>>
    1999             <?php bp_the_profile_field_options( array( 'type' => 'day' ) ); ?>
    2000         </select>
    2001 
    2002         <select <?php echo $this->get_edit_field_html_elements( $month_r ); ?>>
    2003             <?php bp_the_profile_field_options( array( 'type' => 'month' ) ); ?>
    2004         </select>
    2005 
    2006         <select <?php echo $this->get_edit_field_html_elements( $year_r ); ?>>
    2007             <?php bp_the_profile_field_options( array( 'type' => 'year' ) ); ?>
    2008         </select>
    2009 
    2010     <?php
    2011     }
    2012 
    2013     /**
    2014      * This method usually outputs HTML for this field type's children options on the wp-admin Profile Fields
    2015      * "Add Field" and "Edit Field" screens, but for this field type, we don't want it, so it's stubbed out.
    2016      *
    2017      * @param BP_XProfile_Field $current_field The current profile field on the add/edit screen.
    2018      * @param string $control_type Optional. HTML input type used to render the current field's child options.
    2019      * @since BuddyPress (2.0.0)
    2020      */
    2021     public function admin_new_field_html( BP_XProfile_Field $current_field, $control_type = '' ) {}
    2022 
    2023     /**
    2024      * Format Date values for display.
    2025      *
    2026      * @since BuddyPress (2.1.0)
    2027      *
    2028      * @param string $field_value The date value, as saved in the database.
    2029      *        Typically, this is a MySQL-formatted date string (Y-m-d H:i:s).
    2030      * @return string Date formatted by bp_format_time().
    2031      */
    2032     public static function display_filter( $field_value ) {
    2033 
    2034         // If Unix timestamp
    2035         if ( ! is_numeric( $field_value ) ) {
    2036             $field_value = strtotime( $field_value );
    2037         }
    2038 
    2039         return bp_format_time( $field_value, true, false );
    2040     }
    2041 }
    2042 
    2043 /**
    2044  * Checkbox xprofile field type.
    2045  *
    2046  * @since BuddyPress (2.0.0)
    2047  */
    2048 class BP_XProfile_Field_Type_Checkbox extends BP_XProfile_Field_Type {
    2049 
    2050     /**
    2051      * Constructor for the checkbox field type
    2052      *
    2053      * @since BuddyPress (2.0.0)
    2054      */
    2055     public function __construct() {
    2056         parent::__construct();
    2057 
    2058         $this->category = _x( 'Multi Fields', 'xprofile field type category', 'buddypress' );
    2059         $this->name     = _x( 'Checkboxes', 'xprofile field type', 'buddypress' );
    2060 
    2061         $this->supports_multiple_defaults = true;
    2062         $this->accepts_null_value         = true;
    2063         $this->supports_options           = true;
    2064 
    2065         $this->set_format( '/^.+$/', 'replace' );
    2066 
    2067         /**
    2068          * Fires inside __construct() method for BP_XProfile_Field_Type_Checkbox class.
    2069          *
    2070          * @since BuddyPress (2.0.0)
    2071          *
    2072          * @param BP_XProfile_Field_Type_Checkbox $this Current instance of
    2073          *                                              the field type checkbox.
    2074          */
    2075         do_action( 'bp_xprofile_field_type_checkbox', $this );
    2076     }
    2077 
    2078     /**
    2079      * Output the edit field HTML for this field type.
    2080      *
    2081      * Must be used inside the {@link bp_profile_fields()} template loop.
    2082      *
    2083      * @param array $raw_properties Optional key/value array of {@link http://dev.w3.org/html5/markup/input.checkbox.html permitted attributes} that you want to add.
    2084      * @since BuddyPress (2.0.0)
    2085      */
    2086     public function edit_field_html( array $raw_properties = array() ) {
    2087 
    2088         // user_id is a special optional parameter that we pass to
    2089         // {@link bp_the_profile_field_options()}.
    2090         if ( isset( $raw_properties['user_id'] ) ) {
    2091             $user_id = (int) $raw_properties['user_id'];
    2092             unset( $raw_properties['user_id'] );
    2093         } else {
    2094             $user_id = bp_displayed_user_id();
    2095         } ?>
    2096 
    2097         <div class="checkbox">
    2098             <label for="<?php bp_the_profile_field_input_name(); ?>">
    2099                 <?php bp_the_profile_field_name(); ?>
    2100                 <?php if ( bp_get_the_profile_field_is_required() ) : ?>
    2101                     <?php esc_html_e( '(required)', 'buddypress' ); ?>
    2102                 <?php endif; ?>
    2103             </label>
    2104 
    2105             <?php
    2106 
    2107             /** This action is documented in bp-xprofile/bp-xprofile-classes */
    2108             do_action( bp_get_the_profile_field_errors_action() ); ?>
    2109 
    2110             <?php bp_the_profile_field_options( array(
    2111                 'user_id' => $user_id
    2112             ) ); ?>
    2113 
    2114         </div>
    2115 
    2116         <?php
    2117     }
    2118 
    2119     /**
    2120      * Output the edit field options HTML for this field type.
    2121      *
    2122      * BuddyPress considers a field's "options" to be, for example, the items in a selectbox.
    2123      * These are stored separately in the database, and their templating is handled separately.
    2124      *
    2125      * This templating is separate from {@link BP_XProfile_Field_Type::edit_field_html()} because
    2126      * it's also used in the wp-admin screens when creating new fields, and for backwards compatibility.
    2127      *
    2128      * Must be used inside the {@link bp_profile_fields()} template loop.
    2129      *
    2130      * @param array $args Optional. The arguments passed to {@link bp_the_profile_field_options()}.
    2131      * @since BuddyPress (2.0.0)
    2132      */
    2133     public function edit_field_options_html( array $args = array() ) {
    2134         $options       = $this->field_obj->get_children();
    2135         $option_values = BP_XProfile_ProfileData::get_value_byid( $this->field_obj->id, $args['user_id'] );
    2136         $option_values = (array) maybe_unserialize( $option_values );
    2137 
    2138         $html = '';
    2139 
    2140         // Check for updated posted values, but errors preventing them from
    2141         // being saved first time
    2142         if ( isset( $_POST['field_' . $this->field_obj->id] ) && $option_values != maybe_serialize( $_POST['field_' . $this->field_obj->id] ) ) {
    2143             if ( ! empty( $_POST['field_' . $this->field_obj->id] ) ) {
    2144                 $option_values = array_map( 'sanitize_text_field', $_POST['field_' . $this->field_obj->id] );
    2145             }
    2146         }
    2147 
    2148         for ( $k = 0, $count = count( $options ); $k < $count; ++$k ) {
    2149             $selected = '';
    2150 
    2151             // First, check to see whether the user's saved values match the option
    2152             for ( $j = 0, $count_values = count( $option_values ); $j < $count_values; ++$j ) {
    2153 
    2154                 // Run the allowed option name through the before_save filter,
    2155                 // so we'll be sure to get a match
    2156                 $allowed_options = xprofile_sanitize_data_value_before_save( $options[$k]->name, false, false );
    2157 
    2158                 if ( $option_values[$j] === $allowed_options || in_array( $allowed_options, $option_values ) ) {
    2159                     $selected = ' checked="checked"';
    2160                     break;
    2161                 }
    2162             }
    2163 
    2164             // If the user has not yet supplied a value for this field, check to
    2165             // see whether there is a default value available
    2166             if ( ! is_array( $option_values ) && empty( $option_values ) && empty( $selected ) && ! empty( $options[$k]->is_default_option ) ) {
    2167                 $selected = ' checked="checked"';
    2168             }
    2169 
    2170             $new_html = sprintf( '<label><input %1$s type="checkbox" name="%2$s" id="%3$s" value="%4$s">%5$s</label>',
    2171                 $selected,
    2172                 esc_attr( "field_{$this->field_obj->id}[]" ),
    2173                 esc_attr( "field_{$options[$k]->id}_{$k}" ),
    2174                 esc_attr( stripslashes( $options[$k]->name ) ),
    2175                 esc_html( stripslashes( $options[$k]->name ) )
    2176             );
    2177 
    2178             /**
    2179              * Filters the HTML output for an individual field options checkbox.
    2180              *
    2181              * @since BuddyPress (1.1.0)
    2182              *
    2183              * @param string $new_html Label and checkbox input field.
    2184              * @param object $value    Current option being rendered for.
    2185              * @param int    $id       ID of the field object being rendered.
    2186              * @param string $selected Current selected value.
    2187              * @param string $k        Current index in the foreach loop.
    2188              */
    2189             $html .= apply_filters( 'bp_get_the_profile_field_options_checkbox', $new_html, $options[$k], $this->field_obj->id, $selected, $k );
    2190         }
    2191 
    2192         echo $html;
    2193     }
    2194 
    2195     /**
    2196      * Output HTML for this field type on the wp-admin Profile Fields screen.
    2197      *
    2198      * Must be used inside the {@link bp_profile_fields()} template loop.
    2199      *
    2200      * @param array $raw_properties Optional key/value array of permitted attributes that you want to add.
    2201      * @since BuddyPress (2.0.0)
    2202      */
    2203     public function admin_field_html( array $raw_properties = array() ) {
    2204         bp_the_profile_field_options();
    2205     }
    2206 
    2207     /**
    2208      * Output HTML for this field type's children options on the wp-admin Profile Fields "Add Field" and "Edit Field" screens.
    2209      *
    2210      * Must be used inside the {@link bp_profile_fields()} template loop.
    2211      *
    2212      * @param BP_XProfile_Field $current_field The current profile field on the add/edit screen.
    2213      * @param string $control_type Optional. HTML input type used to render the current field's child options.
    2214      * @since BuddyPress (2.0.0)
    2215      */
    2216     public function admin_new_field_html( BP_XProfile_Field $current_field, $control_type = '' ) {
    2217         parent::admin_new_field_html( $current_field, 'checkbox' );
    2218     }
    2219 }
    2220 
    2221 /**
    2222  * Radio button xprofile field type.
    2223  *
    2224  * @since BuddyPress (2.0.0)
    2225  */
    2226 class BP_XProfile_Field_Type_Radiobutton extends BP_XProfile_Field_Type {
    2227 
    2228     /**
    2229      * Constructor for the radio button field type
    2230      *
    2231      * @since BuddyPress (2.0.0)
    2232      */
    2233     public function __construct() {
    2234         parent::__construct();
    2235 
    2236         $this->category = _x( 'Multi Fields', 'xprofile field type category', 'buddypress' );
    2237         $this->name     = _x( 'Radio Buttons', 'xprofile field type', 'buddypress' );
    2238 
    2239         $this->supports_options = true;
    2240 
    2241         $this->set_format( '/^.+$/', 'replace' );
    2242 
    2243         /**
    2244          * Fires inside __construct() method for BP_XProfile_Field_Type_Radiobutton class.
    2245          *
    2246          * @since BuddyPress (2.0.0)
    2247          *
    2248          * @param BP_XProfile_Field_Type_Radiobutton $this Current instance of
    2249          *                                                 the field type radio button.
    2250          */
    2251         do_action( 'bp_xprofile_field_type_radiobutton', $this );
    2252     }
    2253 
    2254     /**
    2255      * Output the edit field HTML for this field type.
    2256      *
    2257      * Must be used inside the {@link bp_profile_fields()} template loop.
    2258      *
    2259      * @param array $raw_properties Optional key/value array of {@link http://dev.w3.org/html5/markup/input.radio.html permitted attributes} that you want to add.
    2260      * @since BuddyPress (2.0.0)
    2261      */
    2262     public function edit_field_html( array $raw_properties = array() ) {
    2263 
    2264         // user_id is a special optional parameter that we pass to
    2265         // {@link bp_the_profile_field_options()}.
    2266         if ( isset( $raw_properties['user_id'] ) ) {
    2267             $user_id = (int) $raw_properties['user_id'];
    2268             unset( $raw_properties['user_id'] );
    2269         } else {
    2270             $user_id = bp_displayed_user_id();
    2271         } ?>
    2272 
    2273         <div class="radio">
    2274 
    2275             <label for="<?php bp_the_profile_field_input_name(); ?>">
    2276                 <?php bp_the_profile_field_name(); ?>
    2277                 <?php if ( bp_get_the_profile_field_is_required() ) : ?>
    2278                     <?php esc_html_e( '(required)', 'buddypress' ); ?>
    2279                 <?php endif; ?>
    2280             </label>
    2281 
    2282             <?php
    2283 
    2284             /** This action is documented in bp-xprofile/bp-xprofile-classes */
    2285             do_action( bp_get_the_profile_field_errors_action() ); ?>
    2286 
    2287             <?php bp_the_profile_field_options( array( 'user_id' => $user_id ) );
    2288 
    2289             if ( ! bp_get_the_profile_field_is_required() ) : ?>
    2290 
    2291                 <a class="clear-value" href="javascript:clear( '<?php echo esc_js( bp_get_the_profile_field_input_name() ); ?>' );">
    2292                     <?php esc_html_e( 'Clear', 'buddypress' ); ?>
    2293                 </a>
    2294 
    2295             <?php endif; ?>
    2296 
    2297         </div>
    2298 
    2299         <?php
    2300     }
    2301 
    2302     /**
    2303      * Output the edit field options HTML for this field type.
    2304      *
    2305      * BuddyPress considers a field's "options" to be, for example, the items in a selectbox.
    2306      * These are stored separately in the database, and their templating is handled separately.
    2307      *
    2308      * This templating is separate from {@link BP_XProfile_Field_Type::edit_field_html()} because
    2309      * it's also used in the wp-admin screens when creating new fields, and for backwards compatibility.
    2310      *
    2311      * Must be used inside the {@link bp_profile_fields()} template loop.
    2312      *
    2313      * @param array $args Optional. The arguments passed to {@link bp_the_profile_field_options()}.
    2314      * @since BuddyPress (2.0.0)
    2315      */
    2316     public function edit_field_options_html( array $args = array() ) {
    2317         $option_value = BP_XProfile_ProfileData::get_value_byid( $this->field_obj->id, $args['user_id'] );
    2318         $options      = $this->field_obj->get_children();
    2319 
    2320         $html = sprintf( '<div id="%s">', esc_attr( 'field_' . $this->field_obj->id ) );
    2321 
    2322         for ( $k = 0, $count = count( $options ); $k < $count; ++$k ) {
    2323 
    2324             // Check for updated posted values, but errors preventing them from
    2325             // being saved first time
    2326             if ( isset( $_POST['field_' . $this->field_obj->id] ) && $option_value != $_POST['field_' . $this->field_obj->id] ) {
    2327                 if ( ! empty( $_POST['field_' . $this->field_obj->id] ) ) {
    2328                     $option_value = sanitize_text_field( $_POST['field_' . $this->field_obj->id] );
    2329                 }
    2330             }
    2331 
    2332             // Run the allowed option name through the before_save filter, so
    2333             // we'll be sure to get a match
    2334             $allowed_options = xprofile_sanitize_data_value_before_save( $options[$k]->name, false, false );
    2335             $selected        = '';
    2336 
    2337             if ( $option_value === $allowed_options || ( empty( $option_value ) && ! empty( $options[$k]->is_default_option ) ) ) {
    2338                 $selected = ' checked="checked"';
    2339             }
    2340 
    2341             $new_html = sprintf( '<label><input %1$s type="radio" name="%2$s" id="%3$s" value="%4$s">%5$s</label>',
    2342                 $selected,
    2343                 esc_attr( "field_{$this->field_obj->id}" ),
    2344                 esc_attr( "option_{$options[$k]->id}" ),
    2345                 esc_attr( stripslashes( $options[$k]->name ) ),
    2346                 esc_html( stripslashes( $options[$k]->name ) )
    2347             );
    2348 
    2349             /**
    2350              * Filters the HTML output for an individual field options radio button.
    2351              *
    2352              * @since BuddyPress (1.1.0)
    2353              *
    2354              * @param string $new_html Label and radio input field.
    2355              * @param object $value    Current option being rendered for.
    2356              * @param int    $id       ID of the field object being rendered.
    2357              * @param string $selected Current selected value.
    2358              * @param string $k        Current index in the foreach loop.
    2359              */
    2360             $html .= apply_filters( 'bp_get_the_profile_field_options_radio', $new_html, $options[$k], $this->field_obj->id, $selected, $k );
    2361         }
    2362 
    2363         echo $html . '</div>';
    2364     }
    2365 
    2366     /**
    2367      * Output HTML for this field type on the wp-admin Profile Fields screen.
    2368      *
    2369      * Must be used inside the {@link bp_profile_fields()} template loop.
    2370      *
    2371      * @param array $raw_properties Optional key/value array of permitted attributes that you want to add.
    2372      * @since BuddyPress (2.0.0)
    2373      */
    2374     public function admin_field_html( array $raw_properties = array() ) {
    2375         bp_the_profile_field_options();
    2376 
    2377         if ( bp_get_the_profile_field_is_required() ) {
    2378             return;
    2379         } ?>
    2380 
    2381         <a class="clear-value" href="javascript:clear( '<?php echo esc_js( bp_get_the_profile_field_input_name() ); ?>' );">
    2382             <?php esc_html_e( 'Clear', 'buddypress' ); ?>
    2383         </a>
    2384 
    2385         <?php
    2386     }
    2387 
    2388     /**
    2389      * Output HTML for this field type's children options on the wp-admin Profile Fields "Add Field" and "Edit Field" screens.
    2390      *
    2391      * Must be used inside the {@link bp_profile_fields()} template loop.
    2392      *
    2393      * @param BP_XProfile_Field $current_field The current profile field on the add/edit screen.
    2394      * @param string $control_type Optional. HTML input type used to render the current field's child options.
    2395      * @since BuddyPress (2.0.0)
    2396      */
    2397     public function admin_new_field_html( BP_XProfile_Field $current_field, $control_type = '' ) {
    2398         parent::admin_new_field_html( $current_field, 'radio' );
    2399     }
    2400 }
    2401 
    2402 /**
    2403  * Multi-selectbox xprofile field type.
    2404  *
    2405  * @since BuddyPress (2.0.0)
    2406  */
    2407 class BP_XProfile_Field_Type_Multiselectbox extends BP_XProfile_Field_Type {
    2408 
    2409     /**
    2410      * Constructor for the multi-selectbox field type
    2411      *
    2412      * @since BuddyPress (2.0.0)
    2413      */
    2414     public function __construct() {
    2415         parent::__construct();
    2416 
    2417         $this->category = _x( 'Multi Fields', 'xprofile field type category', 'buddypress' );
    2418         $this->name     = _x( 'Multi Select Box', 'xprofile field type', 'buddypress' );
    2419 
    2420         $this->supports_multiple_defaults = true;
    2421         $this->accepts_null_value         = true;
    2422         $this->supports_options           = true;
    2423 
    2424         $this->set_format( '/^.+$/', 'replace' );
    2425 
    2426         /**
    2427          * Fires inside __construct() method for BP_XProfile_Field_Type_Multiselectbox class.
    2428          *
    2429          * @since BuddyPress (2.0.0)
    2430          *
    2431          * @param BP_XProfile_Field_Type_Multiselectbox $this Current instance of
    2432          *                                                    the field type multiple select box.
    2433          */
    2434         do_action( 'bp_xprofile_field_type_multiselectbox', $this );
    2435     }
    2436 
    2437     /**
    2438      * Output the edit field HTML for this field type.
    2439      *
    2440      * Must be used inside the {@link bp_profile_fields()} template loop.
    2441      *
    2442      * @param array $raw_properties Optional key/value array of {@link http://dev.w3.org/html5/markup/select.html permitted attributes} that you want to add.
    2443      * @since BuddyPress (2.0.0)
    2444      */
    2445     public function edit_field_html( array $raw_properties = array() ) {
    2446 
    2447         // user_id is a special optional parameter that we pass to
    2448         // {@link bp_the_profile_field_options()}.
    2449         if ( isset( $raw_properties['user_id'] ) ) {
    2450             $user_id = (int) $raw_properties['user_id'];
    2451             unset( $raw_properties['user_id'] );
    2452         } else {
    2453             $user_id = bp_displayed_user_id();
    2454         }
    2455 
    2456         $r = bp_parse_args( $raw_properties, array(
    2457             'multiple' => 'multiple',
    2458             'id'       => bp_get_the_profile_field_input_name() . '[]',
    2459             'name'     => bp_get_the_profile_field_input_name() . '[]',
    2460         ) ); ?>
    2461 
    2462         <label for="<?php bp_the_profile_field_input_name(); ?>[]"><?php bp_the_profile_field_name(); ?> <?php if ( bp_get_the_profile_field_is_required() ) : ?><?php _e( '(required)', 'buddypress' ); ?><?php endif; ?></label>
    2463 
    2464         <?php
    2465 
    2466         /** This action is documented in bp-xprofile/bp-xprofile-classes */
    2467         do_action( bp_get_the_profile_field_errors_action() ); ?>
    2468 
    2469         <select <?php echo $this->get_edit_field_html_elements( $r ); ?>>
    2470             <?php bp_the_profile_field_options( array(
    2471                 'user_id' => $user_id
    2472             ) ); ?>
    2473         </select>
    2474 
    2475         <?php if ( ! bp_get_the_profile_field_is_required() ) : ?>
    2476 
    2477             <a class="clear-value" href="javascript:clear( '<?php echo esc_js( bp_get_the_profile_field_input_name() ); ?>[]' );">
    2478                 <?php esc_html_e( 'Clear', 'buddypress' ); ?>
    2479             </a>
    2480 
    2481         <?php endif; ?>
    2482     <?php
    2483     }
    2484 
    2485     /**
    2486      * Output the edit field options HTML for this field type.
    2487      *
    2488      * BuddyPress considers a field's "options" to be, for example, the items in a selectbox.
    2489      * These are stored separately in the database, and their templating is handled separately.
    2490      *
    2491      * This templating is separate from {@link BP_XProfile_Field_Type::edit_field_html()} because
    2492      * it's also used in the wp-admin screens when creating new fields, and for backwards compatibility.
    2493      *
    2494      * Must be used inside the {@link bp_profile_fields()} template loop.
    2495      *
    2496      * @param array $args Optional. The arguments passed to {@link bp_the_profile_field_options()}.
    2497      * @since BuddyPress (2.0.0)
    2498      */
    2499     public function edit_field_options_html( array $args = array() ) {
    2500         $original_option_values = maybe_unserialize( BP_XProfile_ProfileData::get_value_byid( $this->field_obj->id, $args['user_id'] ) );
    2501 
    2502         $options = $this->field_obj->get_children();
    2503         $html    = '';
    2504 
    2505         if ( empty( $original_option_values ) && ! empty( $_POST['field_' . $this->field_obj->id] ) ) {
    2506             $original_option_values = sanitize_text_field( $_POST['field_' . $this->field_obj->id] );
    2507         }
    2508 
    2509         $option_values = (array) $original_option_values;
    2510         for ( $k = 0, $count = count( $options ); $k < $count; ++$k ) {
    2511             $selected = '';
    2512 
    2513             // Check for updated posted values, but errors preventing them from
    2514             // being saved first time
    2515             foreach( $option_values as $i => $option_value ) {
    2516                 if ( isset( $_POST['field_' . $this->field_obj->id] ) && $_POST['field_' . $this->field_obj->id][$i] != $option_value ) {
    2517                     if ( ! empty( $_POST['field_' . $this->field_obj->id][$i] ) ) {
    2518                         $option_values[] = sanitize_text_field( $_POST['field_' . $this->field_obj->id][$i] );
    2519                     }
    2520                 }
    2521             }
    2522 
    2523             // Run the allowed option name through the before_save filter, so
    2524             // we'll be sure to get a match
    2525             $allowed_options = xprofile_sanitize_data_value_before_save( $options[$k]->name, false, false );
    2526 
    2527             // First, check to see whether the user-entered value matches
    2528             if ( in_array( $allowed_options, $option_values ) ) {
    2529                 $selected = ' selected="selected"';
    2530             }
    2531 
    2532             // Then, if the user has not provided a value, check for defaults
    2533             if ( ! is_array( $original_option_values ) && empty( $option_values ) && ! empty( $options[$k]->is_default_option ) ) {
    2534                 $selected = ' selected="selected"';
    2535             }
    2536 
    2537             /**
    2538              * Filters the HTML output for options in a multiselect input.
    2539              *
    2540              * @since BuddyPress (1.5.0)
    2541              *
    2542              * @param string $value    Option tag for current value being rendered.
    2543              * @param object $value    Current option being rendered for.
    2544              * @param int    $id       ID of the field object being rendered.
    2545              * @param string $selected Current selected value.
    2546              * @param string $k        Current index in the foreach loop.
    2547              */
    2548             $html .= apply_filters( 'bp_get_the_profile_field_options_multiselect', '<option' . $selected . ' value="' . esc_attr( stripslashes( $options[$k]->name ) ) . '">' . esc_html( stripslashes( $options[$k]->name ) ) . '</option>', $options[$k], $this->field_obj->id, $selected, $k );
    2549         }
    2550 
    2551         echo $html;
    2552     }
    2553 
    2554     /**
    2555      * Output HTML for this field type on the wp-admin Profile Fields screen.
    2556      *
    2557      * Must be used inside the {@link bp_profile_fields()} template loop.
    2558      *
    2559      * @param array $raw_properties Optional key/value array of permitted attributes that you want to add.
    2560      * @since BuddyPress (2.0.0)
    2561      */
    2562     public function admin_field_html( array $raw_properties = array() ) {
    2563         $r = bp_parse_args( $raw_properties, array(
    2564             'multiple' => 'multiple'
    2565         ) ); ?>
    2566 
    2567         <select <?php echo $this->get_edit_field_html_elements( $r ); ?>>
    2568             <?php bp_the_profile_field_options(); ?>
    2569         </select>
    2570 
    2571         <?php
    2572     }
    2573 
    2574     /**
    2575      * Output HTML for this field type's children options on the wp-admin Profile Fields "Add Field" and "Edit Field" screens.
    2576      *
    2577      * Must be used inside the {@link bp_profile_fields()} template loop.
    2578      *
    2579      * @param BP_XProfile_Field $current_field The current profile field on the add/edit screen.
    2580      * @param string $control_type Optional. HTML input type used to render the current field's child options.
    2581      * @since BuddyPress (2.0.0)
    2582      */
    2583     public function admin_new_field_html( BP_XProfile_Field $current_field, $control_type = '' ) {
    2584         parent::admin_new_field_html( $current_field, 'checkbox' );
    2585     }
    2586 }
    2587 
    2588 /**
    2589  * Selectbox xprofile field type.
    2590  *
    2591  * @since BuddyPress (2.0.0)
    2592  */
    2593 class BP_XProfile_Field_Type_Selectbox extends BP_XProfile_Field_Type {
    2594 
    2595     /**
    2596      * Constructor for the selectbox field type
    2597      *
    2598      * @since BuddyPress (2.0.0)
    2599      */
    2600     public function __construct() {
    2601         parent::__construct();
    2602 
    2603         $this->category = _x( 'Multi Fields', 'xprofile field type category', 'buddypress' );
    2604         $this->name     = _x( 'Drop Down Select Box', 'xprofile field type', 'buddypress' );
    2605 
    2606         $this->supports_options = true;
    2607 
    2608         $this->set_format( '/^.+$/', 'replace' );
    2609 
    2610         /**
    2611          * Fires inside __construct() method for BP_XProfile_Field_Type_Selectbox class.
    2612          *
    2613          * @since BuddyPress (2.0.0)
    2614          *
    2615          * @param BP_XProfile_Field_Type_Selectbox $this Current instance of
    2616          *                                               the field type select box.
    2617          */
    2618         do_action( 'bp_xprofile_field_type_selectbox', $this );
    2619     }
    2620 
    2621     /**
    2622      * Output the edit field HTML for this field type.
    2623      *
    2624      * Must be used inside the {@link bp_profile_fields()} template loop.
    2625      *
    2626      * @param array $raw_properties Optional key/value array of {@link http://dev.w3.org/html5/markup/select.html permitted attributes} that you want to add.
    2627      * @since BuddyPress (2.0.0)
    2628      */
    2629     public function edit_field_html( array $raw_properties = array() ) {
    2630 
    2631         // user_id is a special optional parameter that we pass to
    2632         // {@link bp_the_profile_field_options()}.
    2633         if ( isset( $raw_properties['user_id'] ) ) {
    2634             $user_id = (int) $raw_properties['user_id'];
    2635             unset( $raw_properties['user_id'] );
    2636         } else {
    2637             $user_id = bp_displayed_user_id();
    2638         } ?>
    2639 
    2640         <label for="<?php bp_the_profile_field_input_name(); ?>">
    2641             <?php bp_the_profile_field_name(); ?>
    2642             <?php if ( bp_get_the_profile_field_is_required() ) : ?>
    2643                 <?php esc_html_e( '(required)', 'buddypress' ); ?>
    2644             <?php endif; ?>
    2645         </label>
    2646 
    2647         <?php
    2648 
    2649         /** This action is documented in bp-xprofile/bp-xprofile-classes */
    2650         do_action( bp_get_the_profile_field_errors_action() ); ?>
    2651 
    2652         <select <?php echo $this->get_edit_field_html_elements( $raw_properties ); ?>>
    2653             <?php bp_the_profile_field_options( array( 'user_id' => $user_id ) ); ?>
    2654         </select>
    2655 
    2656         <?php
    2657     }
    2658 
    2659     /**
    2660      * Output the edit field options HTML for this field type.
    2661      *
    2662      * BuddyPress considers a field's "options" to be, for example, the items in a selectbox.
    2663      * These are stored separately in the database, and their templating is handled separately.
    2664      *
    2665      * This templating is separate from {@link BP_XProfile_Field_Type::edit_field_html()} because
    2666      * it's also used in the wp-admin screens when creating new fields, and for backwards compatibility.
    2667      *
    2668      * Must be used inside the {@link bp_profile_fields()} template loop.
    2669      *
    2670      * @param array $args Optional. The arguments passed to {@link bp_the_profile_field_options()}.
    2671      * @since BuddyPress (2.0.0)
    2672      */
    2673     public function edit_field_options_html( array $args = array() ) {
    2674         $original_option_values = maybe_unserialize( BP_XProfile_ProfileData::get_value_byid( $this->field_obj->id, $args['user_id'] ) );
    2675 
    2676         $options = $this->field_obj->get_children();
    2677         $html    = '<option value="">' . /* translators: no option picked in select box */ esc_html__( '----', 'buddypress' ) . '</option>';
    2678 
    2679         if ( empty( $original_option_values ) && !empty( $_POST['field_' . $this->field_obj->id] ) ) {
    2680             $original_option_values = sanitize_text_field(  $_POST['field_' . $this->field_obj->id] );
    2681         }
    2682 
    2683         $option_values = (array) $original_option_values;
    2684         for ( $k = 0, $count = count( $options ); $k < $count; ++$k ) {
    2685             $selected = '';
    2686 
    2687             // Check for updated posted values, but errors preventing them from
    2688             // being saved first time
    2689             foreach( $option_values as $i => $option_value ) {
    2690                 if ( isset( $_POST['field_' . $this->field_obj->id] ) && $_POST['field_' . $this->field_obj->id] != $option_value ) {
    2691                     if ( ! empty( $_POST['field_' . $this->field_obj->id] ) ) {
    2692                         $option_values[$i] = sanitize_text_field( $_POST['field_' . $this->field_obj->id] );
    2693                     }
    2694                 }
    2695             }
    2696 
    2697             // Run the allowed option name through the before_save filter, so
    2698             // we'll be sure to get a match
    2699             $allowed_options = xprofile_sanitize_data_value_before_save( $options[$k]->name, false, false );
    2700 
    2701             // First, check to see whether the user-entered value matches
    2702             if ( in_array( $allowed_options, $option_values ) ) {
    2703                 $selected = ' selected="selected"';
    2704             }
    2705 
    2706             // Then, if the user has not provided a value, check for defaults
    2707             if ( ! is_array( $original_option_values ) && empty( $option_values ) && $options[$k]->is_default_option ) {
    2708                 $selected = ' selected="selected"';
    2709             }
    2710 
    2711             /**
    2712              * Filters the HTML output for options in a select input.
    2713              *
    2714              * @since BuddyPress (1.1.0)
    2715              *
    2716              * @param string $value    Option tag for current value being rendered.
    2717              * @param object $value    Current option being rendered for.
    2718              * @param int    $id       ID of the field object being rendered.
    2719              * @param string $selected Current selected value.
    2720              * @param string $k        Current index in the foreach loop.
    2721              */
    2722             $html .= apply_filters( 'bp_get_the_profile_field_options_select', '<option' . $selected . ' value="' . esc_attr( stripslashes( $options[$k]->name ) ) . '">' . esc_html( stripslashes( $options[$k]->name ) ) . '</option>', $options[$k], $this->field_obj->id, $selected, $k );
    2723         }
    2724 
    2725         echo $html;
    2726     }
    2727 
    2728     /**
    2729      * Output HTML for this field type on the wp-admin Profile Fields screen.
    2730      *
    2731      * Must be used inside the {@link bp_profile_fields()} template loop.
    2732      *
    2733      * @param array $raw_properties Optional key/value array of permitted attributes that you want to add.
    2734      * @since BuddyPress (2.0.0)
    2735      */
    2736     public function admin_field_html( array $raw_properties = array() ) {
    2737         ?>
    2738 
    2739         <select <?php echo $this->get_edit_field_html_elements( $raw_properties ); ?>>
    2740             <?php bp_the_profile_field_options(); ?>
    2741         </select>
    2742 
    2743         <?php
    2744     }
    2745 
    2746     /**
    2747      * Output HTML for this field type's children options on the wp-admin Profile Fields "Add Field" and "Edit Field" screens.
    2748      *
    2749      * Must be used inside the {@link bp_profile_fields()} template loop.
    2750      *
    2751      * @param BP_XProfile_Field $current_field The current profile field on the add/edit screen.
    2752      * @param string $control_type Optional. HTML input type used to render the current field's child options.
    2753      * @since BuddyPress (2.0.0)
    2754      */
    2755     public function admin_new_field_html( BP_XProfile_Field $current_field, $control_type = '' ) {
    2756         parent::admin_new_field_html( $current_field, 'radio' );
    2757     }
    2758 }
    2759 
    2760 /**
    2761  * Textarea xprofile field type.
    2762  *
    2763  * @since BuddyPress (2.0.0)
    2764  */
    2765 class BP_XProfile_Field_Type_Textarea extends BP_XProfile_Field_Type {
    2766 
    2767     /**
    2768      * Constructor for the textarea field type
    2769      *
    2770      * @since BuddyPress (2.0.0)
    2771      */
    2772     public function __construct() {
    2773         parent::__construct();
    2774 
    2775         $this->category = _x( 'Single Fields', 'xprofile field type category', 'buddypress' );
    2776         $this->name     = _x( 'Multi-line Text Area', 'xprofile field type', 'buddypress' );
    2777 
    2778         $this->set_format( '/^.*$/m', 'replace' );
    2779 
    2780         /**
    2781          * Fires inside __construct() method for BP_XProfile_Field_Type_Textarea class.
    2782          *
    2783          * @since BuddyPress (2.0.0)
    2784          *
    2785          * @param BP_XProfile_Field_Type_Textarea $this Current instance of
    2786          *                                              the field type textarea.
    2787          */
    2788         do_action( 'bp_xprofile_field_type_textarea', $this );
    2789     }
    2790 
    2791     /**
    2792      * Output the edit field HTML for this field type.
    2793      *
    2794      * Must be used inside the {@link bp_profile_fields()} template loop.
    2795      *
    2796      * @param array $raw_properties Optional key/value array of {@link http://dev.w3.org/html5/markup/textarea.html permitted attributes} that you want to add.
    2797      * @since BuddyPress (2.0.0)
    2798      */
    2799     public function edit_field_html( array $raw_properties = array() ) {
    2800 
    2801         // user_id is a special optional parameter that certain other fields
    2802         // types pass to {@link bp_the_profile_field_options()}.
    2803         if ( isset( $raw_properties['user_id'] ) ) {
    2804             unset( $raw_properties['user_id'] );
    2805         }
    2806 
    2807         $r = bp_parse_args( $raw_properties, array(
    2808             'cols' => 40,
    2809             'rows' => 5,
    2810         ) ); ?>
    2811 
    2812         <label for="<?php bp_the_profile_field_input_name(); ?>">
    2813             <?php bp_the_profile_field_name(); ?>
    2814             <?php if ( bp_get_the_profile_field_is_required() ) : ?>
    2815                 <?php esc_html_e( '(required)', 'buddypress' ); ?>
    2816             <?php endif; ?>
    2817         </label>
    2818 
    2819         <?php
    2820 
    2821         /** This action is documented in bp-xprofile/bp-xprofile-classes */
    2822         do_action( bp_get_the_profile_field_errors_action() ); ?>
    2823 
    2824         <textarea <?php echo $this->get_edit_field_html_elements( $r ); ?>><?php bp_the_profile_field_edit_value(); ?></textarea>
    2825 
    2826         <?php
    2827     }
    2828 
    2829     /**
    2830      * Output HTML for this field type on the wp-admin Profile Fields screen.
    2831      *
    2832      * Must be used inside the {@link bp_profile_fields()} template loop.
    2833      *
    2834      * @param array $raw_properties Optional key/value array of permitted attributes that you want to add.
    2835      * @since BuddyPress (2.0.0)
    2836      */
    2837     public function admin_field_html( array $raw_properties = array() ) {
    2838         $r = bp_parse_args( $raw_properties, array(
    2839             'cols' => 40,
    2840             'rows' => 5,
    2841         ) ); ?>
    2842 
    2843         <textarea <?php echo $this->get_edit_field_html_elements( $r ); ?>></textarea>
    2844 
    2845         <?php
    2846     }
    2847 
    2848     /**
    2849      * This method usually outputs HTML for this field type's children options on the wp-admin Profile Fields
    2850      * "Add Field" and "Edit Field" screens, but for this field type, we don't want it, so it's stubbed out.
    2851      *
    2852      * @param BP_XProfile_Field $current_field The current profile field on the add/edit screen.
    2853      * @param string $control_type Optional. HTML input type used to render the current field's child options.
    2854      * @since BuddyPress (2.0.0)
    2855      */
    2856     public function admin_new_field_html( BP_XProfile_Field $current_field, $control_type = '' ) {}
    2857 }
    2858 
    2859 /**
    2860  * Textbox xprofile field type.
    2861  *
    2862  * @since BuddyPress (2.0.0)
    2863  */
    2864 class BP_XProfile_Field_Type_Textbox extends BP_XProfile_Field_Type {
    2865 
    2866     /**
    2867      * Constructor for the textbox field type
    2868      *
    2869      * @since BuddyPress (2.0.0)
    2870      */
    2871     public function __construct() {
    2872         parent::__construct();
    2873 
    2874         $this->category = _x( 'Single Fields', 'xprofile field type category', 'buddypress' );
    2875         $this->name     = _x( 'Text Box', 'xprofile field type', 'buddypress' );
    2876 
    2877         $this->set_format( '/^.*$/', 'replace' );
    2878 
    2879         /**
    2880          * Fires inside __construct() method for BP_XProfile_Field_Type_Textbox class.
    2881          *
    2882          * @since BuddyPress (2.0.0)
    2883          *
    2884          * @param BP_XProfile_Field_Type_Textbox $this Current instance of
    2885          *                                             the field type text box.
    2886          */
    2887         do_action( 'bp_xprofile_field_type_textbox', $this );
    2888     }
    2889 
    2890     /**
    2891      * Output the edit field HTML for this field type.
    2892      *
    2893      * Must be used inside the {@link bp_profile_fields()} template loop.
    2894      *
    2895      * @param array $raw_properties Optional key/value array of {@link http://dev.w3.org/html5/markup/input.text.html permitted attributes} that you want to add.
    2896      * @since BuddyPress (2.0.0)
    2897      */
    2898     public function edit_field_html( array $raw_properties = array() ) {
    2899 
    2900         // user_id is a special optional parameter that certain other fields
    2901         // types pass to {@link bp_the_profile_field_options()}.
    2902         if ( isset( $raw_properties['user_id'] ) ) {
    2903             unset( $raw_properties['user_id'] );
    2904         }
    2905 
    2906         $r = bp_parse_args( $raw_properties, array(
    2907             'type'  => 'text',
    2908             'value' => bp_get_the_profile_field_edit_value(),
    2909         ) ); ?>
    2910 
    2911         <label for="<?php bp_the_profile_field_input_name(); ?>">
    2912             <?php bp_the_profile_field_name(); ?>
    2913             <?php if ( bp_get_the_profile_field_is_required() ) : ?>
    2914                 <?php esc_html_e( '(required)', 'buddypress' ); ?>
    2915             <?php endif; ?>
    2916         </label>
    2917 
    2918         <?php
    2919 
    2920         /** This action is documented in bp-xprofile/bp-xprofile-classes */
    2921         do_action( bp_get_the_profile_field_errors_action() ); ?>
    2922 
    2923         <input <?php echo $this->get_edit_field_html_elements( $r ); ?>>
    2924 
    2925         <?php
    2926     }
    2927 
    2928     /**
    2929      * Output HTML for this field type on the wp-admin Profile Fields screen.
    2930      *
    2931      * Must be used inside the {@link bp_profile_fields()} template loop.
    2932      *
    2933      * @param array $raw_properties Optional key/value array of permitted attributes that you want to add.
    2934      * @since BuddyPress (2.0.0)
    2935      */
    2936     public function admin_field_html( array $raw_properties = array() ) {
    2937 
    2938         $r = bp_parse_args( $raw_properties, array(
    2939             'type' => 'text'
    2940         ) ); ?>
    2941 
    2942         <input <?php echo $this->get_edit_field_html_elements( $r ); ?>>
    2943 
    2944         <?php
    2945     }
    2946 
    2947     /**
    2948      * This method usually outputs HTML for this field type's children options on the wp-admin Profile Fields
    2949      * "Add Field" and "Edit Field" screens, but for this field type, we don't want it, so it's stubbed out.
    2950      *
    2951      * @param BP_XProfile_Field $current_field The current profile field on the add/edit screen.
    2952      * @param string $control_type Optional. HTML input type used to render the current field's child options.
    2953      * @since BuddyPress (2.0.0)
    2954      */
    2955     public function admin_new_field_html( BP_XProfile_Field $current_field, $control_type = '' ) {}
    2956 }
    2957 
    2958 /**
    2959  * Number xprofile field type.
    2960  *
    2961  * @since BuddyPress (2.0.0)
    2962  */
    2963 class BP_XProfile_Field_Type_Number extends BP_XProfile_Field_Type {
    2964 
    2965     /**
    2966      * Constructor for the number field type
    2967      *
    2968      * @since BuddyPress (2.0.0)
    2969      */
    2970     public function __construct() {
    2971         parent::__construct();
    2972 
    2973         $this->category = _x( 'Single Fields', 'xprofile field type category', 'buddypress' );
    2974         $this->name     = _x( 'Number', 'xprofile field type', 'buddypress' );
    2975 
    2976         $this->set_format( '/^\d+|-\d+$/', 'replace' );
    2977 
    2978         /**
    2979          * Fires inside __construct() method for BP_XProfile_Field_Type_Number class.
    2980          *
    2981          * @since BuddyPress (2.0.0)
    2982          *
    2983          * @param BP_XProfile_Field_Type_Number $this Current instance of
    2984          *                                            the field type number.
    2985          */
    2986         do_action( 'bp_xprofile_field_type_number', $this );
    2987     }
    2988 
    2989     /**
    2990      * Output the edit field HTML for this field type.
    2991      *
    2992      * Must be used inside the {@link bp_profile_fields()} template loop.
    2993      *
    2994      * @param array $raw_properties Optional key/value array of {@link http://dev.w3.org/html5/markup/input.number.html permitted attributes} that you want to add.
    2995      * @since BuddyPress (2.0.0)
    2996      */
    2997     public function edit_field_html( array $raw_properties = array() ) {
    2998 
    2999         // user_id is a special optional parameter that certain other fields
    3000         // types pass to {@link bp_the_profile_field_options()}.
    3001         if ( isset( $raw_properties['user_id'] ) ) {
    3002             unset( $raw_properties['user_id'] );
    3003         }
    3004 
    3005         $r = bp_parse_args( $raw_properties, array(
    3006             'type'  => 'number',
    3007             'value' =>  bp_get_the_profile_field_edit_value()
    3008         ) ); ?>
    3009 
    3010         <label for="<?php bp_the_profile_field_input_name(); ?>">
    3011             <?php bp_the_profile_field_name(); ?>
    3012             <?php if ( bp_get_the_profile_field_is_required() ) : ?>
    3013                 <?php esc_html_e( '(required)', 'buddypress' ); ?>
    3014             <?php endif; ?>
    3015         </label>
    3016 
    3017         <?php
    3018 
    3019         /** This action is documented in bp-xprofile/bp-xprofile-classes */
    3020         do_action( bp_get_the_profile_field_errors_action() ); ?>
    3021 
    3022         <input <?php echo $this->get_edit_field_html_elements( $r ); ?>>
    3023 
    3024         <?php
    3025     }
    3026 
    3027     /**
    3028      * Output HTML for this field type on the wp-admin Profile Fields screen.
    3029      *
    3030      * Must be used inside the {@link bp_profile_fields()} template loop.
    3031      *
    3032      * @param array $raw_properties Optional key/value array of permitted attributes that you want to add.
    3033      * @since BuddyPress (2.0.0)
    3034      */
    3035     public function admin_field_html( array $raw_properties = array() ) {
    3036         $r = bp_parse_args( $raw_properties, array(
    3037             'type' => 'number'
    3038         ) ); ?>
    3039 
    3040         <input <?php echo $this->get_edit_field_html_elements( $r ); ?>>
    3041     <?php
    3042     }
    3043 
    3044     /**
    3045      * This method usually outputs HTML for this field type's children options on the wp-admin Profile Fields
    3046      * "Add Field" and "Edit Field" screens, but for this field type, we don't want it, so it's stubbed out.
    3047      *
    3048      * @param BP_XProfile_Field $current_field The current profile field on the add/edit screen.
    3049      * @param string $control_type Optional. HTML input type used to render the current field's child options.
    3050      * @since BuddyPress (2.0.0)
    3051      */
    3052     public function admin_new_field_html( BP_XProfile_Field $current_field, $control_type = '' ) {}
    3053 }
    3054 
    3055 /**
    3056  * URL xprofile field type.
    3057  *
    3058  * @since BuddyPress (2.1.0)
    3059  */
    3060 class BP_XProfile_Field_Type_URL extends BP_XProfile_Field_Type {
    3061 
    3062     /**
    3063      * Constructor for the URL field type
    3064      *
    3065      * @since BuddyPress (2.1.0)
    3066      */
    3067     public function __construct() {
    3068         parent::__construct();
    3069 
    3070         $this->category = _x( 'Single Fields', 'xprofile field type category', 'buddypress' );
    3071         $this->name     = _x( 'URL', 'xprofile field type', 'buddypress' );
    3072 
    3073         $this->set_format( '_^(?:(?:https?|ftp)://)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)*(?:\.(?:[a-z\x{00a1}-\x{ffff}]{2,})))(?::\d{2,5})?(?:/[^\s]*)?$_iuS', 'replace' );
    3074 
    3075         /**
    3076          * Fires inside __construct() method for BP_XProfile_Field_Type_URL class.
    3077          *
    3078          * @since BuddyPress (2.0.0)
    3079          *
    3080          * @param BP_XProfile_Field_Type_URL $this Current instance of
    3081          *                                         the field type URL.
    3082          */
    3083         do_action( 'bp_xprofile_field_type_url', $this );
    3084     }
    3085 
    3086     /**
    3087      * Output the edit field HTML for this field type.
    3088      *
    3089      * Must be used inside the {@link bp_profile_fields()} template loop.
    3090      *
    3091      * @param array $raw_properties Optional key/value array of
    3092      *        {@link http://dev.w3.org/html5/markup/input.number.html permitted attributes}
    3093      *        that you want to add.
    3094      * @since BuddyPress (2.1.0)
    3095      */
    3096     public function edit_field_html( array $raw_properties = array() ) {
    3097 
    3098         // `user_id` is a special optional parameter that certain other
    3099         // fields types pass to {@link bp_the_profile_field_options()}.
    3100         if ( isset( $raw_properties['user_id'] ) ) {
    3101             unset( $raw_properties['user_id'] );
    3102         }
    3103 
    3104         $r = bp_parse_args( $raw_properties, array(
    3105             'type'      => 'text',
    3106             'inputmode' => 'url',
    3107             'value'     => esc_url( bp_get_the_profile_field_edit_value() ),
    3108         ) ); ?>
    3109 
    3110         <label for="<?php bp_the_profile_field_input_name(); ?>">
    3111             <?php bp_the_profile_field_name(); ?>
    3112             <?php if ( bp_get_the_profile_field_is_required() ) : ?>
    3113                 <?php esc_html_e( '(required)', 'buddypress' ); ?>
    3114             <?php endif; ?>
    3115         </label>
    3116 
    3117         <?php
    3118 
    3119         /** This action is documented in bp-xprofile/bp-xprofile-classes */
    3120         do_action( bp_get_the_profile_field_errors_action() ); ?>
    3121 
    3122         <input <?php echo $this->get_edit_field_html_elements( $r ); ?>>
    3123 
    3124         <?php
    3125     }
    3126 
    3127     /**
    3128      * Output HTML for this field type on the wp-admin Profile Fields screen.
    3129      *
    3130      * Must be used inside the {@link bp_profile_fields()} template loop.
    3131      *
    3132      * @param array $raw_properties Optional key/value array of permitted
    3133      *        attributes that you want to add.
    3134      * @since BuddyPress (2.1.0)
    3135      */
    3136     public function admin_field_html( array $raw_properties = array() ) {
    3137 
    3138         $r = bp_parse_args( $raw_properties, array(
    3139             'type' => 'url'
    3140         ) ); ?>
    3141 
    3142         <input <?php echo $this->get_edit_field_html_elements( $r ); ?>>
    3143 
    3144         <?php
    3145     }
    3146 
    3147     /**
    3148      * This method usually outputs HTML for this field type's children options
    3149      * on the wp-admin Profile Fields "Add Field" and "Edit Field" screens, but
    3150      * for this field type, we don't want it, so it's stubbed out.
    3151      *
    3152      * @param BP_XProfile_Field $current_field The current profile field on the add/edit screen.
    3153      * @param string $control_type Optional. HTML input type used to render the current field's child options.
    3154      * @since BuddyPress (2.1.0)
    3155      */
    3156     public function admin_new_field_html( BP_XProfile_Field $current_field, $control_type = '' ) {}
    3157 
    3158     /**
    3159      * Modify submitted URL values before validation.
    3160      *
    3161      * The URL validation regex requires a http(s) protocol, so that all
    3162      * values saved in the database are fully-formed URLs. However, we
    3163      * still want to allow users to enter URLs without a protocol, for a
    3164      * better user experience. So we catch submitted URL values, and if
    3165      * the protocol is missing, we prepend 'http://' before passing to
    3166      * is_valid().
    3167      *
    3168      * @since BuddyPress (2.1.0)
    3169      *
    3170      * @param string $submitted_value Raw value submitted by the user.
    3171      * @return string
    3172      */
    3173     public static function pre_validate_filter( $submitted_value = '' ) {
    3174 
    3175         // Allow empty URL values
    3176         if ( empty( $submitted_value ) ) {
    3177             return '';
    3178         }
    3179 
    3180         // Run some checks on the submitted value
    3181         if ( false === strpos( $submitted_value, ':'  )
    3182              && substr( $submitted_value, 0, 1 ) !== '/'
    3183              && substr( $submitted_value, 0, 1 ) !== '#'
    3184              && ! preg_match( '/^[a-z0-9-]+?\.php/i', $submitted_value )
    3185         ) {
    3186             $submitted_value = 'http://' . $submitted_value;
    3187         }
    3188 
    3189         return $submitted_value;
    3190     }
    3191 
    3192     /**
    3193      * Format URL values for display.
    3194      *
    3195      * @since BuddyPress (2.1.0)
    3196      *
    3197      * @param string $field_value The URL value, as saved in the database.
    3198      * @return string URL converted to a link.
    3199      */
    3200     public static function display_filter( $field_value ) {
    3201         $link      = strip_tags( $field_value );
    3202         $no_scheme = preg_replace( '#^https?://#', '', rtrim( $link, '/' ) );
    3203         $url_text  = str_replace( $link, $no_scheme, $field_value );
    3204         return '<a href="' . esc_url( $field_value ) . '" rel="nofollow">' . esc_html( $url_text ) . '</a>';
    3205     }
    3206 }
    3207 
    3208 /**
    3209  * A placeholder xprofile field type. Doesn't do anything.
    3210  *
    3211  * Used if an existing field has an unknown type (e.g. one provided by a missing third-party plugin).
    3212  *
    3213  * @since BuddyPress (2.0.1)
    3214  */
    3215 class BP_XProfile_Field_Type_Placeholder extends BP_XProfile_Field_Type {
    3216 
    3217     /**
    3218      * Constructor for the placeholder field type.
    3219      *
    3220      * @since BuddyPress (2.0.1)
    3221      */
    3222     public function __construct() {
    3223         $this->set_format( '/.*/', 'replace' );
    3224     }
    3225 
    3226     /**
    3227      * Prevent any HTML being output for this field type.
    3228      *
    3229      * @param array $raw_properties Optional key/value array of {@link http://dev.w3.org/html5/markup/input.text.html permitted attributes} that you want to add.
    3230      * @since BuddyPress (2.0.1)
    3231      */
    3232     public function edit_field_html( array $raw_properties = array() ) {
    3233     }
    3234 
    3235     /**
    3236      * Prevent any HTML being output for this field type.
    3237      *
    3238      * @param array $raw_properties Optional key/value array of permitted attributes that you want to add.
    3239      * @since BuddyPress (2.0.1)
    3240      */
    3241     public function admin_field_html( array $raw_properties = array() ) {
    3242     }
    3243 
    3244     /**
    3245      * Prevent any HTML being output for this field type.
    3246      *
    3247      * @param BP_XProfile_Field $current_field The current profile field on the add/edit screen.
    3248      * @param string $control_type Optional. HTML input type used to render the current field's child options.
    3249      * @since BuddyPress (2.0.1)
    3250      */
    3251     public function admin_new_field_html( BP_XProfile_Field $current_field, $control_type = '' ) {}
    3252 }
    3253 
    3254 /**
    3255  * Represents a type of XProfile field and holds meta information about the type of value that it accepts.
    3256  *
    3257  * @since BuddyPress (2.0.0)
    3258  */
    3259 abstract class BP_XProfile_Field_Type {
    3260 
    3261     /**
    3262      * @since BuddyPress (2.0.0)
    3263      * @var array Field type validation regexes
    3264      */
    3265     protected $validation_regex = array();
    3266 
    3267     /**
    3268      * @since BuddyPress (2.0.0)
    3269      * @var array Field type whitelisted values
    3270      */
    3271     protected $validation_whitelist = array();
    3272 
    3273     /**
    3274      * @since BuddyPress (2.0.0)
    3275      * @var string The name of this field type
    3276      */
    3277     public $name = '';
    3278 
    3279     /**
    3280      * The name of the category that this field type should be grouped with. Used on the [Users > Profile Fields] screen in wp-admin.
    3281      *
    3282      * @since BuddyPress (2.0.0)
    3283      * @var string
    3284      */
    3285     public $category = '';
    3286 
    3287     /**
    3288      * @since BuddyPress (2.0.0)
    3289      * @var bool If this is set, allow BP to store null/empty values for this field type.
    3290      */
    3291     public $accepts_null_value = false;
    3292 
    3293     /**
    3294      * If this is set, BP will set this field type's validation whitelist from the field's options (e.g checkbox, selectbox).
    3295      *
    3296      * @since BuddyPress (2.0.0)
    3297      * @var bool Does this field support options? e.g. selectbox, radio buttons, etc.
    3298      */
    3299     public $supports_options = false;
    3300 
    3301     /**
    3302      * @since BuddyPress (2.0.0)
    3303      * @var bool Does this field type support multiple options being set as default values? e.g. multiselectbox, checkbox.
    3304      */
    3305     public $supports_multiple_defaults = false;
    3306 
    3307     /**
    3308      * @since BuddyPress (2.0.0)
    3309      * @var BP_XProfile_Field If this object is created by instantiating a {@link BP_XProfile_Field}, this is a reference back to that object.
    3310      */
    3311     public $field_obj = null;
    3312 
    3313     /**
    3314      * Constructor
    3315      *
    3316      * @since BuddyPress (2.0.0)
    3317      */
    3318     public function __construct() {
    3319 
    3320         /**
    3321          * Fires inside __construct() method for BP_XProfile_Field_Type class.
    3322          *
    3323          * @since BuddyPress (2.0.0)
    3324          *
    3325          * @param BP_XProfile_Field_Type $this Current instance of
    3326          *                                     the field type class.
    3327          */
    3328         do_action( 'bp_xprofile_field_type', $this );
    3329     }
    3330 
    3331     /**
    3332      * Set a regex that profile data will be asserted against.
    3333      *
    3334      * You can call this method multiple times to set multiple formats. When validation is performed,
    3335      * it's successful as long as the new value matches any one of the registered formats.
    3336      *
    3337      * @param string $format Regex string
    3338      * @param string $replace_format Optional; if 'replace', replaces the format instead of adding to it. Defaults to 'add'.
    3339      * @return BP_XProfile_Field_Type
    3340      * @since BuddyPress (2.0.0)
    3341      */
    3342     public function set_format( $format, $replace_format = 'add' ) {
    3343 
    3344         /**
    3345          * Filters the regex format for the field type.
    3346          *
    3347          * @since BuddyPress (2.0.0)
    3348          *
    3349          * @param string                 $format         Regex string.
    3350          * @param string                 $replace_format Optional replace format If "replace", replaces the
    3351          *                                               format instead of adding to it. Defaults to "add".
    3352          * @param BP_XProfile_Field_Type $this           Current instance of the BP_XProfile_Field_Type class.
    3353          */
    3354         $format = apply_filters( 'bp_xprofile_field_type_set_format', $format, $replace_format, $this );
    3355 
    3356         if ( 'add' === $replace_format ) {
    3357             $this->validation_regex[] = $format;
    3358         } elseif ( 'replace' === $replace_format ) {
    3359             $this->validation_regex = array( $format );
    3360         }
    3361 
    3362         return $this;
    3363     }
    3364 
    3365     /**
    3366      * Add a value to this type's whitelist that profile data will be asserted against.
    3367      *
    3368      * You can call this method multiple times to set multiple formats. When validation is performed,
    3369      * it's successful as long as the new value matches any one of the registered formats.
    3370      *
    3371      * @param string|array $values
    3372      * @return BP_XProfile_Field_Type
    3373      * @since BuddyPress (2.0.0)
    3374      */
    3375     public function set_whitelist_values( $values ) {
    3376         foreach ( (array) $values as $value ) {
    3377 
    3378             /**
    3379              * Filters values for field type's whitelist that profile data will be asserted against.
    3380              *
    3381              * @since BuddyPress (2.0.0)
    3382              *
    3383              * @param string                 $value  Field value.
    3384              * @param array                  $values Original array of values.
    3385              * @param BP_XProfile_Field_Type $this   Current instance of the BP_XProfile_Field_Type class.
    3386              */
    3387             $this->validation_whitelist[] = apply_filters( 'bp_xprofile_field_type_set_whitelist_values', $value, $values, $this );
    3388         }
    3389 
    3390         return $this;
    3391     }
    3392 
    3393     /**
    3394      * Check the given string against the registered formats for this field type.
    3395      *
    3396      * This method doesn't support chaining.
    3397      *
    3398      * @param string|array $values Value to check against the registered formats
    3399      * @return bool True if the value validates
    3400      * @since BuddyPress (2.0.0)
    3401      */
    3402     public function is_valid( $values ) {
    3403         $validated = false;
    3404 
    3405         // Some types of field (e.g. multi-selectbox) may have multiple values to check
    3406         foreach ( (array) $values as $value ) {
    3407 
    3408             // Validate the $value against the type's accepted format(s).
    3409             foreach ( $this->validation_regex as $format ) {
    3410                 if ( 1 === preg_match( $format, $value ) ) {
    3411                     $validated = true;
    3412                     continue;
    3413 
    3414                 } else {
    3415                     $validated = false;
    3416                 }
    3417             }
    3418         }
    3419 
    3420         // Handle field types with accepts_null_value set if $values is an empty array
    3421         if ( ( false === $validated ) && is_array( $values ) && empty( $values ) && $this->accepts_null_value ) {
    3422             $validated = true;
    3423         }
    3424 
    3425         // If there's a whitelist set, also check the $value.
    3426         if ( ( true === $validated ) && ! empty( $values ) && ! empty( $this->validation_whitelist ) ) {
    3427             foreach ( (array) $values as $value ) {
    3428                 $validated = in_array( $value, $this->validation_whitelist, true );
    3429             }
    3430         }
    3431 
    3432         /**
    3433          * Filters whether or not field type is a valid format.
    3434          *
    3435          * @since BuddyPress (2.0.0)
    3436          *
    3437          * @param bool                   $validated Whether or not the field type is valid.
    3438          * @param string|array           $values    Value to check against the registered formats.
    3439          * @param BP_XProfile_Field_Type $this      Current instance of the BP_XProfile_Field_Type class.
    3440          */
    3441         return (bool) apply_filters( 'bp_xprofile_field_type_is_valid', $validated, $values, $this );
    3442     }
    3443 
    3444     /**
    3445      * Output the edit field HTML for this field type.
    3446      *
    3447      * Must be used inside the {@link bp_profile_fields()} template loop.
    3448      *
    3449      * @param array $raw_properties Optional key/value array of permitted attributes that you want to add.
    3450      * @since BuddyPress (2.0.0)
    3451      */
    3452     abstract public function edit_field_html( array $raw_properties = array() );
    3453 
    3454     /**
    3455      * Output the edit field options HTML for this field type.
    3456      *
    3457      * BuddyPress considers a field's "options" to be, for example, the items in a selectbox.
    3458      * These are stored separately in the database, and their templating is handled separately.
    3459      * Populate this method in a child class if it's required. Otherwise, you can leave it out.
    3460      *
    3461      * This templating is separate from {@link BP_XProfile_Field_Type::edit_field_html()} because
    3462      * it's also used in the wp-admin screens when creating new fields, and for backwards compatibility.
    3463      *
    3464      * Must be used inside the {@link bp_profile_fields()} template loop.
    3465      *
    3466      * @param array $args Optional. The arguments passed to {@link bp_the_profile_field_options()}.
    3467      * @since BuddyPress (2.0.0)
    3468      */
    3469     public function edit_field_options_html( array $args = array() ) {}
    3470 
    3471     /**
    3472      * Output HTML for this field type on the wp-admin Profile Fields screen.
    3473      *
    3474      * Must be used inside the {@link bp_profile_fields()} template loop.
    3475      *
    3476      * @param array $raw_properties Optional key/value array of permitted attributes that you want to add.
    3477      * @since BuddyPress (2.0.0)
    3478      */
    3479     abstract public function admin_field_html( array $raw_properties = array() );
    3480 
    3481     /**
    3482      * Output HTML for this field type's children options on the wp-admin Profile Fields "Add Field" and "Edit Field" screens.
    3483      *
    3484      * You don't need to implement this method for all field types. It's used in core by the
    3485      * selectbox, multi selectbox, checkbox, and radio button fields, to allow the admin to
    3486      * enter the child option values (e.g. the choices in a select box).
    3487      *
    3488      * Must be used inside the {@link bp_profile_fields()} template loop.
    3489      *
    3490      * @param BP_XProfile_Field $current_field The current profile field on the add/edit screen.
    3491      * @param string $control_type Optional. HTML input type used to render the current field's child options.
    3492      * @since BuddyPress (2.0.0)
    3493      */
    3494     public function admin_new_field_html( BP_XProfile_Field $current_field, $control_type = '' ) {
    3495         $type = array_search( get_class( $this ), bp_xprofile_get_field_types() );
    3496         if ( false === $type ) {
    3497             return;
    3498         }
    3499 
    3500         $class            = $current_field->type != $type ? 'display: none;' : '';
    3501         $current_type_obj = bp_xprofile_create_field_type( $type );
    3502         ?>
    3503 
    3504         <div id="<?php echo esc_attr( $type ); ?>" class="postbox bp-options-box" style="<?php echo esc_attr( $class ); ?> margin-top: 15px;">
    3505             <h3><?php esc_html_e( 'Please enter options for this Field:', 'buddypress' ); ?></h3>
    3506             <div class="inside">
    3507                 <p>
    3508                     <label for="sort_order_<?php echo esc_attr( $type ); ?>"><?php esc_html_e( 'Sort Order:', 'buddypress' ); ?></label>
    3509                     <select name="sort_order_<?php echo esc_attr( $type ); ?>" id="sort_order_<?php echo esc_attr( $type ); ?>" >
    3510                         <option value="custom" <?php selected( 'custom', $current_field->order_by ); ?>><?php esc_html_e( 'Custom',     'buddypress' ); ?></option>
    3511                         <option value="asc"    <?php selected( 'asc',    $current_field->order_by ); ?>><?php esc_html_e( 'Ascending',  'buddypress' ); ?></option>
    3512                         <option value="desc"   <?php selected( 'desc',   $current_field->order_by ); ?>><?php esc_html_e( 'Descending', 'buddypress' ); ?></option>
    3513                     </select>
    3514                 </p>
    3515 
    3516                 <?php
    3517 
    3518                 // Does option have children?
    3519                 $options = $current_field->get_children( true );
    3520 
    3521                 // If no children options exists for this field, check in $_POST
    3522                 // for a submitted form (e.g. on the "new field" screen).
    3523                 if ( empty( $options ) ) {
    3524 
    3525                     $options = array();
    3526                     $i       = 1;
    3527 
    3528                     while ( isset( $_POST[$type . '_option'][$i] ) ) {
    3529 
    3530                         // Multiselectbox and checkboxes support MULTIPLE default options; all other core types support only ONE.
    3531                         if ( $current_type_obj->supports_options && ! $current_type_obj->supports_multiple_defaults && isset( $_POST["isDefault_{$type}_option"][$i] ) && (int) $_POST["isDefault_{$type}_option"] === $i ) {
    3532                             $is_default_option = true;
    3533                         } elseif ( isset( $_POST["isDefault_{$type}_option"][$i] ) ) {
    3534                             $is_default_option = (bool) $_POST["isDefault_{$type}_option"][$i];
    3535                         } else {
    3536                             $is_default_option = false;
    3537                         }
    3538 
    3539                         // Grab the values from $_POST to use as the form's options
    3540                         $options[] = (object) array(
    3541                             'id'                => -1,
    3542                             'is_default_option' => $is_default_option,
    3543                             'name'              => sanitize_text_field( stripslashes( $_POST[$type . '_option'][$i] ) ),
    3544                         );
    3545 
    3546                         ++$i;
    3547                     }
    3548 
    3549                     // If there are still no children options set, this must be the "new field" screen, so add one new/empty option.
    3550                     if ( empty( $options ) ) {
    3551                         $options[] = (object) array(
    3552                             'id'                => -1,
    3553                             'is_default_option' => false,
    3554                             'name'              => '',
    3555                         );
    3556                     }
    3557                 }
    3558 
    3559                 // Render the markup for the children options
    3560                 if ( ! empty( $options ) ) {
    3561                     $default_name = '';
    3562 
    3563                     for ( $i = 0, $count = count( $options ); $i < $count; ++$i ) :
    3564                         $j = $i + 1;
    3565 
    3566                         // Multiselectbox and checkboxes support MULTIPLE default options; all other core types support only ONE.
    3567                         if ( $current_type_obj->supports_options && $current_type_obj->supports_multiple_defaults ) {
    3568                             $default_name = '[' . $j . ']';
    3569                         }
    3570                         ?>
    3571 
    3572                         <div id="<?php echo esc_attr( "{$type}_div{$j}" ); ?>" class="bp-option sortable">
    3573                             <span class="bp-option-icon grabber"></span>
    3574                             <input type="text" name="<?php echo esc_attr( "{$type}_option[{$j}]" ); ?>" id="<?php echo esc_attr( "{$type}_option{$j}" ); ?>" value="<?php echo esc_attr( stripslashes( $options[$i]->name ) ); ?>" />
    3575                             <label>
    3576                                 <input type="<?php echo esc_attr( $control_type ); ?>" name="<?php echo esc_attr( "isDefault_{$type}_option{$default_name}" ); ?>" <?php checked( $options[$i]->is_default_option, true ); ?> value="<?php echo esc_attr( $j ); ?>" />
    3577                                 <?php _e( 'Default Value', 'buddypress' ); ?>
    3578                             </label>
    3579 
    3580                             <?php if ( 1 !== $j ) : ?>
    3581                                 <div class ="delete-button">
    3582                                     <a href='javascript:hide("<?php echo esc_attr( "{$type}_div{$j}" ); ?>")' class="delete"><?php esc_html_e( 'Delete', 'buddypress' ); ?></a>
    3583                                 </div>
    3584                             <?php endif; ?>
    3585 
    3586                         </div>
    3587 
    3588                     <?php endfor; ?>
    3589 
    3590                     <input type="hidden" name="<?php echo esc_attr( "{$type}_option_number" ); ?>" id="<?php echo esc_attr( "{$type}_option_number" ); ?>" value="<?php echo esc_attr( $j + 1 ); ?>" />
    3591                 <?php } ?>
    3592 
    3593                 <div id="<?php echo esc_attr( "{$type}_more" ); ?>"></div>
    3594                 <p><a href="javascript:add_option('<?php echo esc_js( $type ); ?>')"><?php esc_html_e( 'Add Another Option', 'buddypress' ); ?></a></p>
    3595             </div>
    3596         </div>
    3597 
    3598         <?php
    3599     }
    3600 
    3601     /**
    3602      * Allow field types to modify submitted values before they are validated.
    3603      *
    3604      * In some cases, it may be appropriate for a field type to catch
    3605      * submitted values and modify them before they are passed to the
    3606      * is_valid() method. For example, URL validation requires the
    3607      * 'http://' scheme (so that the value saved in the database is always
    3608      * a fully-formed URL), but in order to allow users to enter a URL
    3609      * without this scheme, BP_XProfile_Field_Type_URL prepends 'http://'
    3610      * when it's not present.
    3611      *
    3612      * By default, this is a pass-through method that does nothing. Only
    3613      * override in your own field type if you need this kind of pre-
    3614      * validation filtering.
    3615      *
    3616      * @since BuddyPress (2.1.0)
    3617      *
    3618      * @param mixed $submitted_value Submitted value.
    3619      * @return mixed
    3620      */
    3621     public static function pre_validate_filter( $field_value ) {
    3622         return $field_value;
    3623     }
    3624 
    3625     /**
    3626      * Allow field types to modify the appearance of their values.
    3627      *
    3628      * By default, this is a pass-through method that does nothing. Only
    3629      * override in your own field type if you need to provide custom
    3630      * filtering for output values.
    3631      *
    3632      * @since BuddyPress (2.1.0)
    3633      *
    3634      * @param mixed $field_value Field value.
    3635      * @return mixed
    3636      */
    3637     public static function display_filter( $field_value ) {
    3638         return $field_value;
    3639     }
    3640 
    3641     /** Protected *************************************************************/
    3642 
    3643     /**
    3644      * Get a sanitised and escaped string of the edit field's HTML elements and attributes.
    3645      *
    3646      * Must be used inside the {@link bp_profile_fields()} template loop.
    3647      * This method was intended to be static but couldn't be because php.net/lsb/ requires PHP >= 5.3.
    3648      *
    3649      * @param array $properties Optional key/value array of attributes for this edit field.
    3650      * @return string
    3651      * @since BuddyPress (2.0.0)
    3652      */
    3653     protected function get_edit_field_html_elements( array $properties = array() ) {
    3654 
    3655         $r = bp_parse_args( $properties, array(
    3656             'id'   => bp_get_the_profile_field_input_name(),
    3657             'name' => bp_get_the_profile_field_input_name(),
    3658         ) );
    3659 
    3660         if ( bp_get_the_profile_field_is_required() ) {
    3661             $r['aria-required'] = 'true';
    3662         }
    3663 
    3664         $html = '';
    3665 
    3666         /**
    3667          * Filters the edit html elements and attributes.
    3668          *
    3669          * @since BuddyPress (2.0.0)
    3670          *
    3671          * @param array  $r     Array of parsed arguments.
    3672          * @param string $value Class name for the current class instance.
    3673          */
    3674         $r = (array) apply_filters( 'bp_xprofile_field_edit_html_elements', $r, get_class( $this ) );
    3675 
    3676         return bp_get_form_field_attributes( sanitize_key( bp_get_the_profile_field_name() ), $r );
    3677     }
    3678 }
    3679 
    3680 /**
    3681  * Class for generating SQL clauses to filter a user query by xprofile data.
    3682  *
    3683  * @since BuddyPress (2.2.0)
    3684  */
    3685 class BP_XProfile_Query {
    3686     /**
    3687      * Array of xprofile queries.
    3688      *
    3689      * See {@see WP_XProfile_Query::__construct()} for information on parameters.
    3690      *
    3691      * @since  BuddyPress (2.2.0)
    3692      * @access public
    3693      * @var    array
    3694      */
    3695     public $queries = array();
    3696 
    3697     /**
    3698      * Database table that where the metadata's objects are stored (eg $wpdb->users).
    3699      *
    3700      * @since  BuddyPress (2.2.0)
    3701      * @access public
    3702      * @var    string
    3703      */
    3704     public $primary_table;
    3705 
    3706     /**
    3707      * Column in primary_table that represents the ID of the object.
    3708      *
    3709      * @since  BuddyPress (2.2.0)
    3710      * @access public
    3711      * @var    string
    3712      */
    3713     public $primary_id_column;
    3714 
    3715     /**
    3716      * A flat list of table aliases used in JOIN clauses.
    3717      *
    3718      * @since  BuddyPress (2.2.0)
    3719      * @access protected
    3720      * @var    array
    3721      */
    3722     protected $table_aliases = array();
    3723 
    3724     /**
    3725      * Constructor.
    3726      *
    3727      * @since  BuddyPress (2.2.0)
    3728      * @access public
    3729      *
    3730      * @param array $xprofile_query {
    3731      *     Array of xprofile query clauses.
    3732      *
    3733      *     @type string $relation Optional. The MySQL keyword used to join the clauses of the query.
    3734      *                            Accepts 'AND', or 'OR'. Default 'AND'.
    3735      *     @type array {
    3736      *         Optional. An array of first-order clause parameters, or another fully-formed xprofile query.
    3737      *
    3738      *         @type string|int $field   XProfile field to filter by. Accepts a field name or ID.
    3739      *         @type string     $value   XProfile value to filter by.
    3740      *         @type string     $compare MySQL operator used for comparing the $value. Accepts '=', '!=', '>',
    3741      *                                   '>=', '<', '<=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'BETWEEN',
    3742      *                                   'NOT BETWEEN', 'REGEXP', 'NOT REGEXP', or 'RLIKE'. Default is 'IN'
    3743      *                                   when `$value` is an array, '=' otherwise.
    3744      *         @type string     $type    MySQL data type that the `value` column will be CAST to for comparisons.
    3745      *                                   Accepts 'NUMERIC', 'BINARY', 'CHAR', 'DATE', 'DATETIME', 'DECIMAL',
    3746      *                                   'SIGNED', 'TIME', or 'UNSIGNED'. Default is 'CHAR'.
    3747      *     }
    3748      * }
    3749      */
    3750     public function __construct( $xprofile_query ) {
    3751         if ( empty( $xprofile_query ) ) {
    3752             return;
    3753         }
    3754 
    3755         $this->queries = $this->sanitize_query( $xprofile_query );
    3756     }
    3757 
    3758     /**
    3759      * Ensure the `xprofile_query` argument passed to the class constructor is well-formed.
    3760      *
    3761      * Eliminates empty items and ensures that a 'relation' is set.
    3762      *
    3763      * @since  BuddyPress (2.2.0)
    3764      * @access public
    3765      *
    3766      * @param  array $queries Array of query clauses.
    3767      * @return array Sanitized array of query clauses.
    3768      */
    3769     public function sanitize_query( $queries ) {
    3770         $clean_queries = array();
    3771 
    3772         if ( ! is_array( $queries ) ) {
    3773             return $clean_queries;
    3774         }
    3775 
    3776         foreach ( $queries as $key => $query ) {
    3777             if ( 'relation' === $key ) {
    3778                 $relation = $query;
    3779 
    3780             } elseif ( ! is_array( $query ) ) {
    3781                 continue;
    3782 
    3783             // First-order clause.
    3784             } elseif ( $this->is_first_order_clause( $query ) ) {
    3785                 if ( isset( $query['value'] ) && array() === $query['value'] ) {
    3786                     unset( $query['value'] );
    3787                 }
    3788 
    3789                 $clean_queries[] = $query;
    3790 
    3791             // Otherwise, it's a nested query, so we recurse.
    3792             } else {
    3793                 $cleaned_query = $this->sanitize_query( $query );
    3794 
    3795                 if ( ! empty( $cleaned_query ) ) {
    3796                     $clean_queries[] = $cleaned_query;
    3797                 }
    3798             }
    3799         }
    3800 
    3801         if ( empty( $clean_queries ) ) {
    3802             return $clean_queries;
    3803         }
    3804 
    3805         // Sanitize the 'relation' key provided in the query.
    3806         if ( isset( $relation ) && 'OR' === strtoupper( $relation ) ) {
    3807             $clean_queries['relation'] = 'OR';
    3808 
    3809         /*
    3810          * If there is only a single clause, call the relation 'OR'.
    3811          * This value will not actually be used to join clauses, but it
    3812          * simplifies the logic around combining key-only queries.
    3813          */
    3814         } elseif ( 1 === count( $clean_queries ) ) {
    3815             $clean_queries['relation'] = 'OR';
    3816 
    3817         // Default to AND.
    3818         } else {
    3819             $clean_queries['relation'] = 'AND';
    3820         }
    3821 
    3822         return $clean_queries;
    3823     }
    3824 
    3825     /**
    3826      * Determine whether a query clause is first-order.
    3827      *
    3828      * A first-order query clause is one that has either a 'key' or a 'value' array key.
    3829      *
    3830      * @since  BuddyPress (2.2.0)
    3831      * @access protected
    3832      *
    3833      * @param  array $query XProfile query arguments.
    3834      * @return bool  Whether the query clause is a first-order clause.
    3835      */
    3836     protected function is_first_order_clause( $query ) {
    3837         return isset( $query['field'] ) || isset( $query['value'] );
    3838     }
    3839 
    3840     /**
    3841      * Return the appropriate alias for the given field type if applicable.
    3842      *
    3843      * @since  BuddyPress (2.2.0)
    3844      * @access public
    3845      *
    3846      * @param  string $type MySQL type to cast `value`.
    3847      * @return string MySQL type.
    3848      */
    3849     public function get_cast_for_type( $type = '' ) {
    3850         if ( empty( $type ) )
    3851             return 'CHAR';
    3852 
    3853         $meta_type = strtoupper( $type );
    3854 
    3855         if ( ! preg_match( '/^(?:BINARY|CHAR|DATE|DATETIME|SIGNED|UNSIGNED|TIME|NUMERIC(?:\(\d+(?:,\s?\d+)?\))?|DECIMAL(?:\(\d+(?:,\s?\d+)?\))?)$/', $meta_type ) )
    3856             return 'CHAR';
    3857 
    3858         if ( 'NUMERIC' == $meta_type )
    3859             $meta_type = 'SIGNED';
    3860 
    3861         return $meta_type;
    3862     }
    3863 
    3864     /**
    3865      * Generate SQL clauses to be appended to a main query.
    3866      *
    3867      * Called by the public {@see BP_XProfile_Query::get_sql()}, this method is abstracted out to maintain parity
    3868      * with WP's Query classes.
    3869      *
    3870      * @since  BuddyPress (2.2.0)
    3871      * @access protected
    3872      *
    3873      * @return array {
    3874      *     Array containing JOIN and WHERE SQL clauses to append to the main query.
    3875      *
    3876      *     @type string $join  SQL fragment to append to the main JOIN clause.
    3877      *     @type string $where SQL fragment to append to the main WHERE clause.
    3878      * }
    3879      */
    3880     protected function get_sql_clauses() {
    3881         /*
    3882          * $queries are passed by reference to get_sql_for_query() for recursion.
    3883          * To keep $this->queries unaltered, pass a copy.
    3884          */
    3885         $queries = $this->queries;
    3886         $sql = $this->get_sql_for_query( $queries );
    3887 
    3888         if ( ! empty( $sql['where'] ) ) {
    3889             $sql['where'] = ' AND ' . $sql['where'];
    3890         }
    3891 
    3892         return $sql;
    3893     }
    3894 
    3895     /**
    3896      * Generate SQL clauses for a single query array.
    3897      *
    3898      * If nested subqueries are found, this method recurses the tree to produce the properly nested SQL.
    3899      *
    3900      * @since  BuddyPress (2.2.0)
    3901      * @access protected
    3902      *
    3903      * @param  array $query Query to parse.
    3904      * @param  int   $depth Optional. Number of tree levels deep we currently are. Used to calculate indentation.
    3905      * @return array {
    3906      *     Array containing JOIN and WHERE SQL clauses to append to a single query array.
    3907      *
    3908      *     @type string $join  SQL fragment to append to the main JOIN clause.
    3909      *     @type string $where SQL fragment to append to the main WHERE clause.
    3910      * }
    3911      */
    3912     protected function get_sql_for_query( &$query, $depth = 0 ) {
    3913         $sql_chunks = array(
    3914             'join'  => array(),
    3915             'where' => array(),
    3916         );
    3917 
    3918         $sql = array(
    3919             'join'  => '',
    3920             'where' => '',
    3921         );
    3922 
    3923         $indent = '';
    3924         for ( $i = 0; $i < $depth; $i++ ) {
    3925             $indent .= "  ";
    3926         }
    3927 
    3928         foreach ( $query as $key => &$clause ) {
    3929             if ( 'relation' === $key ) {
    3930                 $relation = $query['relation'];
    3931             } elseif ( is_array( $clause ) ) {
    3932 
    3933                 // This is a first-order clause.
    3934                 if ( $this->is_first_order_clause( $clause ) ) {
    3935                     $clause_sql = $this->get_sql_for_clause( $clause, $query );
    3936 
    3937                     $where_count = count( $clause_sql['where'] );
    3938                     if ( ! $where_count ) {
    3939                         $sql_chunks['where'][] = '';
    3940                     } elseif ( 1 === $where_count ) {
    3941                         $sql_chunks['where'][] = $clause_sql['where'][0];
    3942                     } else {
    3943                         $sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )';
    3944                     }
    3945 
    3946                     $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] );
    3947                 // This is a subquery, so we recurse.
    3948                 } else {
    3949                     $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 );
    3950 
    3951                     $sql_chunks['where'][] = $clause_sql['where'];
    3952                     $sql_chunks['join'][]  = $clause_sql['join'];
    3953                 }
    3954             }
    3955         }
    3956 
    3957         // Filter to remove empties.
    3958         $sql_chunks['join']  = array_filter( $sql_chunks['join'] );
    3959         $sql_chunks['where'] = array_filter( $sql_chunks['where'] );
    3960 
    3961         if ( empty( $relation ) ) {
    3962             $relation = 'AND';
    3963         }
    3964 
    3965         // Filter duplicate JOIN clauses and combine into a single string.
    3966         if ( ! empty( $sql_chunks['join'] ) ) {
    3967             $sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) );
    3968         }
    3969 
    3970         // Generate a single WHERE clause with proper brackets and indentation.
    3971         if ( ! empty( $sql_chunks['where'] ) ) {
    3972             $sql['where'] = '( ' . "\n  " . $indent . implode( ' ' . "\n  " . $indent . $relation . ' ' . "\n  " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')';
    3973         }
    3974 
    3975         return $sql;
    3976     }
    3977 
    3978     /**
    3979      * Generates SQL clauses to be appended to a main query.
    3980      *
    3981      * @since  BuddyPress (2.2.0)
    3982      * @access public
    3983      *
    3984      * @param string $primary_table     Database table where the object being filtered is stored (eg wp_users).
    3985      * @param string $primary_id_column ID column for the filtered object in $primary_table.
    3986      * @return array {
    3987      *     Array containing JOIN and WHERE SQL clauses to append to the main query.
    3988      *
    3989      *     @type string $join  SQL fragment to append to the main JOIN clause.
    3990      *     @type string $where SQL fragment to append to the main WHERE clause.
    3991      * }
    3992      */
    3993     public function get_sql( $primary_table, $primary_id_column, $context = null ) {
    3994         global $wpdb;
    3995 
    3996         $this->primary_table     = $primary_table;
    3997         $this->primary_id_column = $primary_id_column;
    3998 
    3999         $sql = $this->get_sql_clauses();
    4000 
    4001         /*
    4002          * If any JOINs are LEFT JOINs (as in the case of NOT EXISTS), then all JOINs should
    4003          * be LEFT. Otherwise posts with no metadata will be excluded from results.
    4004          */
    4005         if ( false !== strpos( $sql['join'], 'LEFT JOIN' ) ) {
    4006             $sql['join'] = str_replace( 'INNER JOIN', 'LEFT JOIN', $sql['join'] );
    4007         }
    4008 
    4009         return $sql;
    4010     }
    4011 
    4012     /**
    4013      * Generate SQL JOIN and WHERE clauses for a first-order query clause.
    4014      *
    4015      * "First-order" means that it's an array with a 'field' or 'value'.
    4016      *
    4017      * @since  BuddyPress (2.2.0)
    4018      * @access public
    4019      *
    4020      * @param array $clause       Query clause.
    4021      * @param array $parent_query Parent query array.
    4022      * @return array {
    4023      *     Array containing JOIN and WHERE SQL clauses to append to a first-order query.
    4024      *
    4025      *     @type string $join  SQL fragment to append to the main JOIN clause.
    4026      *     @type string $where SQL fragment to append to the main WHERE clause.
    4027      * }
    4028      */
    4029     public function get_sql_for_clause( &$clause, $parent_query ) {
    4030         global $wpdb;
    4031 
    4032         $sql_chunks = array(
    4033             'where' => array(),
    4034             'join' => array(),
    4035         );
    4036 
    4037         if ( isset( $clause['compare'] ) ) {
    4038             $clause['compare'] = strtoupper( $clause['compare'] );
    4039         } else {
    4040             $clause['compare'] = isset( $clause['value'] ) && is_array( $clause['value'] ) ? 'IN' : '=';
    4041         }
    4042 
    4043         if ( ! in_array( $clause['compare'], array(
    4044             '=', '!=', '>', '>=', '<', '<=',
    4045             'LIKE', 'NOT LIKE',
    4046             'IN', 'NOT IN',
    4047             'BETWEEN', 'NOT BETWEEN',
    4048             'EXISTS', 'NOT EXISTS',
    4049             'REGEXP', 'NOT REGEXP', 'RLIKE'
    4050         ) ) ) {
    4051             $clause['compare'] = '=';
    4052         }
    4053 
    4054         $field_compare = $clause['compare'];
    4055 
    4056         // First build the JOIN clause, if one is required.
    4057         $join = '';
    4058 
    4059         $data_table = buddypress()->profile->table_name_data;
    4060 
    4061         // We prefer to avoid joins if possible. Look for an existing join compatible with this clause.
    4062         $alias = $this->find_compatible_table_alias( $clause, $parent_query );
    4063         if ( false === $alias ) {
    4064             $i = count( $this->table_aliases );
    4065             $alias = $i ? 'xpq' . $i : $data_table;
    4066 
    4067             // JOIN clauses for NOT EXISTS have their own syntax.
    4068             if ( 'NOT EXISTS' === $field_compare ) {
    4069                 $join .= " LEFT JOIN $data_table";
    4070                 $join .= $i ? " AS $alias" : '';
    4071                 $join .= $wpdb->prepare( " ON ($this->primary_table.$this->primary_id_column = $alias.user_id AND $alias.field_id = %d )", $clause['field'] );
    4072 
    4073             // All other JOIN clauses.
    4074             } else {
    4075                 $join .= " INNER JOIN $data_table";
    4076                 $join .= $i ? " AS $alias" : '';
    4077                 $join .= " ON ( $this->primary_table.$this->primary_id_column = $alias.user_id )";
    4078             }
    4079 
    4080             $this->table_aliases[] = $alias;
    4081             $sql_chunks['join'][] = $join;
    4082         }
    4083 
    4084         // Save the alias to this clause, for future siblings to find.
    4085         $clause['alias'] = $alias;
    4086 
    4087         // Next, build the WHERE clause.
    4088         $where = '';
    4089 
    4090         // field_id.
    4091         if ( array_key_exists( 'field', $clause ) ) {
    4092             // Convert field name to ID if necessary.
    4093             if ( ! is_numeric( $clause['field'] ) ) {
    4094                 $clause['field'] = xprofile_get_field_id_from_name( $clause['field'] );
    4095             }
    4096 
    4097             // NOT EXISTS has its own syntax.
    4098             if ( 'NOT EXISTS' === $field_compare ) {
    4099                 $sql_chunks['where'][] = $alias . '.user_id IS NULL';
    4100             } else {
    4101                 $sql_chunks['where'][] = $wpdb->prepare( "$alias.field_id = %d", $clause['field'] );
    4102             }
    4103         }
    4104 
    4105         // value.
    4106         if ( array_key_exists( 'value', $clause ) ) {
    4107             $field_value = $clause['value'];
    4108             $field_type = $this->get_cast_for_type( isset( $clause['type'] ) ? $clause['type'] : '' );
    4109 
    4110             if ( in_array( $field_compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ) ) ) {
    4111                 if ( ! is_array( $field_value ) ) {
    4112                     $field_value = preg_split( '/[,\s]+/', $field_value );
    4113                 }
    4114             } else {
    4115                 $field_value = trim( $field_value );
    4116             }
    4117 
    4118             switch ( $field_compare ) {
    4119                 case 'IN' :
    4120                 case 'NOT IN' :
    4121                     $field_compare_string = '(' . substr( str_repeat( ',%s', count( $field_value ) ), 1 ) . ')';
    4122                     $where = $wpdb->prepare( $field_compare_string, $field_value );
    4123                     break;
    4124 
    4125                 case 'BETWEEN' :
    4126                 case 'NOT BETWEEN' :
    4127                     $field_value = array_slice( $field_value, 0, 2 );
    4128                     $where = $wpdb->prepare( '%s AND %s', $field_value );
    4129                     break;
    4130 
    4131                 case 'LIKE' :
    4132                 case 'NOT LIKE' :
    4133                     $field_value = '%' . bp_esc_like( $field_value ) . '%';
    4134                     $where = $wpdb->prepare( '%s', $field_value );
    4135                     break;
    4136 
    4137                 default :
    4138                     $where = $wpdb->prepare( '%s', $field_value );
    4139                     break;
    4140 
    4141             }
    4142 
    4143             if ( $where ) {
    4144                 $sql_chunks['where'][] = "CAST($alias.value AS {$field_type}) {$field_compare} {$where}";
    4145             }
    4146         }
    4147 
    4148         /*
    4149          * Multiple WHERE clauses (`field` and `value` pairs) should be joined in parentheses.
    4150          */
    4151         if ( 1 < count( $sql_chunks['where'] ) ) {
    4152             $sql_chunks['where'] = array( '( ' . implode( ' AND ', $sql_chunks['where'] ) . ' )' );
    4153         }
    4154 
    4155         return $sql_chunks;
    4156     }
    4157 
    4158     /**
    4159      * Identify an existing table alias that is compatible with the current query clause.
    4160      *
    4161      * We avoid unnecessary table joins by allowing each clause to look for an existing table alias that is
    4162      * compatible with the query that it needs to perform. An existing alias is compatible if (a) it is a
    4163      * sibling of $clause (ie, it's under the scope of the same relation), and (b) the combination of
    4164      * operator and relation between the clauses allows for a shared table join. In the case of BP_XProfile_Query,
    4165      * this * only applies to IN clauses that are connected by the relation OR.
    4166      *
    4167      * @since  BuddyPress (2.2.0)
    4168      * @access protected
    4169      *
    4170      * @param  array       $clause       Query clause.
    4171      * @param  array       $parent_query Parent query of $clause.
    4172      * @return string|bool Table alias if found, otherwise false.
    4173      */
    4174     protected function find_compatible_table_alias( $clause, $parent_query ) {
    4175         $alias = false;
    4176 
    4177         foreach ( $parent_query as $sibling ) {
    4178             // If the sibling has no alias yet, there's nothing to check.
    4179             if ( empty( $sibling['alias'] ) ) {
    4180                 continue;
    4181             }
    4182 
    4183             // We're only interested in siblings that are first-order clauses.
    4184             if ( ! is_array( $sibling ) || ! $this->is_first_order_clause( $sibling ) ) {
    4185                 continue;
    4186             }
    4187 
    4188             $compatible_compares = array();
    4189 
    4190             // Clauses connected by OR can share joins as long as they have "positive" operators.
    4191             if ( 'OR' === $parent_query['relation'] ) {
    4192                 $compatible_compares = array( '=', 'IN', 'BETWEEN', 'LIKE', 'REGEXP', 'RLIKE', '>', '>=', '<', '<=' );
    4193 
    4194             // Clauses joined by AND with "negative" operators share a join only if they also share a key.
    4195             } elseif ( isset( $sibling['field_id'] ) && isset( $clause['field_id'] ) && $sibling['field_id'] === $clause['field_id'] ) {
    4196                 $compatible_compares = array( '!=', 'NOT IN', 'NOT LIKE' );
    4197             }
    4198 
    4199             $clause_compare  = strtoupper( $clause['compare'] );
    4200             $sibling_compare = strtoupper( $sibling['compare'] );
    4201             if ( in_array( $clause_compare, $compatible_compares ) && in_array( $sibling_compare, $compatible_compares ) ) {
    4202                 $alias = $sibling['alias'];
    4203                 break;
    4204             }
    4205         }
    4206 
    4207         return $alias;
    4208     }
    4209 }
     12require __DIR__ . '/classes/class-bp-xprofile-group.php';
     13require __DIR__ . '/classes/class-bp-xprofile-field.php';
     14require __DIR__ . '/classes/class-bp-xprofile-profiledata.php';
     15require __DIR__ . '/classes/class-bp-xprofile-field-type.php';
     16require __DIR__ . '/classes/class-bp-xprofile-field-type-datebox.php';
     17require __DIR__ . '/classes/class-bp-xprofile-field-type-checkbox.php';
     18require __DIR__ . '/classes/class-bp-xprofile-field-type-radiobutton.php';
     19require __DIR__ . '/classes/class-bp-xprofile-field-type-multiselectbox.php';
     20require __DIR__ . '/classes/class-bp-xprofile-field-type-selectbox.php';
     21require __DIR__ . '/classes/class-bp-xprofile-field-type-textarea.php';
     22require __DIR__ . '/classes/class-bp-xprofile-field-type-textbox.php';
     23require __DIR__ . '/classes/class-bp-xprofile-field-type-number.php';
     24require __DIR__ . '/classes/class-bp-xprofile-field-type-url.php';
     25require __DIR__ . '/classes/class-bp-xprofile-field-type-placeholder.php';
     26require __DIR__ . '/classes/class-bp-xprofile-query.php';
Note: See TracChangeset for help on using the changeset viewer.