WordPress.org

Make WordPress Core

Changeset 43437 for trunk


Ignore:
Timestamp:
07/11/2018 06:22:10 AM (17 months ago)
Author:
pento
Message:

REST API: Declare user capabilities using JSON Hyper Schema's "targetSchema".

There are a variety of operations a WordPress user can only perform if they have the correct capabilities. A REST API client should only display UI for one of these operations if the WordPress user can perform the operation.

Rather than requiring REST API clients to calculate whether to display UI based on potentially complicated combinations of user capabilities, targetSchema allows us to expose a single flag to show whether the corresponding UI should be displayed.

This change also includes flags on post objects for the following actions:

  • action-publish: The current user can publish this post.
  • action-sticky: The current user can make this post sticky, and the post type supports sticking.
  • `action-assign-author': The current user can change the author on this post.
  • action-assign-{$taxonomy}: The current user can assign terms from the "$taxonomy" taxonomy to this post.
  • action-create-{$taxonomy}: The current user can create terms int the "$taxonomy" taxonomy.

Props TimothyBlynJacobs, danielbachhuber.
Fixes #44287.

Location:
trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php

    r43087 r43437  
    369369        $data = $this->filter_response_by_context( $data, $context );
    370370
     371        $links = $response->get_links();
     372
    371373        // Wrap the data in a response object.
    372374        $response = rest_ensure_response( $data );
    373 
    374         $response->add_links( $this->prepare_links( $post ) );
     375        $response->add_links( $links );
    375376
    376377        /**
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php

    r43087 r43437  
    15911591        $response = rest_ensure_response( $data );
    15921592
    1593         $response->add_links( $this->prepare_links( $post ) );
     1593        $links = $this->prepare_links( $post );
     1594        $response->add_links( $links );
     1595
     1596        if ( ! empty( $links['self']['href'] ) ) {
     1597            $actions = $this->get_available_actions( $post, $request );
     1598
     1599            $self = $links['self']['href'];
     1600
     1601            foreach ( $actions as $rel ) {
     1602                $response->add_link( $rel, $self );
     1603            }
     1604        }
    15941605
    15951606        /**
     
    17281739
    17291740        return $links;
     1741    }
     1742
     1743    /**
     1744     * Get the link relations available for the post and current user.
     1745     *
     1746     * @since 4.9.7
     1747     *
     1748     * @param WP_Post $post Post object.
     1749     * @param WP_REST_Request Request object.
     1750     *
     1751     * @return array List of link relations.
     1752     */
     1753    protected function get_available_actions( $post, $request ) {
     1754
     1755        if ( 'edit' !== $request['context'] ) {
     1756            return array();
     1757        }
     1758
     1759        $rels = array();
     1760
     1761        $post_type = get_post_type_object( $post->post_type );
     1762
     1763        if ( 'attachment' !== $this->post_type && current_user_can( $post_type->cap->publish_posts ) ) {
     1764            $rels[] = 'https://api.w.org/action-publish';
     1765        }
     1766
     1767        if ( 'post' === $post_type->name ) {
     1768            if ( current_user_can( $post_type->cap->edit_others_posts ) && current_user_can( $post_type->cap->publish_posts ) ) {
     1769                $rels[] = 'https://api.w.org/action-sticky';
     1770            }
     1771        }
     1772
     1773        if ( post_type_supports( $post_type->name, 'author' ) ) {
     1774            if ( current_user_can( $post_type->cap->edit_others_posts ) ) {
     1775                $rels[] = 'https://api.w.org/action-assign-author';
     1776            }
     1777        }
     1778
     1779        $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
     1780
     1781        foreach ( $taxonomies as $tax ) {
     1782            $tax_base   = ! empty( $tax->rest_base ) ? $tax->rest_base : $tax->name;
     1783            $create_cap = is_taxonomy_hierarchical( $tax->name ) ? $tax->cap->edit_terms : $tax->cap->assign_terms;
     1784
     1785            if ( current_user_can( $create_cap ) ) {
     1786                $rels[] = 'https://api.w.org/action-create-' . $tax_base;
     1787            }
     1788
     1789            if ( current_user_can( $tax->cap->assign_terms ) ) {
     1790                $rels[] = 'https://api.w.org/action-assign-' . $tax_base;
     1791            }
     1792        }
     1793
     1794        return $rels;
    17301795    }
    17311796
     
    20702135        }
    20712136
     2137        $schema_links = $this->get_schema_links();
     2138
     2139        if ( $schema_links ) {
     2140            $schema['links'] = $schema_links;
     2141        }
     2142
    20722143        return $this->add_additional_fields_schema( $schema );
     2144    }
     2145
     2146    /**
     2147     * Retrieve Link Description Objects that should be added to the Schema for the posts collection.
     2148     *
     2149     * @since 4.9.7
     2150     *
     2151     * @return array
     2152     */
     2153    protected function get_schema_links() {
     2154
     2155        $href = rest_url( "{$this->namespace}/{$this->rest_base}/{id}" );
     2156
     2157        $links = array();
     2158
     2159        if ( 'attachment' !== $this->post_type ) {
     2160            $links[] = array(
     2161                'rel'          => 'https://api.w.org/action-publish',
     2162                'title'        => __( 'The current user can publish this post.' ),
     2163                'href'         => $href,
     2164                'targetSchema' => array(
     2165                    'type'       => 'object',
     2166                    'properties' => array(
     2167                        'status' => array(
     2168                            'type' => 'string',
     2169                            'enum' => array( 'publish', 'future' ),
     2170                        ),
     2171                    ),
     2172                ),
     2173            );
     2174        }
     2175
     2176        if ( 'post' === $this->post_type ) {
     2177            $links[] = array(
     2178                'rel'          => 'https://api.w.org/action-sticky',
     2179                'title'        => __( 'The current user can sticky this post.' ),
     2180                'href'         => $href,
     2181                'targetSchema' => array(
     2182                    'type'       => 'object',
     2183                    'properties' => array(
     2184                        'sticky' => array(
     2185                            'type' => 'boolean',
     2186                        ),
     2187                    ),
     2188                ),
     2189            );
     2190        }
     2191
     2192        if ( post_type_supports( $this->post_type, 'author' ) ) {
     2193            $links[] = array(
     2194                'rel'          => 'https://api.w.org/action-assign-author',
     2195                'title'        => __( 'The current user can change the author on this post.' ),
     2196                'href'         => $href,
     2197                'targetSchema' => array(
     2198                    'type'       => 'object',
     2199                    'properties' => array(
     2200                        'author' => array(
     2201                            'type' => 'integer',
     2202                        ),
     2203                    ),
     2204                ),
     2205            );
     2206        }
     2207
     2208        $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
     2209
     2210        foreach ( $taxonomies as $tax ) {
     2211            $tax_base = ! empty( $tax->rest_base ) ? $tax->rest_base : $tax->name;
     2212
     2213            /* translators: %s: taxonomy name */
     2214            $assign_title = sprintf( __( 'The current user can assign terms in the %s taxonomy.' ), $tax->name );
     2215            /* translators: %s: taxonomy name */
     2216            $create_title = sprintf( __( 'The current user can create terms in the %s taxonomy.' ), $tax->name );
     2217
     2218            $links[] = array(
     2219                'rel'          => 'https://api.w.org/action-assign-' . $tax_base,
     2220                'title'        => $assign_title,
     2221                'href'         => $href,
     2222                'targetSchema' => array(
     2223                    'type'       => 'object',
     2224                    'properties' => array(
     2225                        $tax_base => array(
     2226                            'type'  => 'array',
     2227                            'items' => array(
     2228                                'type' => 'integer',
     2229                            ),
     2230                        ),
     2231                    ),
     2232                ),
     2233            );
     2234
     2235            $links[] = array(
     2236                'rel'          => 'https://api.w.org/action-create-' . $tax_base,
     2237                'title'        => $create_title,
     2238                'href'         => $href,
     2239                'targetSchema' => array(
     2240                    'type'       => 'object',
     2241                    'properties' => array(
     2242                        $tax_base => array(
     2243                            'type'  => 'array',
     2244                            'items' => array(
     2245                                'type' => 'integer',
     2246                            ),
     2247                        ),
     2248                    ),
     2249                ),
     2250            );
     2251        }
     2252
     2253        return $links;
    20732254    }
    20742255
  • trunk/tests/phpunit/tests/rest-api/rest-attachments-controller.php

    r43087 r43437  
    13511351    }
    13521352
     1353    public function test_links_exist() {
     1354
     1355        wp_set_current_user( self::$editor_id );
     1356
     1357        $post = self::factory()->attachment->create( array( 'post_author' => self::$editor_id ) );
     1358        $this->assertGreaterThan( 0, $post );
     1359
     1360        $request = new WP_REST_Request( 'GET', "/wp/v2/media/{$post}" );
     1361        $request->set_query_params( array( 'context' => 'edit' ) );
     1362
     1363        $response = rest_get_server()->dispatch( $request );
     1364        $links    = $response->get_links();
     1365
     1366        $this->assertArrayHasKey( 'self', $links );
     1367    }
     1368
     1369    public function test_publish_action_ldo_not_registered() {
     1370
     1371        $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/media' ) );
     1372        $data     = $response->get_data();
     1373        $schema   = $data['schema'];
     1374
     1375        $this->assertArrayHasKey( 'links', $schema );
     1376        $publish = wp_list_filter( $schema['links'], array( 'rel' => 'https://api.w.org/action-publish' ) );
     1377
     1378        $this->assertCount( 0, $publish, 'LDO not found on schema.' );
     1379    }
     1380
     1381    public function test_publish_action_link_does_not_exists() {
     1382
     1383        wp_set_current_user( self::$editor_id );
     1384
     1385        $post = self::factory()->attachment->create( array( 'post_author' => self::$editor_id ) );
     1386        $this->assertGreaterThan( 0, $post );
     1387
     1388        $request = new WP_REST_Request( 'GET', "/wp/v2/media/{$post}" );
     1389        $request->set_query_params( array( 'context' => 'edit' ) );
     1390
     1391        $response = rest_get_server()->dispatch( $request );
     1392        $links    = $response->get_links();
     1393
     1394        $this->assertArrayNotHasKey( 'https://api.w.org/action-publish', $links );
     1395    }
     1396
    13531397    public function tearDown() {
    13541398        parent::tearDown();
  • trunk/tests/phpunit/tests/rest-api/rest-posts-controller.php

    r43087 r43437  
    36413641    }
    36423642
     3643    public function test_publish_action_ldo_registered() {
     3644
     3645        $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' ) );
     3646        $data     = $response->get_data();
     3647        $schema   = $data['schema'];
     3648
     3649        $this->assertArrayHasKey( 'links', $schema );
     3650        $publish = wp_list_filter( $schema['links'], array( 'rel' => 'https://api.w.org/action-publish' ) );
     3651
     3652        $this->assertCount( 1, $publish, 'LDO found on schema.' );
     3653    }
     3654
     3655    public function test_sticky_action_ldo_registered_for_posts() {
     3656
     3657        $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' ) );
     3658        $data     = $response->get_data();
     3659        $schema   = $data['schema'];
     3660
     3661        $this->assertArrayHasKey( 'links', $schema );
     3662        $publish = wp_list_filter( $schema['links'], array( 'rel' => 'https://api.w.org/action-sticky' ) );
     3663
     3664        $this->assertCount( 1, $publish, 'LDO found on schema.' );
     3665    }
     3666
     3667    public function test_sticky_action_ldo_not_registered_for_non_posts() {
     3668
     3669        $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/pages' ) );
     3670        $data     = $response->get_data();
     3671        $schema   = $data['schema'];
     3672
     3673        $this->assertArrayHasKey( 'links', $schema );
     3674        $publish = wp_list_filter( $schema['links'], array( 'rel' => 'https://api.w.org/action-sticky' ) );
     3675
     3676        $this->assertCount( 0, $publish, 'LDO found on schema.' );
     3677    }
     3678
     3679    public function test_author_action_ldo_registered_for_post_types_with_author_support() {
     3680
     3681        $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' ) );
     3682        $data     = $response->get_data();
     3683        $schema   = $data['schema'];
     3684
     3685        $this->assertArrayHasKey( 'links', $schema );
     3686        $publish = wp_list_filter( $schema['links'], array( 'rel' => 'https://api.w.org/action-assign-author' ) );
     3687
     3688        $this->assertCount( 1, $publish, 'LDO found on schema.' );
     3689    }
     3690
     3691    public function test_author_action_ldo_not_registered_for_post_types_without_author_support() {
     3692
     3693        remove_post_type_support( 'post', 'author' );
     3694
     3695        $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' ) );
     3696        $data     = $response->get_data();
     3697        $schema   = $data['schema'];
     3698
     3699        $this->assertArrayHasKey( 'links', $schema );
     3700        $publish = wp_list_filter( $schema['links'], array( 'rel' => 'https://api.w.org/action-assign-author' ) );
     3701
     3702        $this->assertCount( 0, $publish, 'LDO found on schema.' );
     3703    }
     3704
     3705    public function test_term_action_ldos_registered() {
     3706
     3707        $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' ) );
     3708        $data     = $response->get_data();
     3709        $schema   = $data['schema'];
     3710
     3711        $this->assertArrayHasKey( 'links', $schema );
     3712        $rels = array_flip( wp_list_pluck( $schema['links'], 'rel' ) );
     3713
     3714        $this->assertArrayHasKey( 'https://api.w.org/action-assign-categories', $rels );
     3715        $this->assertArrayHasKey( 'https://api.w.org/action-create-categories', $rels );
     3716        $this->assertArrayHasKey( 'https://api.w.org/action-assign-tags', $rels );
     3717        $this->assertArrayHasKey( 'https://api.w.org/action-create-tags', $rels );
     3718
     3719        $this->assertArrayNotHasKey( 'https://api.w.org/action-assign-post_format', $rels );
     3720        $this->assertArrayNotHasKey( 'https://api.w.org/action-create-post_format', $rels );
     3721        $this->assertArrayNotHasKey( 'https://api.w.org/action-assign-nav_menu', $rels );
     3722        $this->assertArrayNotHasKey( 'https://api.w.org/action-create-nav_menu', $rels );
     3723    }
     3724
     3725    public function test_action_links_only_available_in_edit_context() {
     3726
     3727        wp_set_current_user( self::$author_id );
     3728
     3729        $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) );
     3730        $this->assertGreaterThan( 0, $post );
     3731
     3732        $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
     3733        $request->set_query_params( array( 'context' => 'view' ) );
     3734
     3735        $response = rest_get_server()->dispatch( $request );
     3736        $links    = $response->get_links();
     3737
     3738        $this->assertArrayNotHasKey( 'https://api.w.org/action-publish', $links );
     3739    }
     3740
     3741    public function test_publish_action_link_exists_for_author() {
     3742
     3743        wp_set_current_user( self::$author_id );
     3744
     3745        $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) );
     3746        $this->assertGreaterThan( 0, $post );
     3747
     3748        $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
     3749        $request->set_query_params( array( 'context' => 'edit' ) );
     3750
     3751        $response = rest_get_server()->dispatch( $request );
     3752        $links    = $response->get_links();
     3753
     3754        $this->assertArrayHasKey( 'https://api.w.org/action-publish', $links );
     3755    }
     3756
     3757    public function test_publish_action_link_does_not_exist_for_contributor() {
     3758
     3759        wp_set_current_user( self::$contributor_id );
     3760
     3761        $post = self::factory()->post->create( array( 'post_author' => self::$contributor_id ) );
     3762        $this->assertGreaterThan( 0, $post );
     3763
     3764        $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
     3765        $request->set_query_params( array( 'context' => 'edit' ) );
     3766
     3767        $response = rest_get_server()->dispatch( $request );
     3768        $links    = $response->get_links();
     3769
     3770        $this->assertArrayNotHasKey( 'https://api.w.org/action-publish', $links );
     3771    }
     3772
     3773    public function test_sticky_action_exists_for_editor() {
     3774
     3775        wp_set_current_user( self::$editor_id );
     3776
     3777        $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) );
     3778        $this->assertGreaterThan( 0, $post );
     3779
     3780        $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
     3781        $request->set_query_params( array( 'context' => 'edit' ) );
     3782
     3783        $response = rest_get_server()->dispatch( $request );
     3784        $links    = $response->get_links();
     3785
     3786        $this->assertArrayHasKey( 'https://api.w.org/action-sticky', $links );
     3787    }
     3788
     3789    public function test_sticky_action_does_not_exist_for_author() {
     3790
     3791        wp_set_current_user( self::$author_id );
     3792
     3793        $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) );
     3794        $this->assertGreaterThan( 0, $post );
     3795
     3796        $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
     3797        $request->set_query_params( array( 'context' => 'edit' ) );
     3798
     3799        $response = rest_get_server()->dispatch( $request );
     3800        $links    = $response->get_links();
     3801
     3802        $this->assertArrayNotHasKey( 'https://api.w.org/action-sticky', $links );
     3803    }
     3804
     3805    public function test_sticky_action_does_not_exist_for_non_post_posts() {
     3806
     3807        wp_set_current_user( self::$editor_id );
     3808
     3809        $post = self::factory()->post->create(
     3810            array(
     3811                'post_author' => self::$author_id,
     3812                'post_type'   => 'page',
     3813            )
     3814        );
     3815        $this->assertGreaterThan( 0, $post );
     3816
     3817        $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
     3818        $request->set_query_params( array( 'context' => 'edit' ) );
     3819
     3820        $response = rest_get_server()->dispatch( $request );
     3821        $links    = $response->get_links();
     3822
     3823        $this->assertArrayNotHasKey( 'https://api.w.org/action-sticky', $links );
     3824    }
     3825
     3826
     3827    public function test_assign_author_action_exists_for_editor() {
     3828
     3829        wp_set_current_user( self::$editor_id );
     3830
     3831        $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) );
     3832        $this->assertGreaterThan( 0, $post );
     3833
     3834        $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
     3835        $request->set_query_params( array( 'context' => 'edit' ) );
     3836
     3837        $response = rest_get_server()->dispatch( $request );
     3838        $links    = $response->get_links();
     3839
     3840        $this->assertArrayHasKey( 'https://api.w.org/action-assign-author', $links );
     3841    }
     3842
     3843    public function test_assign_author_action_does_not_exist_for_author() {
     3844
     3845        wp_set_current_user( self::$author_id );
     3846
     3847        $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) );
     3848        $this->assertGreaterThan( 0, $post );
     3849
     3850        $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
     3851        $request->set_query_params( array( 'context' => 'edit' ) );
     3852
     3853        $response = rest_get_server()->dispatch( $request );
     3854        $links    = $response->get_links();
     3855
     3856        $this->assertArrayNotHasKey( 'https://api.w.org/action-assign-author', $links );
     3857    }
     3858
     3859    public function test_assign_author_action_does_not_exist_for_post_types_without_author_support() {
     3860
     3861        remove_post_type_support( 'post', 'author' );
     3862
     3863        wp_set_current_user( self::$editor_id );
     3864
     3865        $post = self::factory()->post->create();
     3866        $this->assertGreaterThan( 0, $post );
     3867
     3868        $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
     3869        $request->set_query_params( array( 'context' => 'edit' ) );
     3870
     3871        $response = rest_get_server()->dispatch( $request );
     3872        $links    = $response->get_links();
     3873
     3874        $this->assertArrayNotHasKey( 'https://api.w.org/action-assign-author', $links );
     3875    }
     3876
     3877    public function test_create_term_action_exists_for_editor() {
     3878
     3879        wp_set_current_user( self::$editor_id );
     3880
     3881        $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) );
     3882        $this->assertGreaterThan( 0, $post );
     3883
     3884        $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
     3885        $request->set_query_params( array( 'context' => 'edit' ) );
     3886
     3887        $response = rest_get_server()->dispatch( $request );
     3888        $links    = $response->get_links();
     3889
     3890        $this->assertArrayHasKey( 'https://api.w.org/action-create-categories', $links );
     3891        $this->assertArrayHasKey( 'https://api.w.org/action-create-tags', $links );
     3892        $this->assertArrayNotHasKey( 'https://api.w.org/action-create-post_format', $links );
     3893    }
     3894
     3895    public function test_create_term_action_non_hierarchical_exists_for_author() {
     3896
     3897        wp_set_current_user( self::$author_id );
     3898
     3899        $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) );
     3900        $this->assertGreaterThan( 0, $post );
     3901
     3902        $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
     3903        $request->set_query_params( array( 'context' => 'edit' ) );
     3904
     3905        $response = rest_get_server()->dispatch( $request );
     3906        $links    = $response->get_links();
     3907
     3908        $this->assertArrayHasKey( 'https://api.w.org/action-create-tags', $links );
     3909    }
     3910
     3911    public function test_create_term_action_hierarchical_does_not_exists_for_author() {
     3912
     3913        wp_set_current_user( self::$author_id );
     3914
     3915        $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) );
     3916        $this->assertGreaterThan( 0, $post );
     3917
     3918        $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
     3919        $request->set_query_params( array( 'context' => 'edit' ) );
     3920
     3921        $response = rest_get_server()->dispatch( $request );
     3922        $links    = $response->get_links();
     3923
     3924        $this->assertArrayNotHasKey( 'https://api.w.org/action-create-categories', $links );
     3925    }
     3926
     3927    public function test_assign_term_action_exists_for_contributor() {
     3928
     3929        wp_set_current_user( self::$contributor_id );
     3930
     3931        $post = self::factory()->post->create(
     3932            array(
     3933                'post_author' => self::$contributor_id,
     3934                'post_status' => 'draft',
     3935            )
     3936        );
     3937        $this->assertGreaterThan( 0, $post );
     3938
     3939        $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
     3940        $request->set_query_params( array( 'context' => 'edit' ) );
     3941
     3942        $response = rest_get_server()->dispatch( $request );
     3943        $links    = $response->get_links();
     3944
     3945        $this->assertArrayHasKey( 'https://api.w.org/action-assign-categories', $links );
     3946        $this->assertArrayHasKey( 'https://api.w.org/action-assign-tags', $links );
     3947    }
     3948
    36433949    public function tearDown() {
    36443950        _unregister_post_type( 'youseeeme' );
Note: See TracChangeset for help on using the changeset viewer.