248 lines
8.8 KiB
PHP
248 lines
8.8 KiB
PHP
<?php
|
|
/**
|
|
* Pinterest Block.
|
|
*
|
|
* Note: this block is no longer available to be added to new posts.
|
|
* It is only kept available for existing posts with Pinterest blocks.
|
|
* You can still embed Pinterest content using the embed method provided by WordPress itself.
|
|
*
|
|
* @since 8.0.0
|
|
*
|
|
* @package automattic/jetpack
|
|
*/
|
|
|
|
namespace Automattic\Jetpack\Extensions\Pinterest;
|
|
|
|
use Automattic\Jetpack\Blocks;
|
|
use WP_Error;
|
|
|
|
const URL_PATTERN = '#^https?://(?:www\.)?(?:[a-z]{2}\.)?pinterest\.[a-z.]+/pin/(?P<pin_id>[^/]+)/?#i'; // Taken from AMP plugin, originally from Jetpack.
|
|
// This is the validate Pinterest URLs, converted from URL_REGEX in extensions/blocks/pinterest/index.js.
|
|
const PINTEREST_URL_REGEX = '/^https?:\/\/(?:www\.)?(?:[a-z]{2}\.)?(?:pinterest\.[a-z.]+|pin\.it)\/([^\/]+)(\/[^\/]+)?/i';
|
|
// This looks for matches in /foo/ of https://www.pinterest.ca/foo/.
|
|
const REMAINING_URL_PATH_REGEX = '/^\/([^\/]+)\/?$/';
|
|
// This looks for matches with /foo/bar/ of https://www.pinterest.ca/foo/bar/.
|
|
const REMAINING_URL_PATH_WITH_SUBPATH_REGEX = '/^\/([^\/]+)\/([^\/]+)\/?$/';
|
|
|
|
/**
|
|
* Determines the Pinterest embed type from the URL.
|
|
*
|
|
* @param string $url the URL to check.
|
|
* @return string The pin type. Empty string if it isn't a valid Pinterest URL.
|
|
*/
|
|
function pin_type( $url ) {
|
|
if ( null === $url || ! preg_match( PINTEREST_URL_REGEX, $url ) ) {
|
|
return '';
|
|
}
|
|
|
|
$path = wp_parse_url( $url, PHP_URL_PATH );
|
|
|
|
if ( ! $path ) {
|
|
return '';
|
|
}
|
|
|
|
if ( str_starts_with( $path, '/pin/' ) ) {
|
|
return 'embedPin';
|
|
}
|
|
|
|
if ( preg_match( REMAINING_URL_PATH_REGEX, $path ) ) {
|
|
return 'embedUser';
|
|
}
|
|
|
|
if ( preg_match( REMAINING_URL_PATH_WITH_SUBPATH_REGEX, $path ) ) {
|
|
return 'embedBoard';
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Registers the block for use in Gutenberg
|
|
* This is done via an action so that we can disable
|
|
* registration if we need to.
|
|
*/
|
|
function register_block() {
|
|
Blocks::jetpack_register_block(
|
|
__DIR__,
|
|
array( 'render_callback' => __NAMESPACE__ . '\load_assets' )
|
|
);
|
|
}
|
|
add_action( 'init', __NAMESPACE__ . '\register_block' );
|
|
|
|
/**
|
|
* Fetch info for a Pin.
|
|
*
|
|
* This is using the same pin info API as AMP is using client-side in the amp-pinterest component.
|
|
* Successful API responses are cached in a transient for 1 month. Unsuccessful responses are cached for 1 hour.
|
|
*
|
|
* @link https://github.com/ampproject/amphtml/blob/b5dea36e0b8bd012585d50839766a084f99a3685/extensions/amp-pinterest/0.1/pin-widget.js#L83-L97
|
|
* @param string $pin_id Pin ID.
|
|
* @return array|WP_Error Pin info or error on failure.
|
|
*/
|
|
function fetch_pin_info( $pin_id ) {
|
|
$transient_id = substr( "jetpack_pin_info_{$pin_id}", 0, 172 );
|
|
|
|
$info = get_transient( $transient_id );
|
|
if ( is_array( $info ) || is_wp_error( $info ) ) {
|
|
return $info;
|
|
}
|
|
|
|
$pin_info_api_url = add_query_arg(
|
|
array(
|
|
'pin_ids' => rawurlencode( $pin_id ),
|
|
'sub' => 'wwww',
|
|
'base_scheme' => 'https',
|
|
),
|
|
'https://widgets.pinterest.com/v3/pidgets/pins/info/'
|
|
);
|
|
|
|
$response = wp_remote_get( esc_url_raw( $pin_info_api_url ) );
|
|
if ( is_wp_error( $response ) ) {
|
|
set_transient( $transient_id, $response, HOUR_IN_SECONDS );
|
|
return $response;
|
|
}
|
|
|
|
$error = null;
|
|
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
|
if ( ! is_array( $body ) || ! isset( $body['status'] ) ) {
|
|
$error = new WP_Error( 'bad_json_response', '', compact( 'pin_id' ) );
|
|
} elseif ( 'success' !== $body['status'] || ! isset( $body['data'][0] ) ) {
|
|
$error = new WP_Error( 'unsuccessful_request', '', compact( 'pin_id' ) );
|
|
} elseif ( ! isset( $body['data'][0]['images']['237x'] ) ) {
|
|
// See <https://github.com/ampproject/amphtml/blob/b5dea36e0b8bd012585d50839766a084f99a3685/extensions/amp-pinterest/0.1/pin-widget.js#L106>.
|
|
$error = new WP_Error( 'missing_required_image', '', compact( 'pin_id' ) );
|
|
}
|
|
|
|
if ( $error ) {
|
|
set_transient( $transient_id, $error, HOUR_IN_SECONDS );
|
|
return $error;
|
|
} else {
|
|
$data = $body['data'][0];
|
|
set_transient( $transient_id, $data, MONTH_IN_SECONDS );
|
|
return $data;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render a Pin using the amp-pinterest component.
|
|
*
|
|
* This does not render boards or user profiles.
|
|
*
|
|
* Since AMP components need to be statically sized to be valid (so as to avoid layout shifting), there are quite a few
|
|
* hard-coded numbers as taken from the CSS for the AMP component.
|
|
*
|
|
* @param array $attr Block attributes.
|
|
* @return string Markup for <amp-pinterest>.
|
|
*/
|
|
function render_amp_pin( $attr ) {
|
|
$info = null;
|
|
if ( preg_match( URL_PATTERN, $attr['url'], $matches ) ) {
|
|
$info = fetch_pin_info( $matches['pin_id'] );
|
|
}
|
|
|
|
if ( is_array( $info ) ) {
|
|
$image = $info['images']['237x'];
|
|
$title = isset( $info['rich_metadata']['title'] ) ? $info['rich_metadata']['title'] : null;
|
|
$description = isset( $info['rich_metadata']['description'] ) ? $info['rich_metadata']['description'] : null;
|
|
|
|
// This placeholder will appear while waiting for the amp-pinterest component to initialize (or if it fails to initialize due to JS being disabled).
|
|
$placeholder = sprintf(
|
|
// The AMP_Img_Sanitizer will convert his to <amp-img> while also supplying `noscript > img` as fallback when JS is disabled.
|
|
'<a href="%s" placeholder><img src="%s" alt="%s" layout="fill" object-fit="contain" object-position="top left"></a>',
|
|
esc_url( $attr['url'] ),
|
|
esc_url( $image['url'] ),
|
|
esc_attr( $title )
|
|
);
|
|
|
|
$amp_padding = 5; // See <https://github.com/ampproject/amphtml/blob/b5dea36e0b8bd012585d50839766a084f99a3685/extensions/amp-pinterest/0.1/amp-pinterest.css#L269>.
|
|
$amp_fixed_width = 237; // See <https://github.com/ampproject/amphtml/blob/b5dea36e0b8bd012585d50839766a084f99a3685/extensions/amp-pinterest/0.1/amp-pinterest.css#L270>.
|
|
$pin_info_height = 60; // Minimum Obtained by measuring the height of the .-amp-pinterest-embed-pin-text element.
|
|
|
|
// Add height based on how much description there is. There are roughly 30 characters on a line of description text.
|
|
$has_description = false;
|
|
if ( ! empty( $info['description'] ) ) {
|
|
$desc_padding_top = 5; // See <https://github.com/ampproject/amphtml/blob/b5dea36e0b8bd012585d50839766a084f99a3685/extensions/amp-pinterest/0.1/amp-pinterest.css#L342>.
|
|
$pin_info_height += $desc_padding_top;
|
|
|
|
// Trim whitespace on description if there is any left, use to calculate the likely rows of text.
|
|
$description = trim( $info['description'] );
|
|
if ( strlen( $description ) > 0 ) {
|
|
$has_description = true;
|
|
$desc_line_height = 17; // See <https://github.com/ampproject/amphtml/blob/b5dea36e0b8bd012585d50839766a084f99a3685/extensions/amp-pinterest/0.1/amp-pinterest.css#L341>.
|
|
$pin_info_height += ceil( strlen( $description ) / 30 ) * $desc_line_height;
|
|
}
|
|
}
|
|
|
|
if ( ! empty( $info['repin_count'] ) ) {
|
|
$pin_stats_height = 16; // See <https://github.com/ampproject/amphtml/blob/b5dea36e0b8bd012585d50839766a084f99a3685/extensions/amp-pinterest/0.1/amp-pinterest.css#L322>.
|
|
$pin_info_height += $pin_stats_height;
|
|
}
|
|
|
|
// When Pin description is empty, make sure title and description from rich metadata are supplied for accessibility and discoverability.
|
|
$title = $has_description ? '' : implode( "\n", array_filter( array( $title, $description ) ) );
|
|
|
|
$amp_pinterest = sprintf(
|
|
'<amp-pinterest style="%1$s" data-do="embedPin" data-url="%2$s" width="%3$d" height="%4$d" title="%5$s">%6$s</amp-pinterest>',
|
|
esc_attr( 'line-height:1.5; font-size:21px' ), // Override styles from theme due to precise height calculations above.
|
|
esc_url( $attr['url'] ),
|
|
$amp_fixed_width + ( $amp_padding * 2 ),
|
|
$image['height'] + $pin_info_height + ( $amp_padding * 2 ),
|
|
esc_attr( $title ),
|
|
$placeholder
|
|
);
|
|
} else {
|
|
// Fallback embed when info is not available.
|
|
$amp_pinterest = sprintf(
|
|
'<amp-pinterest data-do="embedPin" data-url="%1$s" width="%2$d" height="%3$d">%4$s</amp-pinterest>',
|
|
esc_url( $attr['url'] ),
|
|
450, // Fallback width.
|
|
750, // Fallback height.
|
|
sprintf(
|
|
'<a placeholder href="%s">%s</a>',
|
|
esc_url( $attr['url'] ),
|
|
esc_html( $attr['url'] )
|
|
)
|
|
);
|
|
}
|
|
|
|
return sprintf(
|
|
'<div class="wp-block-jetpack-pinterest">%s</div>',
|
|
$amp_pinterest
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Pinterest block registration/dependency declaration.
|
|
*
|
|
* @param array $attr Array containing the Pinterest block attributes.
|
|
* @param string $content String containing the Pinterest block content.
|
|
*
|
|
* @return string
|
|
*/
|
|
function load_assets( $attr, $content ) {
|
|
if ( ! jetpack_is_frontend() ) {
|
|
return $content;
|
|
}
|
|
if ( Blocks::is_amp_request() ) {
|
|
return render_amp_pin( $attr );
|
|
} else {
|
|
$url = $attr['url'];
|
|
$type = pin_type( $url );
|
|
|
|
if ( ! $type ) {
|
|
return '';
|
|
}
|
|
|
|
wp_enqueue_script( 'pinterest-pinit', 'https://assets.pinterest.com/js/pinit.js', array(), JETPACK__VERSION, true );
|
|
return sprintf(
|
|
'
|
|
<div class="%1$s">
|
|
<a data-pin-do="%2$s" href="%3$s"></a>
|
|
</div>
|
|
',
|
|
esc_attr( Blocks::classes( Blocks::get_block_feature( __DIR__ ), $attr ) ),
|
|
esc_attr( $type ),
|
|
esc_url( $url )
|
|
);
|
|
}
|
|
}
|