Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pinterest block #13905

Merged
merged 16 commits into from
Nov 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php
/**
* REST API endpoint for resolving URL redirects.
*
* @package Jetpack
* @since 8.0.0
*/

/**
* Resolve URL redirects.
*
* @since 8.0.0
*/
class WPCOM_REST_API_V2_Endpoint_Resolve_Redirect extends WP_REST_Controller {
/**
* Constructor.
*/
public function __construct() {
$this->namespace = 'wpcom/v2';
$this->rest_base = 'resolve-redirect';
// This endpoint *does not* need to connect directly to Jetpack sites.
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
}

/**
* Register the route.
*/
public function register_routes() {
// GET /sites/<blog_id>/resolve-redirect/<url> - Follow 301/302 redirects on a URL, and return the final destination.
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<url>.+)',
array(
'args' => array(
'url' => array(
'description' => __( 'The URL to check for redirects.', 'jetpack' ),
'type' => 'string',
'required' => 'true',
'validate_callback' => 'wp_http_validate_url',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'follow_redirect' ),
'permission_callback' => 'is_user_logged_in',
jeherve marked this conversation as resolved.
Show resolved Hide resolved
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}

/**
* Follows 301/302 redirect for the passed URL, and returns the final destination.
*
* @param WP_REST_Request $request The REST API request data.
* @return WP_REST_Response The REST API response.
*/
public function follow_redirect( $request ) {
$response = wp_safe_remote_get( $request['url'] );
if ( is_wp_error( $response ) ) {
return rest_ensure_response( '' );
}

$history = $response['http_response']->get_response_object()->history;
if ( ! $history ) {
return response_ensure_response( $request['url'] );
}

$location = $history[0]->headers->getValues( 'location' );
if ( ! $location ) {
return response_ensure_response( $request['url'] );
}

return rest_ensure_response( $location[0] );
}

/**
* Retrieves the comment's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'resolve-redirect',
'type' => 'string',
'description' => __( 'The final destination of the URL being checked for redirects.', 'jetpack' ),
);

return $schema;
}
}

wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Resolve_Redirect' );
1 change: 1 addition & 0 deletions bin/phpcs-whitelist.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = [
'_inc/lib/class.jetpack-password-checker.php',
'_inc/lib/components.php',
'_inc/lib/core-api/class.jetpack-core-api-site-endpoints.php',
'_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-resolve-redirect.php',
'_inc/lib/core-api/wpcom-endpoints/memberships.php',
'_inc/lib/debugger/',
'_inc/lib/plans.php',
Expand Down
217 changes: 217 additions & 0 deletions extensions/blocks/pinterest/edit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/**
* External dependencies
*/
import { invoke } from 'lodash';
import { __, _x } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { Placeholder, SandBox, Button, IconButton, Spinner, Toolbar } from '@wordpress/components';
import { BlockControls, BlockIcon } from '@wordpress/editor';
import apiFetch from '@wordpress/api-fetch';

/**
* Internal dependencies
*/
import { fallback, pinType } from './utils';
import { icon } from '.';

const PINIT_URL_REGEX = /^\s*https?:\/\/pin\.it\//i;

class PinterestEdit extends Component {
constructor() {
super( ...arguments );

this.state = {
editedUrl: this.props.attributes.url || '',
editingUrl: false,
// If this is a pin.it URL, we're going to need to find where it redirects to.
resolvingRedirect: PINIT_URL_REGEX.test( this.props.attributes.url ),
// The interactive-related magic comes from Core's EmbedPreview component,
// which currently isn't exported in a way we can use.
interactive: false,
};
}

componentDidMount() {
const { resolvingRedirect } = this.state;

// Check if we need to resolve a pin.it URL immediately.
if ( resolvingRedirect ) {
this.resolveRedirect();
}
}

componentDidUpdate( prevProps, prevState ) {
// Check if a pin.it URL has been entered, so we need to resolve it.
if ( ! prevState.resolvingRedirect && this.state.resolvingRedirect ) {
this.resolveRedirect();
}
}

componentWillUnmount() {
invoke( this.fetchRequest, [ 'abort' ] );
}

resolveRedirect = () => {
const { url } = this.props.attributes;

this.fetchRequest = apiFetch( {
path: `/wpcom/v2/resolve-redirect/${ url }`,
} );

this.fetchRequest.then(
resolvedUrl => {
// resolve
this.fetchRequest = null;
this.props.setAttributes( { url: resolvedUrl } );
this.setState( {
resolvingRedirect: false,
editedUrl: resolvedUrl,
} );
},
xhr => {
// reject
if ( xhr.statusText === 'abort' ) {
return;
}
this.fetchRequest = null;
this.setState( {
resolvingRedirect: false,
editingUrl: true,
} );
}
);
};

static getDerivedStateFromProps( nextProps, state ) {
if ( ! nextProps.isSelected && state.interactive ) {
// We only want to change this when the block is not selected, because changing it when
jeherve marked this conversation as resolved.
Show resolved Hide resolved
// the block becomes selected makes the overlap disappear too early. Hiding the overlay
// happens on mouseup when the overlay is clicked.
return { interactive: false };
}

return null;
}

hideOverlay = () => {
// This is called onMouseUp on the overlay. We can't respond to the `isSelected` prop
// changing, because that happens on mouse down, and the overlay immediately disappears,
// and the mouse event can end up in the preview content. We can't use onClick on
// the overlay to hide it either, because then the editor misses the mouseup event, and
// thinks we're multi-selecting blocks.
this.setState( { interactive: true } );
};

setUrl = event => {
if ( event ) {
event.preventDefault();
}

const { editedUrl: url } = this.state;

this.props.setAttributes( { url } );
this.setState( { editingUrl: false } );

if ( PINIT_URL_REGEX.test( url ) ) {
// Setting the `resolvingRedirect` state here, then waiting for `componentDidUpdate()` to
// be called before actually resolving it ensures that the `editedUrl` state has also been
// updated before resolveRedirect() is called.
this.setState( { resolvingRedirect: true } );
}
};

/**
* Render a preview of the Pinterest embed.
*
* @returns {object} The UI displayed when user edits this block.
*/
render() {
const { attributes, className } = this.props;
const { url } = attributes;
const { editedUrl, interactive, editingUrl, resolvingRedirect } = this.state;

if ( resolvingRedirect ) {
return (
<div className="wp-block-embed is-loading">
<Spinner />
<p>{ __( 'Embedding…' ) }</p>
</div>
);
}

const type = pinType( url );
const html = `<a data-pin-do='${ type }' href='${ url }'></a>`;
pento marked this conversation as resolved.
Show resolved Hide resolved

const cannotEmbed = url && ! type;

const controls = (
<BlockControls>
<Toolbar>
<IconButton
className="components-toolbar__control"
label={ __( 'Edit URL', 'jetpack' ) }
icon="edit"
onClick={ () => this.setState( { editingUrl: true } ) }
/>
</Toolbar>
</BlockControls>
);

if ( editingUrl || ! url || cannotEmbed ) {
return (
<div className={ className }>
{ controls }
<Placeholder label={ __( 'Pinterest', 'jetpack' ) } icon={ <BlockIcon icon={ icon } /> }>
<form onSubmit={ this.setUrl }>
<input
type="url"
value={ editedUrl }
className="components-placeholder__input"
aria-label={ __( 'Pinterest URL', 'jetpack' ) }
placeholder={ __( 'Enter URL to embed here…', 'jetpack' ) }
onChange={ event => this.setState( { editedUrl: event.target.value } ) }
/>
<Button isLarge type="submit">
{ _x( 'Embed', 'button label', 'jetpack' ) }
</Button>
{ cannotEmbed && (
<p className="components-placeholder__error">
{ __( 'Sorry, this content could not be embedded.', 'jetpack' ) }
<br />
<Button isLarge onClick={ () => fallback( editedUrl, this.props.onReplace ) }>
{ _x( 'Convert to link', 'button label', 'jetpack' ) }
</Button>
</p>
) }
</form>
</Placeholder>
</div>
);
}

// Disabled because the overlay div doesn't actually have a role or functionality
// as far as the user is concerned. We're just catching the first click so that
// the block can be selected without interacting with the embed preview that the overlay covers.
/* eslint-disable jsx-a11y/no-static-element-interactions */
return (
<div className={ className }>
{ controls }
<div>
<SandBox
html={ html }
scripts={ [ 'https://assets.pinterest.com/js/pinit.js' ] }
onFocus={ this.hideOverlay }
/>
{ ! interactive && (
<div
className="block-library-embed__interactive-overlay"
onMouseUp={ this.hideOverlay }
/>
) }
</div>
</div>
);
}
}

export default PinterestEdit;
7 changes: 7 additions & 0 deletions extensions/blocks/pinterest/editor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Internal dependencies
*/
import registerJetpackBlock from '../../shared/register-jetpack-block';
import { name, settings } from '.';

registerJetpackBlock( name, settings );
Loading