Skip to:
Content

BuddyPress.org

Ticket #4140: bp-core-catchuri.php

File bp-core-catchuri.php, 45.4 KB (added by foxly, 14 years ago)

Updated catchuri file

Line 
1<?php
2
3/**
4 * BUDDYPRESS ROUTER
5 * Analyzes a URI passed from the web server and determines the correct page module to send the request to.
6 *
7 * @version 1.6
8 * @since 1.6
9 * @package Core
10 * @subpackage Router
11 * @license GPL v2.0
12 *
13 * ========================================================================================================
14 */
15
16class BP_router {
17
18
19        var $http_referer;                  // $_SERVER['HTTP_REFERER'] sent in from the web server
20        var $request_uri;                   // $_SERVER['REQUEST_URI'] sent in from the web server
21        var $wp_http_referer;               // $_REQUEST['_wp_http_referer'] sent in from the web server
22
23        var $bp;                            // Local copy of $bp singleton
24
25        var $wpdb;                          // Local copy of $wpdb singleton
26        var $wp_query;                      // Local copy of $wp_query singleton
27        var $current_blog;                  // Local copy of WordPress $current_blog global
28        var $current_site;                  // Local copy of WordPress $current_site global
29
30        var $walk;                          // Walk array for current URI
31        var $flat_pages;                    // The page tree for the root blog or current blog as a flat array
32        var $lofted_pages;                  // The page tree for the root blog or current blog as a hierarchical array
33        var $intersect;                     // Intersect object
34
35        var $unit_test = false;             // Set true to disable die() calls in template loader methods
36
37
38        // ================================================================================================================
39
40
41        function BP_router($args=null) {
42
43                $this->__construct($args);
44        }
45
46        function __construct($args=null) {
47
48                // Handle dependency-injection for unit tests
49                if($args){
50
51                        $this->http_referer = &$args['http_referer'];
52                        $this->request_uri = &$args['request_uri'];
53                        $this->wp_http_referer = &$args['wp_http_referer'];
54
55                        $this->bp = &$args['bp'];
56
57                        $this->wpdb = &$args['wpdb'];
58                        $this->wp_query = &$args['wp_query'];
59                        $this->current_blog = &$args['current_blog'];
60                        $this->current_site = &$args['current_site'];
61
62                        $this->walk = &$args['walk'];
63                        $this->flat_pages = &$args['flat_pages'];
64                        $this->lofted_pages = &$args['lofted_pages'];
65                        $this->intersect = &$args['intersect'];
66
67                }
68                else {
69
70                        global $bp;
71                        global $wpdb, $wp_query, $current_blog, $current_site;
72
73                        $this->http_referer = $_SERVER['HTTP_REFERER'];
74                        $this->request_uri = $_SERVER['REQUEST_URI'];
75                        $this->wp_http_referer = $_REQUEST['_wp_http_referer'];
76
77                        $this->bp = &$bp;
78
79                        $this->wpdb = &$wpdb;
80                        $this->wp_query = &$wp_query;
81                        $this->current_blog = &$current_blog;
82                        $this->current_site = &$current_site;
83
84                        $this->walk = null;
85                        $this->flat_pages = null;
86                        $this->lofted_pages = null;
87                        $this->intersect = null;
88                }
89
90        }
91
92
93        /**
94         * Given a URI owned by BuddyPress, load the correct templates
95         *
96         * @version 1.6
97         * @since 1.6
98         */
99
100        public function route(&$status=null, &$error=null) {
101
102
103                // Reset the global component, action, and item variables
104                // ===============================================================
105
106                $this->bp->current_component = "";
107                $this->bp->current_action = "";
108                $this->bp->current_item = "";
109                $this->bp->action_variables = array();
110                $this->bp->displayed_user->id = null;
111
112                // Don't catch URIs on non-root blogs unless multiblog mode is on
113                // ===============================================================
114
115                if( !bp_is_root_blog() && !bp_is_multiblog_mode() ){
116
117                        $status = array(
118                                'numeric'=>0,   // We normally start numbering at 1 but this is a
119                                                // special case due to the &$status reference from
120                                                // the matchComponent() method
121
122                                'text'=>"Multiblog mode is off and URI was on non-root blog",
123                                'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__
124                        );
125                        return false;
126                }
127
128                // Convert the URI passed by the web server into a walk array
129                // ===============================================================
130
131                $this->walk = self::buildWalk(&$walk_error);
132
133                if($walk_error){
134
135                        $error = array(
136                                'numeric'=>1,
137                                'text'=>"Walk error",
138                                'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__,
139                                'child'=>$walk_error
140                        );
141                        return false;
142                }
143
144                // Intersect the walk array with the site's page tree
145                // ===============================================================
146
147                $this->intersect = self::pageIntersect($this->walk, &$intersect_error);
148
149                if($intersect_error){
150
151                        $error = array(
152                                'numeric'=>2,
153                                'text'=>"Intersect error",
154                                'data'=>array('walk'=>$this->walk),
155                                'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__,
156                                'child'=>$intersect_error
157                        );
158                        return false;
159                }
160
161                // Match the intersect to a BuddyPress component, and set the global BP variables
162                // ===============================================================
163
164                $result = self::matchComponent($this->intersect, &$status, &$match_error);
165
166                if($match_error){
167
168                        $error = array(
169                                'numeric'=>3,
170                                'text'=>"Match error",
171                                'data'=>array('intersect'=>$this->intersect),
172                                'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__,
173                                'child'=>$match_error
174                        );
175                        return false;           
176                }
177
178                return $result;
179
180        }
181
182
183        /**
184         * Given a URI from the web server, create a walk array
185         *
186         * @link http://en.wikipedia.org/wiki/Glossary_of_graph_theory#Walks
187         * @version 1.6
188         * @since 1.6
189         * @return array $walk | Walk array
190         */
191
192        public function buildWalk(&$error=null) {
193
194
195                if ( strpos($this->request_uri, 'wp-load.php') ){
196
197                        // Try to match on the $_REQUEST['_wp_http_referer'] variable
198                        if( !empty($this->_wp_http_referer) ){
199
200                                $ref = $this->_wp_http_referer;
201                        }
202                        // Otherwise, try to match on the $_SERVER['http_referer'] variable
203                        elseif( !empty($this->http_referer) ){
204
205                                $ref = $this->http_referer;
206                        }
207
208                        // If the $_SERVER['request_uri'] variable is NULL, or if the referer
209                        // is pointing to itself, this is not a valid request
210
211                        if($ref == $this->request_uri){
212
213                                $error = array(
214                                        'numeric'=>1,
215                                        'text'=>"Invalid AJAX referer",
216                                        'data'=>array(  "_wp_http_referer"=>$this->_wp_http_referer,
217                                                        "http_referer"=>$this->http_referer,
218                                                        "request_uri"=>$this->request_uri
219                                                     ),
220                                        'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__,
221                                        'child'=>null
222                                );
223                                return false;
224                        }
225
226                        // The $_wp_http_referer and $http_referer variables have the structure
227                        // "http://site.com/foo/bar/baz" so we remove the "http://site.com/" from
228                        // the string to make it have the same structure as $request_uri
229
230                        $referer = explode('/', $ref);
231                        unset($referer[0], $referer[1], $referer[2]);
232                        $raw_uri = implode('/', $referer);
233
234                }
235                else {
236                        // The $request_uri variable has the structure "/foo/bar/baz"
237                        $raw_uri = esc_url($this->request_uri);
238                }
239
240
241                // Parse the URI into an array of tokens
242                // =================================================
243
244                $raw_uri = apply_filters('bp_uri', $raw_uri);
245                $parsed_uri = parse_url($raw_uri);
246
247                if(!$parsed_uri){
248
249                        $error = array(
250                                'numeric'=>2,
251                                'text'=>"Couldn't parse supplied URI string",
252                                'data'=>$raw_uri,
253                                'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__,
254                                'child'=>null
255                        );
256                        return false;
257                }
258
259                // Strip any surplus "/" characters from the URI string, and
260                // explode it into a walk array
261
262                $walk = explode('/', trim($parsed_uri["path"], '/') );
263
264
265                // If BP is running off a non-root blog, remove the
266                // the blog's base path from the beginning of the walk
267                // =================================================
268
269                if( is_multisite() && !is_subdomain_install() && ( bp_is_multiblog_mode() || bp_get_root_blog_id() != 1 ) ){
270
271                        // Any subdirectory names must be removed from $bp_uri. This includes two cases:
272                        // a) when WP is installed in a subdirectory,
273                        // b) when BP is running on secondary blog of a subdirectory multisite install
274
275                        $base_walk = explode( '/', trim($this->current_blog->path, '/') );
276                        $base_count = count($base_walk);
277
278                        if($base_count > 0){
279
280                                // Remove the base tokens from the walk array while
281                                // simultaneously re-basing the array
282
283                                $temp_walk = array();
284
285                                foreach($walk as $index => $token){
286
287                                        if($token == $base_walk[$index]){
288
289                                                $intersect_count++;
290                                        }
291                                        else {
292                                                $temp_walk[] = $token;
293                                        }
294                                }
295                                unset($index, $token);
296
297                                // If any tokens in the base array fail to intersect with
298                                // walk array, this is not a valid URI
299
300                                if($base_count != $intersect_count){
301
302                                        $error = array(
303                                                'numeric'=>3,
304                                                'text'=>"Malformed base URI",
305                                                'data'=>array(
306                                                                "walk"=>$walk,
307                                                                "base_tokens"=>$base_walk,
308                                                                "result"=>$temp_walk
309                                                 ),
310                                                'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__,
311                                                'child'=>null
312                                        );
313                                        return false;
314                                }
315
316                                $walk = $temp_walk;
317
318                        }
319
320                }
321
322                return $walk;
323
324        }
325
326
327        /**
328         * Intersect a walk with the site's pages tree, returning the endpoint id,
329         * endpoint slug, and transect array
330         *
331         * @link http://en.wikipedia.org/wiki/Tree_(graph_theory)
332         * @link http://en.wikipedia.org/wiki/Union_(set_theory)
333         * @version 1.6
334         * @since 1.6
335         * @param array $walk | Walk array
336         * @return array $result | Result array
337         */
338
339        public function pageIntersect($walk, &$error=null) {
340
341
342                // Fetch the site's pages and loft them into a hierarchical tree
343                // ==============================================================
344
345                $this->flat_pages = self::getPageHierarchy(&$pages_error);
346
347                if($pages_error){
348
349                        $error = array(
350                                'numeric'=>1,
351                                'text'=>"Error fetching site pages",
352                                'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__,
353                                'child'=>$pages_error
354                        );
355                        return false;
356                }
357
358                $this->lofted_pages = self::loftHierarchy($this->flat_pages, &$loft_error);
359
360                if($loft_error){
361
362                        $error = array(
363                                'numeric'=>2,
364                                'text'=>"Error lofting pages array",
365                                'data'=>array('flat_pages'=>$this->flat_pages),
366                                'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__,
367                                'child'=>$loft_error
368                        );
369                        return false;
370                }
371
372
373                // Intersect the walk array with the pages tree
374                // ==============================================================
375
376                $intersect = self::walkIntersectTree($walk, $this->lofted_pages, &$intersect_error);
377
378                if($intersect_error){
379
380                        $error = array(
381                                'numeric'=>3,
382                                'text'=>"Error intersecting walk with pages tree",
383                                'data'=>array("walk"=>$walk, "lofted_pages"=>$this->lofted_pages),
384                                'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__,
385                                'child'=>$intersect_error
386                        );
387                        return false;
388                }
389
390                return $intersect;
391
392        }
393
394
395        /**
396         * Determine which BP component (if any) matches a given transect
397         *
398         * @link http://en.wikipedia.org/wiki/Cycle_(graph_theory)
399         * @link http://en.wikipedia.org/wiki/Cycle_detection
400         * @version 1.6
401         * @since 1.6
402         * @param array $intersect | Intersect array
403         * @param array $status | Reason no match was found
404         * @return bool $result | True on match. False on no match.
405         */
406
407        public function matchComponent($intersect, &$status, &$error=null) {
408
409           
410                $transect = $intersect["transect"]; 
411
412                // CASE 1: Front-page component
413                // ====================================================================
414                if( $intersect["endpoint_id"] === null ){
415
416                        // If a component is set to the front page, and the user is not requesting
417                        // a specific post via a URL parameter, we have a match
418
419                        $not_preview_mode = ( empty($_GET['p']) && empty($_GET['page_id']) );
420
421                        if($not_preview_mode){
422
423                                $show_page_on_front = (get_option('show_on_front') == 'page'); // Note comparison operator
424                                $post_id = get_option('page_on_front');
425
426                                if($show_page_on_front && $post_id){
427
428                                        $post = get_post($post_id);
429
430                                        if( !empty($post) ){
431
432                                                $this->bp->current_component = (string)$post->post_name;
433
434                                                $status = array(
435                                                        'numeric'=>1,
436                                                        'text'=>"Successful match on front-page component.",
437                                                        'data'=>array('current_component'=>$this->bp->current_component,
438                                                                      'post_id'=>$post_id,
439                                                                      'post'=>$post ),
440                                                        'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__
441                                                );
442                                        }
443                                        else {
444
445                                                $error = array(
446                                                        'numeric'=>1,
447                                                        'text'=>"Site front page set to component, but component's post was empty",
448                                                        'data'=>array("post_id"=>$post_id),
449                                                        'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__,
450                                                        'child'=>null
451                                                );
452                                                return false;
453                                        }
454                                }
455                        }
456
457                        if(!$route_found){
458
459                                $status = array(
460                                        'numeric'=>2,
461                                        'text'=>"Site front page with no components active on front page.",
462                                        'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__
463                                );
464                                return false;
465                        }
466
467                }
468
469                // CASE 2: Any non-nested component
470                // ====================================================================
471
472                if(!$this->bp->current_component){
473
474                        $this->bp->current_component = self::getPrimaryComponentName($intersect["endpoint_name"], &$primary_component_error);
475
476                        if($primary_component_error){
477
478                                $error = array(
479                                        'numeric'=>2,
480                                        'text'=>"Error fetching primary component name",
481                                        'data'=>array("endpoint_name"=>$intersect["endpoint_name"]),
482                                        'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__,
483                                        'child'=>$primary_component_error
484                                );
485                                return false;
486                        }
487
488                        if($this->bp->current_component){
489
490                                $status = array(
491                                        'numeric'=>3,
492                                        'text'=>"Successful match on primary component",
493                                        'data'=>array('current_component'=>$this->bp->current_component),
494                                        'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__
495                                );
496
497                        }
498                }
499
500
501                // CASE 3: Root profile
502                // ====================================================================
503
504                if (    !$this->bp->current_component                                           // 1) Has not matched a component in an earlier stage
505                        && !empty($transect)                                                    // 2) There are tokens in the transect
506                        && !empty($this->bp->pages->members)                                    // 3) Members component is active
507                        && defined( 'BP_ENABLE_ROOT_PROFILES' ) && BP_ENABLE_ROOT_PROFILES )    // 4) Root profiles constant is defined and true
508                {
509
510                        // Shift the user name off the transect
511                        $user_name = array_shift($transect);
512
513                        // Switch the user_id based on compatibility mode
514                        if( bp_is_username_compatibility_mode() ){
515
516                                $user_id = (int) bp_core_get_userid( urldecode($user_name) );
517                        }
518                        else {
519                                $user_id = (int) bp_core_get_userid_from_nicename( urldecode($user_name) );
520                        }
521
522                        if($user_id){
523
524                                $this->bp->current_component = "members";
525                                $this->bp->displayed_user->id = $user_id;
526
527                                $status = array(
528                                        'numeric'=>4,
529                                        'text'=>"Successful match on root profile",
530                                        'data'=>array('current_component'=>$this->bp->current_component),
531                                        'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__
532                                );
533
534                                // Without the 'members' URL chunk, WordPress won't know which page to load,
535                                // so this filter intercepts the WP query and tells it to load the members page
536
537                                $function_string  = '$query_args["pagename"] = "';
538                                $function_string .= $this->bp->pages->members->name;
539                                $function_string .= '"; return $query_args;';
540
541                                add_filter( 'request', create_function('$query_args', $function_string) );
542
543                        }
544                        else {
545
546                                $status = array(
547                                        'numeric'=>5,
548                                        'text'=>"Root profiles enabled. No matching user.",
549                                        'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__
550                                );
551                                return false;
552                        }
553
554                }
555
556                // CASE 4: No match
557                // ====================================================================
558
559                if(!$this->bp->current_component){
560
561                        $status = array(
562                                'numeric'=>6,
563                                'text'=>"No matching components",
564                                'data'=>array('intersect'=>$this->intersect, 'walk'=>$this->walk),
565                                'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__
566                        );
567                        return false;
568                }
569
570                // Members Component secondary processing
571                // ====================================================================
572
573                if( ($this->bp->current_component == "members") && !empty($transect) ){
574
575                        // If the component is "members", the transect must either contain no tokens (show all users on site),
576                        // or the first token in the transect must be a valid user name (show single user)
577
578                        $user_name = array_shift($transect);
579
580                        // Switch the user_id based on compatibility mode
581                        if( bp_is_username_compatibility_mode() ){
582
583                                $user_id = (int) bp_core_get_userid( urldecode($user_name) );
584                        }
585                        else {
586                                $user_id = (int) bp_core_get_userid_from_nicename( urldecode($user_name) );
587                        }
588
589                        // CASE 1: Token in first transect position isn't a valid user_id
590                        // ---------------------------------------------------------------------------------------
591                        if( empty($user_id) ){
592
593                                $this->bp->current_component = null;    // Prevent components from loading their templates
594                                bp_do_404();
595
596                                $status = array(
597                                        'numeric'=>7,
598                                        'text'=>"Match on members component, but user_id is not valid.",
599                                        'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__
600                                );
601                                return false;
602
603                        }
604
605                        elseif( !empty($user_id) ){ 
606
607                                $this->bp->displayed_user->id = $user_id;
608
609                                // CASE 2: Token in first transect position matches a user_id that
610                                // has been marked as a spammer
611                                // ---------------------------------------------------------------------------------------
612                                if( bp_core_is_user_spammer($user_id) ){                               
613
614                                        if( is_super_admin() ){
615
616                                                bp_core_add_message( __( 'This user has been marked as a spammer. Only site admins can view this profile.', 'buddypress' ), 'error' );
617                                        }
618                                        else {
619                                                // If the user viewing the profile is not a super-admin, hide the page
620                                                bp_do_404();
621
622                                                $status = array(
623                                                        'numeric'=>8,
624                                                        'text'=>"Match on members component, but user_id is marked as a spammer and viewer is not a super-admin.",
625                                                        'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__
626                                                );
627                                                return false;
628                                        }
629
630                                }
631                                // CASE 3: There are one or more tokens left in the transect after the user_name has
632                                // been shifted-out. This means we have a secondary component nested inside the members
633                                // component. The secondary component's *slug* will be the first token in the transect. We
634                                // have to set $this->bp->current_component to the *name* of the secondary component so
635                                // BP loads the correct template chain.
636                                // ---------------------------------------------------------------------------------------
637                                elseif( count($transect) > 0) { 
638
639                                        $current_component_slug = array_shift($transect);
640
641                                        // CASE 3A: Match against the "primary" components that can exist both as a top-level
642                                        // page and a secondary page nested beneath the "members" component. External plugins
643                                        // following the "BuddyPress Example Component" pattern will appear in this array.
644                                        //
645                                        // TODO: This creates a cardinality problem. Primary components will appear at
646                                        // both "example.com/members/membername/slug_name" and "example.com/slug_name". This
647                                        // is further complicated by the fact that some components use the alias location as a
648                                        // *context*, for example, "activity" at the root node shows activity for all users on
649                                        // the site, but "activity" nested in the "members" component shows activity for a user.
650                                        // There needs to be a set of configuration options on the admin back-end to specify
651                                        // which location to use for a given component. Note that this is a legacy problem with
652                                        // the original BP router design and we have emulated it for compatibility.
653                                        // ---------------------------------------------------------------------------------------
654
655                                        $this->bp->current_component = self::getPrimaryComponentName($current_component_slug, &$primary_component_error);
656
657                                        if($primary_component_error){
658
659                                                $error = array(
660                                                        'numeric'=>3,
661                                                        'text'=>"Error fetching primary component name",
662                                                        'data'=>array("current_component_slug"=>$current_component_slug),
663                                                        'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__,
664                                                        'child'=>$primary_component_error
665                                                );
666                                                return false;
667                                        }
668
669                                        if($this->bp->current_component != null){
670
671                                                $status = array(
672                                                        'numeric'=>9,
673                                                        'text'=>"Match on members component with primary nested component",
674                                                        'data'=>array(  'bp_pages'=>$this->bp->pages,
675                                                                        'active_components'=>$this->bp->active_components,
676                                                                        'current_component_slug'=>$current_component_slug,
677                                                                        "component"=>$this->bp->current_component),
678                                                        'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__
679                                                );
680                                        }
681                                        else {
682                                       
683                                                // CASE 3B: Match against the "secondary" components that can only exist as a secondary
684                                                // page nested beneath the "members" component. Matching is determined by the component's
685                                                // action functions, which hook on the 'bp_init' action. Action functions are located
686                                                // in "/component_name/bp-component_name-actions.php".
687                                                // ---------------------------------------------------------------------------------------
688
689                                                $this->bp->current_component = $current_component_slug;
690                                               
691                                                $status = array(
692                                                        'numeric'=>10,
693                                                        'text'=>"Match on members component, with possible match on secondary nested component",
694                                                        'data'=>array(  'bp_pages'=>$this->bp->pages,
695                                                                        'active_components'=>$this->bp->active_components,
696                                                                        'current_component_slug'=>$current_component_slug),
697                                                        'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__
698                                                );
699                                               
700
701                                        }
702                                       
703                                }
704                                // CASE 4: There are no tokens left in the transect, so we're at the default screen
705                                // in the members component. Set $this->bp->current_component to the default profile
706                                // component (defined in bp-members-loader.php line 113)
707                                // ---------------------------------------------------------------------------------------
708                                else { 
709                                        $this->bp->current_component = $this->bp->default_component;
710                                       
711                                        $status = array(
712                                                'numeric'=>11,
713                                                'text'=>"Match on members component with no nested component",
714                                                'data'=>array("component"=>$this->bp->current_component),
715                                                'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__
716                                        );
717                                }
718                               
719
720                        }
721
722
723                }
724
725
726                // Set BP's global variables
727                // ====================================================================
728
729                if( isset($transect[0]) ){
730
731                        $this->bp->current_action = array_shift($transect);
732
733                            if( count($transect) > 0 ){
734
735                                    $this->bp->action_variables = $transect;
736                            }
737                       
738                }
739
740                // Set WP global variables
741                // ====================================================================
742                if( !empty($object_id) ){
743
744                        // Set WP's internal query variables to the same state they would be in if
745                        // WP had loaded the page itself instead of BP intercepting the page load
746                        // and replacing it with our own content
747
748                        // TODO: We've emulated this for compatibility. BP should try to avoid
749                        // doing this unless actually necessary, because it costs an extra query on
750                        // each page load.
751
752                        $this->wp_query->queried_object_id = $this->intersect["endpoint_id"];
753                        $this->wp_query->queried_object    = &get_post($this->intersect["endpoint_id"]);
754
755                }
756
757        }
758
759
760        /**
761         * Returns a flat array of the site's page hierarchy
762         *
763         * @version 1.6
764         * @since 1.6
765         * @return array $result | Page hierarchy as flat array
766         */
767
768        public function getPageHierarchy(&$error=null) {
769
770                // TODO: Add caching capabilities
771           
772                global $wpdb;
773
774                // Always get page data from the root blog, except on multiblog mode, when it comes
775                // from the current blog
776
777                if( bp_is_multiblog_mode() ){
778
779                        $posts_table_name = $wpdb->posts;
780                }
781                else {
782                        $posts_table_name = $wpdb->get_blog_prefix( bp_get_root_blog_id() ) . 'posts';
783                }
784               
785                $sql = "SELECT ID, post_name, post_parent, post_title FROM {$posts_table_name} WHERE post_type = 'page' AND post_status != 'auto-draft'";
786                $pages = $wpdb->get_results($sql);
787
788                // Trap any database errors
789                $sql_error = mysql_error($wpdb->dbh);
790
791                if($sql_error){
792
793                        $error = array(
794                                'numeric'=>1,
795                                'text'=>"Database error",
796                                'data'=>array($sql, $sql_error),
797                                'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__,
798                                'child'=>null
799                        );
800
801                        return false;
802                }
803
804                // Spin the SQL server's output into a useful format
805                $result = array();
806
807                foreach($pages as $page){
808
809                        $result[$page->ID] = array( "parent"=>$page->post_parent,
810                                                    "slug"=>$page->post_name,
811                                                    "title"=>$page->post_title
812                                             );
813                }
814                unset($page);
815
816                return $result;
817
818        }
819
820       
821        /**
822         * Lofts a flat array of nodes into a rooted directed tree in O(n) time
823         * with only O(n) extra memory. This is also known as the "in-place quick
824         * union" algorithm.
825         *
826         * @link http://en.wikipedia.org/wiki/Tree_(graph_theory)
827         * @link http://en.wikipedia.org/wiki/Glossary_of_graph_theory#Walks
828         * @link http://en.wikipedia.org/wiki/Quicksort (in-place version)
829         *
830         * @version 1.6
831         * @since 1.6
832         * @param array $nodes | Flat array of nodes
833         * @return array $result | Hierarchical array of nodes
834         */
835
836        public function loftHierarchy($nodes) {
837
838                $tree = array();
839
840                foreach( $nodes as $node_id => $data){
841
842                        // Note: we can operate directly on the passed parameter, because unless
843                        // explicitly told not to by using the "&$" sigil, PHP passes copies
844                        // of variables into a function.
845
846                        $nodes[$node_id]["node_id"] = $node_id;     // Insert the node_id into each node to make the data
847                                                                    // structure easier to use. Note the unit tests are very
848                                                                    // picky about the order this gets done in because it
849                                                                    // affects its position in the output array.
850                        if( empty($data["parent"]) ){
851
852                                $tree["children"][$node_id] =& $nodes[$node_id];
853                        }
854                        else {
855                                $nodes[$data["parent"]]["children"][$node_id] =& $nodes[$node_id];
856                        }
857                }
858
859                return $tree;
860        }
861
862
863        /**
864         * Finds the longest intersect between a walk and a tree.
865         *
866         * @link http://en.wikipedia.org/wiki/Glossary_of_graph_theory#Walks
867         * @link http://en.wikipedia.org/wiki/Breadth-first_search
868         *
869         * @version 1.6
870         * @since 1.6
871         * @param array $walk | Walk array
872         * @param array $tree | Tree array
873         * @return array $result | Walk key and matching node id
874         */
875
876        public function walkIntersectTree($walk, $tree, &$error=null) {
877
878
879                if( !is_array($walk) ){
880
881                        $error = array(
882                                'numeric'=>1,
883                                'text'=>"Walk is not a valid array",
884                                'data'=>array( "walk"=>$walk, "tree"=>$tree),
885                                'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__,
886                                'child'=>null
887                        );
888                        return false;
889                }
890
891                if( !is_array($tree) ){
892
893                        $error = array(
894                                'numeric'=>2,
895                                'text'=>"Tree is not a valid array",
896                                'data'=>array( "walk"=>$walk, "tree"=>$tree),
897                                'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__,
898                                'child'=>null
899                        );
900                        return false;
901                }
902               
903
904                // Loop through each child node, searching for the
905                // child node with the longest walk
906                // ================================================
907
908                $min_offset = null;
909                $min_node = null;
910
911                foreach( $tree["children"] as $node_id => $data){
912
913                        if($data["slug"] == $walk[0]){
914
915                                $reduced_walk = array_slice($walk, 1);
916                                $intersect = self::walkIntersectTree_iterator($reduced_walk, $data);
917
918                                if( ($min_offset === null) || ($intersect["walk_offset"] < $min_offset) ){
919
920                                        $min_offset = $intersect["walk_offset"];
921                                        $min_node = $intersect["node_id"];
922                                }
923                        }
924
925                }
926
927                // Return the child node with the longest walk, or if
928                // there was no matching child node, return this node
929                // ================================================
930
931                if($min_offset === null){
932
933                        $result = array(
934                                            "endpoint_id"=>null,
935                                            "endpoint_name"=>null,
936                                            "walk_key"=>null,
937                                            "transect"=>array()
938                        );
939                }
940                else {
941
942                        // Convert offset to array key number so functions further down
943                        // the chain can use array_slice() to find the tokens after the
944                        // endpoint that correspond to actions/arguements (if they exist)
945
946                        $walk_key = (count($walk) - $min_offset) - 1;
947
948                        $result = array(
949                                            "endpoint_id" => $min_node,
950                                            "endpoint_name"=>$walk[$walk_key],
951                                            "walk_key" => $walk_key,
952                                            "transect"=>array_slice($walk, ($walk_key +1) )
953                        );
954                }
955
956                return $result;
957
958        }
959
960
961        /**
962         * Finds the longest intersect between the walk and the tree.
963         *
964         * @link http://en.wikipedia.org/wiki/Glossary_of_graph_theory#Walks
965         * @link http://en.wikipedia.org/wiki/Breadth-first_search
966         *
967         * @version 1.6
968         * @since 1.6
969         * @param array $walk | Walk array
970         * @param array $tree | Tree array
971         * @return array $result | Walk offset and matching node id
972         */
973
974        public function walkIntersectTree_iterator($walk, $tree) {
975
976
977                // Calculate offsets
978                // ================================================
979
980                $walk_offset = count($walk);
981
982                if( is_array($tree["children"]) ){
983
984                        $children_count = count($tree["children"]);
985                }
986                else {
987                        $children_count = 0;
988                }
989
990                // If either termination condition is met, return
991                // ================================================
992
993                if( ($walk_offset == 0) || ($children_count == 0) ){
994
995                        $result = array(    "node_id"=>$tree["node_id"],
996                                            "walk_offset"=>$walk_offset
997                        );
998
999                        return $result;
1000                }
1001
1002                // Loop through each child node, searching for the
1003                // child node with the longest walk
1004                // ================================================
1005
1006                $min_offset = null;
1007                $min_node = null;
1008
1009                foreach( $tree["children"] as $node_id => $data){
1010
1011                        if($data["slug"] == $walk[0]){
1012
1013                                $reduced_walk = array_slice($walk, 1);
1014                                $intersect = self::walkIntersectTree_iterator($reduced_walk, $data);
1015
1016                                if( ($min_offset === null) || ($intersect["walk_offset"] < $min_offset) ){
1017
1018                                        $min_offset = $intersect["walk_offset"];
1019                                        $min_node = $intersect["node_id"];
1020                                }
1021                        }
1022
1023                }
1024
1025                // Return the child node with the longest walk, or if
1026                // there was no matching child node, return this node
1027                // ================================================
1028
1029                if($min_offset === null){
1030
1031                        $result = array(
1032                                            "node_id"=>$tree["node_id"],
1033                                            "walk_offset"=>$walk_offset
1034                        );
1035                }
1036                else {
1037                        $result = array(
1038                                            "node_id"=>$min_node,
1039                                            "walk_offset"=>$min_offset
1040                        );
1041                }
1042
1043                return $result;
1044
1045        }
1046
1047
1048        /**
1049         * Checks if a slug matches an active "primary" BuddyPress component. Primary components
1050         * are components which can exist as a top-level page on the site, and in some cases
1051         * a secondary page nested below the "members" component. Third-party components following
1052         * the "BuddyPress Example Component" pattern will appear in the results.
1053         *
1054         * @version 1.6
1055         * @since 1.6
1056         * @param string $slug | Name of slug to check
1057         * @return bool/string $result | False on failure. Null on nonexistent. Name of component on success.
1058         */
1059
1060        public function getPrimaryComponentName($slug, &$error=null) {
1061
1062           
1063                if( empty($slug) ){
1064
1065                        $error = array(
1066                                'numeric'=>1,
1067                                'text'=>"Called with empty slug",
1068                                'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__,
1069                                'child'=>null
1070                        );
1071                        return false;
1072                }
1073
1074                // If the BP Pages object hasn't been loaded yet, try to load it
1075                if( empty($this->bp->pages) ){
1076
1077                        $this->bp->pages = self::buildDirectoryPages($this->flat_pages);
1078                }
1079
1080                if( empty($this->bp->pages) ){
1081
1082                        $error = array(
1083                                'numeric'=>2,
1084                                'text'=>"Failed to load BP pages object",
1085                                'data'=>array("bp_pages"=>$this->bp->pages),
1086                                'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__,
1087                                'child'=>null
1088                        );
1089                        return false;
1090                }
1091
1092
1093                foreach($this->bp->pages as $component_name => $data){
1094
1095                        // NOTE: We cannot use an algorithm that checks against $this->bp->active_components,
1096                        // because its impossible for 3rd-party components to add themselves to this array
1097                        // using the 'bp_active_components' filter. The filter is placed so early in the call
1098                        // stack it runs before 3rd-party components can load any of their plugin files.
1099
1100                        if( !array_key_exists($component_name, $this->bp->deactivated_components)   // 1) Component is active
1101                            && $data->name == $slug )                                               // 2) Slug matches
1102                        {
1103                                return $component_name;
1104                        }
1105                }
1106                unset($component_name, $data);
1107
1108                // Separate check for search component (because its not a real BP component,
1109                // and its not included in the $bp->active_components array)
1110
1111                if($slug == bp_get_search_slug()){
1112
1113                        return "search";
1114                }
1115
1116                return null;
1117               
1118        }
1119
1120        /**
1121         * Generates the BP component pages array
1122         *
1123         * @version 1.6
1124         * @since 1.6
1125         * @param array $flat_pages | Flat array of all WordPress pages on the site
1126         * @return obj $pages | Structured object containing page ID's, Names, and Slugs
1127         */
1128        function buildDirectoryPages($flat_pages, &$error=null) {
1129
1130
1131                if( empty($flat_pages) ){
1132
1133                        $error = array(
1134                                'numeric'=>1,
1135                                'text'=>"Called with empty flat_pages array",
1136                                'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__,
1137                                'child'=>null
1138                        );
1139                        return false;
1140                }
1141
1142
1143                $page_ids = (array)bp_core_get_directory_page_ids();
1144
1145                if( empty($page_ids) ){
1146
1147                        $error = array(
1148                                'numeric'=>2,
1149                                'text'=>"BP core directory page ids option is empty",
1150                                'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__,
1151                                'child'=>null
1152                        );
1153                        return false;
1154                }
1155       
1156
1157                $pages = new stdClass;
1158
1159                // Iterate through each entry in the BP pages config option
1160                foreach( $page_ids as $component_id => $bp_page_id ) {
1161
1162                        // Iterate through each WP site page in the flat pages array
1163                        foreach( $flat_pages as $wp_page_id => $data ) {
1164
1165                                // If the page ids match, add this page to the components array
1166                                if( $wp_page_id == $bp_page_id ) {
1167
1168                                        $pages->{$component_id}->name  = $data['slug'];
1169                                        $pages->{$component_id}->id    = $wp_page_id;
1170                                        $pages->{$component_id}->title = $data['title'];
1171
1172                                        $stem = array();
1173                                        $stem[] = $data['slug'];
1174
1175                                        $parent = $data['parent'];
1176
1177                                        // If the page is not attached to the root node, traverse the page tree backwards to the
1178                                        // root node generating the reverse walk, then flip it and implode it to a string.
1179
1180                                        while( $parent != 0 ){
1181
1182                                                $stem[] = $flat_pages[$parent]['slug'];
1183                                                $parent = $flat_pages[$parent]['parent'];
1184                                        }
1185
1186                                        // TODO: BuddyPress incorrectly calls this a "slug", which is confusing. The correct term
1187                                        // is a "stem" (in string form) and a "walk" (in array form).
1188
1189                                        $pages->{$component_id}->slug = implode( '/', array_reverse( (array)$stem ) );
1190                                }
1191
1192                                unset($slug);
1193                        }
1194                        unset($wp_page_id, $data);
1195
1196                }
1197                unset($component_id, $bp_page_id);
1198       
1199                return apply_filters( 'bp_core_get_directory_pages', $pages );
1200               
1201        }
1202
1203
1204        /**
1205         * Load a specific template file, with fallback support.
1206         *
1207         * Example: bp_core_load_template( 'members/index' );
1208         * Loads: wp-content/themes/[activated_theme]/members/index.php
1209         *
1210         * @version 1.6
1211         * @since 1.6
1212         * @param string/array $templates | Single template name as string. Multiple template names as array of string.
1213         * @return bool/die $result | False on failure. Loads template and terminates thread on success.
1214         */
1215        function loadTemplate($templates, &$error=null) {
1216
1217
1218                if( !$this->intersect["endpoint_id"] ){
1219
1220                        $error = array(
1221                                'numeric'=>1,
1222                                'text'=>"Cannot load template because router was unable to intersect the current
1223                                         request URI with any pages in the site's page tree.",
1224                                'data'=>array("intersect"=>$this->intersect,
1225                                              "walk"=>$this->walk,
1226                                              "templates"=>$templates),
1227                                'file'=>__FILE__, 'line'=>__LINE__, 'method'=>__METHOD__,
1228                                'child'=>null
1229                        );
1230                        return false;
1231                }
1232
1233                // Add a ".php" suffix to each template file in the $templates array
1234                foreach( (array)$templates as $template ){
1235                   
1236                        $filtered_templates[] = $template . '.php';
1237                }
1238
1239                // Filter the template locations so that plugins can alter where they are located
1240                $located_template = apply_filters( 'bp_located_template', locate_template( (array) $filtered_templates, false ), $filtered_templates );
1241
1242                if($located_template){
1243
1244                        // Explicitly set WP's internal query variables to the correct state (because the
1245                        // default is to 404 the page)
1246
1247                        $this->wp_query->is_page = true;
1248                        $this->wp_query->is_404 = false;
1249
1250                        // Explicitly set the HTTP headers. Note that this only sets the headers for the web
1251                        // page. The web server generates its own headers for individual items such as images
1252                        // and CSS stylesheets loaded by the page.
1253
1254                        $protocol = $_SERVER["SERVER_PROTOCOL"];
1255                        $code = 200;
1256                        $text = "OK";
1257
1258                        if( ($protocol != 'HTTP/1.1') && ($protocol != 'HTTP/1.0') ){
1259
1260                                $protocol = 'HTTP/1.0';
1261                        }
1262
1263                        $status_header = "$protocol $code $text";
1264
1265                        header($status_header, true, $code);
1266
1267                        load_template( apply_filters( 'bp_load_template', $located_template ) );
1268
1269                }
1270
1271                if(!$this->unit_test){
1272
1273                        die;
1274
1275                        // TODO: It's bad practice to place silent die() calls all over an application's code because it
1276                        // makes it very difficult to unit-test.
1277                        //
1278                        // The die() call above prevents WordPress from loading the template for the WP page we hijacked. If
1279                        // it was removed, the system would display the BP version of the page, followed by the WP version of
1280                        // the page. A better solution might be to set WP's internal variables so that it thinks it's successfully
1281                        // loaded the page, resulting in no further output and correctly running all the WP shutdown processes.
1282
1283                }
1284
1285        }
1286
1287
1288       
1289} // End class BP_router
1290
1291
1292
1293// BRIDGE FUNCTIONS
1294// ========================================================================================================
1295// These functions allow legacy code to access the new router class
1296
1297
1298function bp_core_set_uri_globals(){
1299
1300        global $bp;
1301        $bp->router = new BP_router();
1302
1303        $result = $bp->router->route(&$status, &$error);
1304        return $result;
1305}
1306
1307function bp_core_load_template($templates){
1308
1309        global $bp;
1310        $result = $bp->router->loadTemplate($templates, &$error);
1311        return $result;
1312}
1313
1314// ========================================================================================================
1315
1316
1317/**
1318 * Are root profiles enabled and allowed
1319 *
1320 * @since BuddyPress (1.6)
1321 * @return bool True if yes, false if no
1322 */
1323function bp_core_enable_root_profiles() {
1324
1325        $retval = false;
1326
1327        if ( defined( 'BP_ENABLE_ROOT_PROFILES' ) && ( true == BP_ENABLE_ROOT_PROFILES ) )
1328                $retval = true;
1329
1330        return apply_filters( 'bp_core_enable_root_profiles', $retval );
1331}
1332
1333/**
1334 * bp_core_catch_profile_uri()
1335 *
1336 * If the extended profiles component is not installed we still need
1337 * to catch the /profile URI's and display whatever we have installed.
1338 *
1339 */
1340function bp_core_catch_profile_uri() {
1341        if ( !bp_is_active( 'xprofile' ) ) {
1342                bp_core_load_template( apply_filters( 'bp_core_template_display_profile', 'members/single/home' ) );
1343        }
1344}
1345
1346/**
1347 * Catches invalid access to BuddyPress pages and redirects them accordingly.
1348 *
1349 * @package BuddyPress Core
1350 * @since BuddyPress (1.5)
1351 */
1352function bp_core_catch_no_access() {
1353        global $bp, $wp_query;
1354
1355        // If coming from bp_core_redirect() and $bp_no_status_set is true,
1356        // we are redirecting to an accessible page so skip this check.
1357        if ( !empty( $bp->no_status_set ) )
1358                return false;
1359
1360        if ( !isset( $wp_query->queried_object ) && !bp_is_blog_page() ) {
1361                bp_do_404();
1362        }
1363}
1364add_action( 'bp_template_redirect', 'bp_core_catch_no_access', 1 );
1365
1366/**
1367 * Redirects a user to login for BP pages that require access control and adds an error message (if
1368 * one is provided).
1369 * If authenticated, redirects user back to requested content by default.
1370 *
1371 * @package BuddyPress Core
1372 * @since BuddyPress (1.5)
1373 */
1374function bp_core_no_access( $args = '' ) {
1375
1376        // Build the redirect URL
1377        $redirect_url  = is_ssl() ? 'https://' : 'http://';
1378        $redirect_url .= $_SERVER['HTTP_HOST'];
1379        $redirect_url .= $_SERVER['REQUEST_URI'];
1380
1381        $defaults = array(
1382                'mode'     => '1',                  // 1 = $root, 2 = wp-login.php
1383                'redirect' => $redirect_url,        // the URL you get redirected to when a user successfully logs in
1384                'root'     => bp_get_root_domain(),     // the landing page you get redirected to when a user doesn't have access
1385                'message'  => __( 'You must log in to access the page you requested.', 'buddypress' )
1386        );
1387
1388        $r = wp_parse_args( $args, $defaults );
1389        $r = apply_filters( 'bp_core_no_access', $r );
1390        extract( $r, EXTR_SKIP );
1391
1392        /**
1393         * @ignore Ignore these filters and use 'bp_core_no_access' above
1394         */
1395        $mode           = apply_filters( 'bp_no_access_mode',     $mode,     $root,     $redirect, $message );
1396        $redirect       = apply_filters( 'bp_no_access_redirect', $redirect, $root,     $message,  $mode    );
1397        $root           = apply_filters( 'bp_no_access_root',     $root,     $redirect, $message,  $mode    );
1398        $message        = apply_filters( 'bp_no_access_message',  $message,  $root,     $redirect, $mode    );
1399        $root       = trailingslashit( $root );
1400
1401        switch ( $mode ) {
1402
1403                // Option to redirect to wp-login.php
1404                // Error message is displayed with bp_core_no_access_wp_login_error()
1405                case 2 :
1406                        if ( !empty( $redirect ) ) {
1407                                bp_core_redirect( add_query_arg( array( 'action' => 'bpnoaccess' ), wp_login_url( $redirect ) ) );
1408                        } else {
1409                                bp_core_redirect( $root );
1410                        }
1411
1412                        break;
1413
1414                // Redirect to root with "redirect_to" parameter
1415                // Error message is displayed with bp_core_add_message()
1416                case 1 :
1417                default :
1418
1419                        $url = $root;
1420                        if ( !empty( $redirect ) )
1421                                $url = add_query_arg( 'redirect_to', urlencode( $redirect ), $root );
1422
1423                        if ( !empty( $message ) ) {
1424                                bp_core_add_message( $message, 'error' );
1425                        }
1426
1427                        bp_core_redirect( $url );
1428
1429                        break;
1430        }
1431}
1432
1433/**
1434 * Adds an error message to wp-login.php.
1435 * Hooks into the "bpnoaccess" action defined in bp_core_no_access().
1436 *
1437 * @package BuddyPress Core
1438 * @global $error
1439 * @since BuddyPress (1.5)
1440 */
1441function bp_core_no_access_wp_login_error() {
1442
1443        global $error;
1444
1445        $error = apply_filters( 'bp_wp_login_error', __( 'You must log in to access the page you requested.', 'buddypress' ), $_REQUEST['redirect_to'] );
1446
1447        // Shake the login box to show there was an error
1448        add_action( 'login_head', 'wp_shake_js', 12 );
1449
1450}
1451add_action( 'login_form_bpnoaccess', 'bp_core_no_access_wp_login_error' );
1452
1453
1454/**
1455 * Canonicalizes BuddyPress URLs
1456 *
1457 * This function ensures that requests for BuddyPress content are always redirected to their
1458 * canonical versions. Canonical versions are always trailingslashed, and are typically the most
1459 * general possible versions of the URL - eg, example.com/groups/mygroup/ instead of
1460 * example.com/groups/mygroup/home/
1461 *
1462 * @since 1.6
1463 * @see BP_Members_Component::setup_globals() where $bp->canonical_stack['base_url'] and
1464 *   ['component'] may be set
1465 * @see bp_core_new_nav_item() where $bp->canonical_stack['action'] may be set
1466 */
1467function bp_redirect_canonical() {
1468
1469        global $bp;
1470
1471        if ( !bp_is_blog_page() && apply_filters( 'bp_do_redirect_canonical', true ) ) {
1472
1473                // If this is a POST request, don't do a canonical redirect.
1474                // This is for backward compatibility with plugins that submit form requests to
1475                // non-canonical URLs. Plugin authors should do their best to use canonical URLs in
1476                // their form actions.
1477
1478                if ( !empty( $_POST ) ) {
1479                        return;
1480                }
1481
1482                // Build the URL in the address bar
1483                $requested_url  = is_ssl() ? 'https://' : 'http://';
1484                $requested_url .= $_SERVER['HTTP_HOST'];
1485                $requested_url .= $_SERVER['REQUEST_URI'];
1486
1487                // Stash query args
1488                $url_stack      = explode( '?', $requested_url );
1489                $req_url_clean  = $url_stack[0];
1490
1491                // Build the canonical URL out of the redirect stack
1492                if ( isset( $bp->canonical_stack['base_url'] ) ) {
1493                        $url_stack[0] = $bp->canonical_stack['base_url'];
1494                }
1495
1496                if ( isset( $bp->canonical_stack['component'] ) ) {
1497                        $url_stack[0] = trailingslashit( $url_stack[0] . $bp->canonical_stack['component'] );
1498                }
1499
1500                if ( isset( $bp->canonical_stack['action'] ) ) {
1501                        $url_stack[0] = trailingslashit( $url_stack[0] . $bp->canonical_stack['action'] );
1502                }
1503
1504                if ( !empty( $bp->canonical_stack['action_variables'] ) ) {
1505                        foreach( (array) $bp->canonical_stack['action_variables'] as $av ) {
1506                                $url_stack[0] = trailingslashit( $url_stack[0] . $av );
1507                        }
1508                }
1509
1510                // Add trailing slash
1511                $url_stack[0] = trailingslashit( $url_stack[0] );
1512
1513                // Only redirect if we've assembled a URL different from the request
1514                if ( $url_stack[0] !== $req_url_clean ) {
1515
1516                        // Template messages have been deleted from the cookie by this point, so
1517                        // they must be readded before redirecting
1518                        if ( isset( $bp->template_message ) ) {
1519                                $message      = stripslashes( $bp->template_message );
1520                                $message_type = isset( $bp->template_message_type ) ? $bp->template_message_type : 'success';
1521
1522                                bp_core_add_message( $message, $message_type );
1523                        }
1524
1525                        bp_core_redirect( implode( '?', $url_stack ), 301 );
1526                }
1527        }
1528}
1529
1530
1531/**
1532 * Output rel=canonical header tag for BuddyPress content
1533 *
1534 * @since 1.6
1535 */
1536function bp_rel_canonical() {
1537
1538        // Build the URL in the address bar
1539        $requested_url  = is_ssl() ? 'https://' : 'http://';
1540        $requested_url .= $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
1541
1542        // Stash query args
1543        $url_stack      = explode( '?', $requested_url );
1544
1545        // Build the canonical URL out of the redirect stack
1546        if ( isset( $bp->canonical_stack['base_url'] ) )
1547                $url_stack[0] = $bp->canonical_stack['base_url'];
1548
1549        if ( isset( $bp->canonical_stack['component'] ) )
1550                $url_stack[0] = trailingslashit( $url_stack[0] . $bp->canonical_stack['component'] );
1551
1552        if ( isset( $bp->canonical_stack['action'] ) )
1553                $url_stack[0] = trailingslashit( $url_stack[0] . $bp->canonical_stack['action'] );
1554
1555        if ( !empty( $bp->canonical_stack['action_variables'] ) ) {
1556                foreach( (array) $bp->canonical_stack['action_variables'] as $av ) {
1557                        $url_stack[0] = trailingslashit( $url_stack[0] . $av );
1558                }
1559        }
1560
1561        // Add trailing slash
1562        $url_stack[0] = trailingslashit( $url_stack[0] );
1563
1564        // Output rel=canonical tag
1565        echo "<link rel='canonical' href='" . esc_attr( $url_stack[0] ) . "' />\n";
1566}
1567
1568
1569/**
1570 * Remove WordPress's really awesome canonical redirect if we are trying to load
1571 * BuddyPress specific content. Avoids issues with WordPress thinking that a
1572 * BuddyPress URL might actually be a blog post or page.
1573 *
1574 * This function should be considered temporary, and may be removed without
1575 * notice in future versions of BuddyPress.
1576 *
1577 * @since BuddyPress (1.6)
1578 * @uses bp_is_blog_page()
1579 */
1580function _bp_maybe_remove_redirect_canonical() {
1581
1582        if ( ! bp_is_blog_page() )
1583                remove_action( 'template_redirect', 'redirect_canonical' );
1584
1585}
1586add_action( 'bp_init', '_bp_maybe_remove_redirect_canonical' );
1587
1588
1589/**
1590 * Remove WordPress's rel=canonical HTML tag if we are trying to load BuddyPress
1591 * specific content.
1592 *
1593 * This function should be considered temporary, and may be removed without
1594 * notice in future versions of BuddyPress.
1595 *
1596 * @since 1.6
1597 */
1598function _bp_maybe_remove_rel_canonical() {
1599
1600        if ( ! bp_is_blog_page() && ! is_404() ) {
1601                remove_action( 'wp_head', 'rel_canonical' );
1602                add_action( 'bp_head', 'bp_rel_canonical' );
1603        }
1604
1605}
1606add_action( 'wp_head', '_bp_maybe_remove_rel_canonical', 8 );
1607
1608
1609?>