Make WordPress Core

Changeset 40628


Ignore:
Timestamp:
05/11/2017 06:18:00 PM (8 years ago)
Author:
jnylen0
Message:

REST API: Add endpoint for proxying requests to external oEmbed providers.

This endpoint is a prerequisite for the media widgets work (see https://github.com/xwp/wp-core-media-widgets).

Also use the new endpoint in the media modal instead of the parse-embed AJAX action.

Props westonruter, timmydcrawford, swissspidy, jnylen0.
Fixes #40450.

Location:
trunk
Files:
8 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-oembed.php

    r40574 r40628  
    320320
    321321    /**
     322     * Takes a URL and attempts to return the oEmbed data.
     323     *
     324     * @see WP_oEmbed::fetch()
     325     *
     326     * @since 4.8.0
     327     * @access public
     328     *
     329     * @param string       $url  The URL to the content that should be attempted to be embedded.
     330     * @param array|string $args Optional. Arguments, usually passed from a shortcode. Default empty.
     331     * @return false|object False on failure, otherwise the result in the form of an object.
     332     */
     333    public function get_data( $url, $args = '' ) {
     334        $args = wp_parse_args( $args );
     335
     336        $provider = $this->get_provider( $url, $args );
     337
     338        if ( ! $provider ) {
     339            return false;
     340        }
     341
     342        $data = $this->fetch( $provider, $url, $args );
     343
     344        if ( false === $data ) {
     345            return false;
     346        }
     347
     348        return $data;
     349    }
     350
     351    /**
    322352     * The do-it-all function that takes a URL and attempts to return the HTML.
    323353     *
     
    333363     */
    334364    public function get_html( $url, $args = '' ) {
    335         $args = wp_parse_args( $args );
    336 
    337365        /**
    338366         * Filters the oEmbed result before any HTTP requests are made.
     
    356384        }
    357385
    358         $provider = $this->get_provider( $url, $args );
    359 
    360         if ( ! $provider || false === $data = $this->fetch( $provider, $url, $args ) ) {
     386        $data = $this->get_data( $url, $args );
     387
     388        if ( false === $data ) {
    361389            return false;
    362390        }
  • trunk/src/wp-includes/class-wp-oembed-controller.php

    r37568 r40628  
    5353            ),
    5454        ) );
     55
     56        register_rest_route( 'oembed/1.0', '/proxy', array(
     57            array(
     58                'methods'  => WP_REST_Server::READABLE,
     59                'callback' => array( $this, 'get_proxy_item' ),
     60                'permission_callback' => array( $this, 'get_proxy_item_permissions_check' ),
     61                'args'     => array(
     62                    'url'      => array(
     63                        'description'       => __( 'The URL of the resource for which to fetch oEmbed data.' ),
     64                        'type'              => 'string',
     65                        'required'          => true,
     66                        'sanitize_callback' => 'esc_url_raw',
     67                    ),
     68                    'format'   => array(
     69                        'description'       => __( 'The oEmbed format to use.' ),
     70                        'type'              => 'string',
     71                        'default'           => 'json',
     72                        'enum'              => array(
     73                            'json',
     74                            'xml',
     75                        ),
     76                    ),
     77                    'maxwidth' => array(
     78                        'description'       => __( 'The maximum width of the embed frame in pixels.' ),
     79                        'type'              => 'integer',
     80                        'default'           => $maxwidth,
     81                        'sanitize_callback' => 'absint',
     82                    ),
     83                    'maxheight' => array(
     84                        'description'       => __( 'The maximum height of the embed frame in pixels.' ),
     85                        'type'              => 'integer',
     86                        'sanitize_callback' => 'absint',
     87                    ),
     88                    'discover' => array(
     89                        'description'       => __( 'Whether to perform an oEmbed discovery request for non-whitelisted providers.' ),
     90                        'type'              => 'boolean',
     91                        'default'           => true,
     92                    ),
     93                ),
     94            ),
     95        ) );
    5596    }
    5697
    5798    /**
    58      * Callback for the API endpoint.
     99     * Callback for the embed API endpoint.
    59100     *
    60101     * Returns the JSON object for the post.
     
    87128        return $data;
    88129    }
     130
     131    /**
     132     * Checks if current user can make a proxy oEmbed request.
     133     *
     134     * @since 4.8.0
     135     * @access public
     136     *
     137     * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
     138     */
     139    public function get_proxy_item_permissions_check() {
     140        if ( ! current_user_can( 'edit_posts' ) ) {
     141            return new WP_Error( 'rest_forbidden', __( 'Sorry, you are not allowed to make proxied oEmbed requests.' ), array( 'status' => rest_authorization_required_code() ) );
     142        }
     143        return true;
     144    }
     145
     146    /**
     147     * Callback for the proxy API endpoint.
     148     *
     149     * Returns the JSON object for the proxied item.
     150     *
     151     * @since 4.8.0
     152     * @access public
     153     *
     154     * @see WP_oEmbed::get_html()
     155     * @param WP_REST_Request $request Full data about the request.
     156     * @return WP_Error|array oEmbed response data or WP_Error on failure.
     157     */
     158    public function get_proxy_item( $request ) {
     159        $args = $request->get_params();
     160
     161        // Serve oEmbed data from cache if set.
     162        $cache_key = 'oembed_' . md5( serialize( $args ) );
     163        $data = get_transient( $cache_key );
     164        if ( ! empty( $data ) ) {
     165            return $data;
     166        }
     167
     168        $url = $request['url'];
     169        unset( $args['url'] );
     170
     171        $data = _wp_oembed_get_object()->get_data( $url, $args );
     172
     173        if ( false === $data ) {
     174            return new WP_Error( 'oembed_invalid_url', get_status_header_desc( 404 ), array( 'status' => 404 ) );
     175        }
     176
     177        /**
     178         * Filters the oEmbed TTL value (time to live).
     179         *
     180         * Similar to the {@see 'oembed_ttl'} filter, but for the REST API
     181         * oEmbed proxy endpoint.
     182         *
     183         * @since 4.8.0
     184         *
     185         * @param int    $time    Time to live (in seconds).
     186         * @param string $url     The attempted embed URL.
     187         * @param array  $args    An array of embed request arguments.
     188         */
     189        $ttl = apply_filters( 'rest_oembed_ttl', DAY_IN_SECONDS, $url, $args );
     190
     191        set_transient( $cache_key, $data, $ttl );
     192
     193        return $data;
     194    }
    89195}
  • trunk/src/wp-includes/js/media-views.js

    r40573 r40628  
    46254625
    46264626    fetch: function() {
    4627         var embed;
    46284627
    46294628        // check if they haven't typed in 500 ms
     
    46364635        }
    46374636
    4638         embed = new wp.shortcode({
    4639             tag: 'embed',
    4640             attrs: _.pick( this.model.attributes, [ 'width', 'height', 'src' ] ),
    4641             content: this.model.get('url')
    4642         });
    4643 
    46444637        this.dfd = $.ajax({
    4645             type:    'POST',
    4646             url:     wp.ajax.settings.url,
    4647             context: this,
    4648             data:    {
    4649                 action: 'parse-embed',
    4650                 post_ID: wp.media.view.settings.post.id,
    4651                 shortcode: embed.string()
    4652             }
     4638            url: wp.media.view.settings.oEmbedProxyUrl,
     4639            data: {
     4640                url: this.model.get( 'url' ),
     4641                maxwidth: this.model.get( 'width' ),
     4642                maxheight: this.model.get( 'height' ),
     4643                _wpnonce: wp.media.view.settings.nonce.wpRestApi
     4644            },
     4645            type: 'GET',
     4646            dataType: 'json',
     4647            context: this
    46534648        })
    4654             .done( this.renderoEmbed )
     4649            .done( function( response ) {
     4650                this.renderoEmbed( {
     4651                    data: {
     4652                        body: response.html || ''
     4653                    }
     4654                } );
     4655            } )
    46554656            .fail( this.renderFail );
    46564657    },
  • trunk/src/wp-includes/js/media/views/embed/link.js

    r33337 r40628  
    3636
    3737    fetch: function() {
    38         var embed;
    3938
    4039        // check if they haven't typed in 500 ms
     
    4746        }
    4847
    49         embed = new wp.shortcode({
    50             tag: 'embed',
    51             attrs: _.pick( this.model.attributes, [ 'width', 'height', 'src' ] ),
    52             content: this.model.get('url')
    53         });
    54 
    5548        this.dfd = $.ajax({
    56             type:    'POST',
    57             url:     wp.ajax.settings.url,
    58             context: this,
    59             data:    {
    60                 action: 'parse-embed',
    61                 post_ID: wp.media.view.settings.post.id,
    62                 shortcode: embed.string()
    63             }
     49            url: wp.media.view.settings.oEmbedProxyUrl,
     50            data: {
     51                url: this.model.get( 'url' ),
     52                maxwidth: this.model.get( 'width' ),
     53                maxheight: this.model.get( 'height' ),
     54                _wpnonce: wp.media.view.settings.nonce.wpRestApi
     55            },
     56            type: 'GET',
     57            dataType: 'json',
     58            context: this
    6459        })
    65             .done( this.renderoEmbed )
     60            .done( function( response ) {
     61                this.renderoEmbed( {
     62                    data: {
     63                        body: response.html || ''
     64                    }
     65                } );
     66            } )
    6667            .fail( this.renderFail );
    6768    },
  • trunk/src/wp-includes/media.php

    r40573 r40628  
    34153415        'nonce'     => array(
    34163416            'sendToEditor' => wp_create_nonce( 'media-send-to-editor' ),
     3417            'wpRestApi'    => wp_create_nonce( 'wp_rest' ),
    34173418        ),
    34183419        'post'    => array(
     
    34243425            'video' => ( $show_video_playlist ) ? 1 : 0,
    34253426        ),
     3427        'oEmbedProxyUrl' => rest_url( 'oembed/1.0/proxy' ),
    34263428        'embedExts'    => $exts,
    34273429        'embedMimes'   => $ext_mimes,
  • trunk/tests/phpunit/tests/oembed/controller.php

    r40564 r40628  
    1010     */
    1111    protected $server;
     12    protected static $editor;
     13    protected static $subscriber;
     14    const YOUTUBE_VIDEO_ID = 'OQSNhk5ICTI';
     15    const INVALID_OEMBED_URL = 'https://www.notreallyanoembedprovider.com/watch?v=awesome-cat-video';
     16
     17    public static function wpSetUpBeforeClass( $factory ) {
     18        self::$subscriber = $factory->user->create( array(
     19            'role' => 'subscriber',
     20        ) );
     21        self::$editor = $factory->user->create( array(
     22            'role'       => 'editor',
     23            'user_email' => 'editor@example.com',
     24        ) );
     25    }
     26
     27    public static function wpTearDownAfterClass() {
     28        self::delete_user( self::$subscriber );
     29        self::delete_user( self::$editor );
     30    }
    1231
    1332    public function setUp() {
     
    1938
    2039        do_action( 'rest_api_init', $this->server );
     40        add_filter( 'pre_http_request', array( $this, 'mock_embed_request' ), 10, 3 );
     41        $this->request_count = 0;
     42    }
     43
     44    public function tearDown() {
     45        parent::tearDown();
     46
     47        remove_filter( 'pre_http_request', array( $this, 'mock_embed_request' ), 10, 3 );
     48    }
     49
     50    /**
     51     * Count of the number of requests attempted.
     52     *
     53     * @var int
     54     */
     55    public $request_count = 0;
     56
     57    /**
     58     * Intercept oEmbed requests and mock responses.
     59     *
     60     * @param mixed  $preempt Whether to preempt an HTTP request's return value. Default false.
     61     * @param mixed  $r       HTTP request arguments.
     62     * @param string $url     The request URL.
     63     * @return array Response data.
     64     */
     65    public function mock_embed_request( $preempt, $r, $url ) {
     66        unset( $preempt, $r );
     67
     68        $this->request_count += 1;
     69
     70        // Mock request to YouTube Embed.
     71        if ( false !== strpos( $url, self::YOUTUBE_VIDEO_ID ) ) {
     72            return array(
     73                'response' => array(
     74                    'code' => 200,
     75                ),
     76                'body' => wp_json_encode(
     77                    array(
     78                        'version'          => '1.0',
     79                        'type'             => 'video',
     80                        'provider_name'    => 'YouTube',
     81                        'provider_url'     => 'https://www.youtube.com',
     82                        'thumbnail_width'  => 480,
     83                        'width'            => 500,
     84                        'thumbnail_height' => 360,
     85                        'html'             => '<iframe width="500" height="375" src="https://www.youtube.com/embed/' . self::YOUTUBE_VIDEO_ID . '?feature=oembed" frameborder="0" allowfullscreen></iframe>',
     86                        'author_name'      => 'Yosemitebear62',
     87                        'thumbnail_url'    => 'https://i.ytimg.com/vi/' . self::YOUTUBE_VIDEO_ID . '/hqdefault.jpg',
     88                        'title'            => 'Yosemitebear Mountain Double Rainbow 1-8-10',
     89                        'height'           => 375,
     90                    )
     91                ),
     92            );
     93        } else {
     94            return array(
     95                'response' => array(
     96                    'code' => 404,
     97                ),
     98            );
     99        }
    21100    }
    22101
     
    87166        $this->assertArrayHasKey( 'methods', $route[0] );
    88167        $this->assertArrayHasKey( 'args', $route[0] );
     168
     169        // Check proxy route registration.
     170        $this->assertArrayHasKey( '/oembed/1.0/proxy', $filtered_routes );
     171        $proxy_route = $filtered_routes['/oembed/1.0/proxy'];
     172        $this->assertCount( 1, $proxy_route );
     173        $this->assertArrayHasKey( 'callback', $proxy_route[0] );
     174        $this->assertArrayHasKey( 'permission_callback', $proxy_route[0] );
     175        $this->assertArrayHasKey( 'methods', $proxy_route[0] );
     176        $this->assertArrayHasKey( 'args', $proxy_route[0] );
    89177    }
    90178
     
    349437        update_option( 'permalink_structure', '' );
    350438    }
     439
     440    public function test_proxy_without_permission() {
     441        // Test without a login.
     442        $request = new WP_REST_Request( 'GET', '/oembed/1.0/proxy' );
     443        $response = $this->server->dispatch( $request );
     444
     445        $this->assertEquals( 400, $response->get_status() );
     446
     447        // Test with a user that does not have edit_posts capability.
     448        wp_set_current_user( self::$subscriber );
     449        $request = new WP_REST_Request( 'GET', '/oembed/1.0/proxy' );
     450        $request->set_param( 'url', self::INVALID_OEMBED_URL );
     451        $response = $this->server->dispatch( $request );
     452
     453        $this->assertEquals( 403, $response->get_status() );
     454        $data = $response->get_data();
     455        $this->assertEquals( $data['code'], 'rest_forbidden' );
     456    }
     457
     458    public function test_proxy_with_invalid_oembed_provider() {
     459        wp_set_current_user( self::$editor );
     460        $request = new WP_REST_Request( 'GET', '/oembed/1.0/proxy' );
     461        $request->set_param( 'url', self::INVALID_OEMBED_URL );
     462        $response = $this->server->dispatch( $request );
     463        $this->assertEquals( 404, $response->get_status() );
     464        $data = $response->get_data();
     465        $this->assertEquals( 'oembed_invalid_url', $data['code'] );
     466    }
     467
     468    public function test_proxy_with_invalid_type() {
     469        wp_set_current_user( self::$editor );
     470        $request = new WP_REST_Request( 'GET', '/oembed/1.0/proxy' );
     471        $request->set_param( 'type', 'xml' );
     472        $response = $this->server->dispatch( $request );
     473
     474        $this->assertEquals( 400, $response->get_status() );
     475        $data = $response->get_data();
     476    }
     477
     478    public function test_proxy_with_valid_oembed_provider() {
     479        wp_set_current_user( self::$editor );
     480
     481        $request = new WP_REST_Request( 'GET', '/oembed/1.0/proxy' );
     482        $request->set_param( 'url', 'https://www.youtube.com/watch?v=' . self::YOUTUBE_VIDEO_ID );
     483        $response = $this->server->dispatch( $request );
     484        $this->assertEquals( 200, $response->get_status() );
     485        $this->assertEquals( 1, $this->request_count );
     486
     487        // Subsequent request is cached and so it should not cause a request.
     488        $response = $this->server->dispatch( $request );
     489        $this->assertEquals( 1, $this->request_count );
     490
     491        // Test data object.
     492        $data = $response->get_data();
     493
     494        $this->assertNotEmpty( $data );
     495        $this->assertTrue( is_object( $data ) );
     496        $this->assertEquals( 'YouTube', $data->provider_name );
     497        $this->assertEquals( 'https://i.ytimg.com/vi/' . self::YOUTUBE_VIDEO_ID . '/hqdefault.jpg', $data->thumbnail_url );
     498    }
     499
     500    public function test_proxy_with_invalid_oembed_provider_no_discovery() {
     501        wp_set_current_user( self::$editor );
     502
     503        // If discover is false for an unkown provider, no discovery request should take place.
     504        $request = new WP_REST_Request( 'GET', '/oembed/1.0/proxy' );
     505        $request->set_param( 'url', self::INVALID_OEMBED_URL );
     506        $request->set_param( 'discover', 0 );
     507        $response = $this->server->dispatch( $request );
     508        $this->assertEquals( 404, $response->get_status() );
     509        $this->assertEquals( 0, $this->request_count );
     510    }
     511
     512    public function test_proxy_with_invalid_oembed_provider_with_default_discover_param() {
     513        wp_set_current_user( self::$editor );
     514
     515        // For an unkown provider, a discovery request should happen.
     516        $request = new WP_REST_Request( 'GET', '/oembed/1.0/proxy' );
     517        $request->set_param( 'url', self::INVALID_OEMBED_URL );
     518        $response = $this->server->dispatch( $request );
     519        $this->assertEquals( 404, $response->get_status() );
     520        $this->assertEquals( 1, $this->request_count );
     521    }
     522
     523    public function test_proxy_with_invalid_discover_param() {
     524        wp_set_current_user( self::$editor );
     525        $request = new WP_REST_Request( 'GET', '/oembed/1.0/proxy' );
     526        $request->set_param( 'url', self::INVALID_OEMBED_URL );
     527        $request->set_param( 'discover', 'notaboolean' );
     528
     529        $response = $this->server->dispatch( $request );
     530
     531        $this->assertEquals( 400, $response->get_status() );
     532        $data = $response->get_data();
     533        $this->assertEquals( $data['code'], 'rest_invalid_param' );
     534    }
    351535}
  • trunk/tests/phpunit/tests/rest-api/rest-schema-setup.php

    r40341 r40628  
    4444            '/oembed/1.0',
    4545            '/oembed/1.0/embed',
     46            '/oembed/1.0/proxy',
    4647            '/wp/v2',
    4748            '/wp/v2/posts',
     
    165166                'route' => '/oembed/1.0/embed',
    166167                'name'  => 'oembeds',
     168            ),
     169            array(
     170                'route' => '/oembed/1.0/proxy',
     171                'name'  => 'oembedProxy',
    167172            ),
    168173            array(
  • trunk/tests/qunit/fixtures/wp-api-generated.js

    r40605 r40628  
    9696            }
    9797        },
     98        "/oembed/1.0/proxy": {
     99            "namespace": "oembed/1.0",
     100            "methods": [
     101                "GET"
     102            ],
     103            "endpoints": [
     104                {
     105                    "methods": [
     106                        "GET"
     107                    ],
     108                    "args": {
     109                        "url": {
     110                            "required": true,
     111                            "description": "The URL of the resource for which to fetch oEmbed data.",
     112                            "type": "string"
     113                        },
     114                        "format": {
     115                            "required": false,
     116                            "default": "json",
     117                            "enum": [
     118                                "json",
     119                                "xml"
     120                            ],
     121                            "description": "The oEmbed format to use.",
     122                            "type": "string"
     123                        },
     124                        "maxwidth": {
     125                            "required": false,
     126                            "default": 600,
     127                            "description": "The maximum width of the embed frame in pixels.",
     128                            "type": "integer"
     129                        },
     130                        "maxheight": {
     131                            "required": false,
     132                            "description": "The maximum height of the embed frame in pixels.",
     133                            "type": "integer"
     134                        },
     135                        "discover": {
     136                            "required": false,
     137                            "default": true,
     138                            "description": "Whether to perform an oEmbed discovery request for non-whitelisted providers.",
     139                            "type": "boolean"
     140                        }
     141                    }
     142                }
     143            ],
     144            "_links": {
     145                "self": "http://example.org/?rest_route=/oembed/1.0/proxy"
     146            }
     147        },
    98148        "/wp/v2": {
    99149            "namespace": "wp/v2",
     
    33973447                "self": "http://example.org/?rest_route=/oembed/1.0/embed"
    33983448            }
     3449        },
     3450        "/oembed/1.0/proxy": {
     3451            "namespace": "oembed/1.0",
     3452            "methods": [
     3453                "GET"
     3454            ],
     3455            "endpoints": [
     3456                {
     3457                    "methods": [
     3458                        "GET"
     3459                    ],
     3460                    "args": {
     3461                        "url": {
     3462                            "required": true,
     3463                            "description": "The URL of the resource for which to fetch oEmbed data.",
     3464                            "type": "string"
     3465                        },
     3466                        "format": {
     3467                            "required": false,
     3468                            "default": "json",
     3469                            "enum": [
     3470                                "json",
     3471                                "xml"
     3472                            ],
     3473                            "description": "The oEmbed format to use.",
     3474                            "type": "string"
     3475                        },
     3476                        "maxwidth": {
     3477                            "required": false,
     3478                            "default": 600,
     3479                            "description": "The maximum width of the embed frame in pixels.",
     3480                            "type": "integer"
     3481                        },
     3482                        "maxheight": {
     3483                            "required": false,
     3484                            "description": "The maximum height of the embed frame in pixels.",
     3485                            "type": "integer"
     3486                        },
     3487                        "discover": {
     3488                            "required": false,
     3489                            "default": true,
     3490                            "description": "Whether to perform an oEmbed discovery request for non-whitelisted providers.",
     3491                            "type": "boolean"
     3492                        }
     3493                    }
     3494                }
     3495            ],
     3496            "_links": {
     3497                "self": "http://example.org/?rest_route=/oembed/1.0/proxy"
     3498            }
    33993499        }
    34003500    }
     
    34023502
    34033503mockedApiResponse.oembeds = {
     3504    "code": "rest_missing_callback_param",
     3505    "message": "Missing parameter(s): url",
     3506    "data": {
     3507        "status": 400,
     3508        "params": [
     3509            "url"
     3510        ]
     3511    }
     3512};
     3513
     3514mockedApiResponse.oembedProxy = {
    34043515    "code": "rest_missing_callback_param",
    34053516    "message": "Missing parameter(s): url",
Note: See TracChangeset for help on using the changeset viewer.