Changeset 9485
- Timestamp:
- 02/15/2015 12:48:56 AM (10 years ago)
- Location:
- trunk/src
- Files:
-
- 50 added
- 9 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/bp-activity/bp-activity-classes.php
r9385 r9485 10 10 defined( 'ABSPATH' ) || exit; 11 11 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 } 12 require __DIR__ . '/classes/class-bp-activity-activity.php'; 13 require __DIR__ . '/classes/class-bp-activity-feed.php'; 14 require __DIR__ . '/classes/class-bp-activity-query.php'; -
trunk/src/bp-blogs/bp-blogs-classes.php
r9471 r9485 11 11 defined( 'ABSPATH' ) || exit; 12 12 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 } 13 require __DIR__ . '/classes/class-bp-blogs-blog.php'; -
trunk/src/bp-core/bp-core-classes.php
r9471 r9485 10 10 defined( 'ABSPATH' ) || exit; 11 11 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 & and we need to undo this 2197 // See http://core.trac.wordpress.org/ticket/11311 2198 $url = str_replace( '&', '&', $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 } 12 require __DIR__ . '/classes/class-bp-user-query.php'; 13 require __DIR__ . '/classes/class-bp-core-user.php'; 14 require __DIR__ . '/classes/class-bp-date-query.php'; 15 require __DIR__ . '/classes/class-bp-core-notification.php'; 16 require __DIR__ . '/classes/class-bp-button.php'; 17 require __DIR__ . '/classes/class-bp-embed.php'; 18 require __DIR__ . '/classes/class-bp-walker-nav-menu.php'; 19 require __DIR__ . '/classes/class-bp-walker-nav-menu-checklist.php'; 20 require __DIR__ . '/classes/class-bp-suggestions.php'; 21 require __DIR__ . '/classes/class-bp-members-suggestions.php'; 22 require __DIR__ . '/classes/class-bp-recursive-query.php'; -
trunk/src/bp-friends/bp-friends-classes.php
r9471 r9485 10 10 defined( 'ABSPATH' ) || exit; 11 11 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 } 12 require __DIR__ . '/classes/class-bp-friends-friendship.php'; -
trunk/src/bp-groups/bp-groups-classes.php
r9471 r9485 1 1 <?php 2 3 2 /** 4 3 * BuddyPress Groups Classes … … 11 10 defined( 'ABSPATH' ) || exit; 12 11 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 } 12 require __DIR__ . '/classes/class-bp-group-extension.php'; 13 require __DIR__ . '/classes/class-bp-group-member-query.php'; 14 require __DIR__ . '/classes/class-bp-groups-group.php'; 15 require __DIR__ . '/classes/class-bp-groups-member-suggestions.php'; 16 require __DIR__ . '/classes/class-bp-groups-member.php'; -
trunk/src/bp-members/bp-members-classes.php
r9308 r9485 1 1 <?php 2 3 2 /** 4 * Signups Management class.3 * BuddyPress Members Classes 5 4 * 6 5 * @package BuddyPress 7 * @subpackage coreClasses 8 * 9 * @since BuddyPress (2.0.0) 6 * @subpackage MembersClasses 10 7 */ 11 class BP_Signup {12 8 13 /** 14 * ID of the signup which the object relates to. 15 * 16 * @var integer 17 */ 18 public $id; 9 // Exit if accessed directly 10 defined( 'ABSPATH' ) || exit; 19 11 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 } 12 require __DIR__ . '/classes/class-bp-signup.php'; -
trunk/src/bp-messages/bp-messages-classes.php
r9482 r9485 1 1 <?php 2 3 2 /** 4 3 * BuddyPress Messages Classes … … 11 10 defined( 'ABSPATH' ) || exit; 12 11 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 } 12 require __DIR__ . '/classes/class-bp_messages-thread.php'; 13 require __DIR__ . '/classes/class-bp-messages-message.php'; 14 require __DIR__ . '/classes/class-bp-messages-notice.php'; -
trunk/src/bp-notifications/bp-notifications-classes.php
r9352 r9485 1 1 <?php 2 3 2 /** 4 3 * BuddyPress Notifications Classes … … 15 14 defined( 'ABSPATH' ) || exit; 16 15 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 } 16 require __DIR__ . '/classes/class-bp-notifications-notification.php'; -
trunk/src/bp-xprofile/bp-xprofile-classes.php
r9471 r9485 1 1 <?php 2 3 2 /** 4 3 * BuddyPress XProfile Classes … … 11 10 defined( 'ABSPATH' ) || exit; 12 11 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&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&mode=edit_group&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&group_id=" . $this->group_id . "&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&mode=edit_field&group_id=" . $this->group_id . "&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 } 12 require __DIR__ . '/classes/class-bp-xprofile-group.php'; 13 require __DIR__ . '/classes/class-bp-xprofile-field.php'; 14 require __DIR__ . '/classes/class-bp-xprofile-profiledata.php'; 15 require __DIR__ . '/classes/class-bp-xprofile-field-type.php'; 16 require __DIR__ . '/classes/class-bp-xprofile-field-type-datebox.php'; 17 require __DIR__ . '/classes/class-bp-xprofile-field-type-checkbox.php'; 18 require __DIR__ . '/classes/class-bp-xprofile-field-type-radiobutton.php'; 19 require __DIR__ . '/classes/class-bp-xprofile-field-type-multiselectbox.php'; 20 require __DIR__ . '/classes/class-bp-xprofile-field-type-selectbox.php'; 21 require __DIR__ . '/classes/class-bp-xprofile-field-type-textarea.php'; 22 require __DIR__ . '/classes/class-bp-xprofile-field-type-textbox.php'; 23 require __DIR__ . '/classes/class-bp-xprofile-field-type-number.php'; 24 require __DIR__ . '/classes/class-bp-xprofile-field-type-url.php'; 25 require __DIR__ . '/classes/class-bp-xprofile-field-type-placeholder.php'; 26 require __DIR__ . '/classes/class-bp-xprofile-query.php';
Note: See TracChangeset
for help on using the changeset viewer.