2020-01-26 12:53:20 +01:00
< ? php
/**
* Pinterest Block .
*
2024-04-17 11:32:24 +02:00
* 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 .
*
2020-01-26 12:53:20 +01:00
* @ since 8.0 . 0
*
2021-04-27 08:32:47 +02:00
* @ package automattic / jetpack
2020-01-26 12:53:20 +01:00
*/
2020-05-06 17:20:49 +02:00
namespace Automattic\Jetpack\Extensions\Pinterest ;
2020-10-20 18:05:12 +02:00
use Automattic\Jetpack\Blocks ;
2025-07-27 19:58:08 +02:00
use Automattic\Jetpack\Status\Request ;
2020-10-20 18:05:12 +02:00
use WP_Error ;
2025-12-12 13:13:07 +01:00
if ( ! defined ( 'ABSPATH' ) ) {
exit ( 0 );
}
2023-12-07 09:44:11 +01:00
const URL_PATTERN = '#^https?://(?:www\.)?(?:[a-z]{2}\.)?pinterest\.[a-z.]+/pin/(?P<pin_id>[^/]+)/?#i' ; // Taken from AMP plugin, originally from Jetpack.
2020-12-10 14:04:11 +01:00
// 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 .
2025-02-28 08:42:11 +01:00
* @ return string The pin type . Empty string if it isn ' t a valid Pinterest URL .
2020-12-10 14:04:11 +01:00
*/
function pin_type ( $url ) {
2022-04-02 10:26:41 +02:00
if ( null === $url || ! preg_match ( PINTEREST_URL_REGEX , $url ) ) {
2020-12-10 14:04:11 +01:00
return '' ;
}
$path = wp_parse_url ( $url , PHP_URL_PATH );
if ( ! $path ) {
return '' ;
}
2023-12-07 09:44:11 +01:00
if ( str_starts_with ( $path , '/pin/' ) ) {
2020-12-10 14:04:11 +01:00
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 '' ;
}
2020-05-06 17:20:49 +02:00
/**
* 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 () {
2020-11-18 09:10:44 +01:00
Blocks :: jetpack_register_block (
2023-12-07 09:44:11 +01:00
__DIR__ ,
2020-05-06 17:20:49 +02:00
array ( 'render_callback' => __NAMESPACE__ . '\load_assets' )
);
}
add_action ( 'init' , __NAMESPACE__ . '\register_block' );
2020-01-26 12:53:20 +01:00
2020-10-20 18:05:12 +02:00
/**
* 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
);
}
2020-01-26 12:53:20 +01:00
/**
* 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
*/
2020-05-06 17:20:49 +02:00
function load_assets ( $attr , $content ) {
2025-07-27 19:58:08 +02:00
if ( ! Request :: is_frontend () ) {
2020-12-10 14:04:11 +01:00
return $content ;
}
2020-10-20 18:05:12 +02:00
if ( Blocks :: is_amp_request () ) {
return render_amp_pin ( $attr );
} else {
2020-12-10 14:04:11 +01:00
$url = $attr [ 'url' ];
$type = pin_type ( $url );
if ( ! $type ) {
return '' ;
}
2020-10-20 18:05:12 +02:00
wp_enqueue_script ( 'pinterest-pinit' , 'https://assets.pinterest.com/js/pinit.js' , array (), JETPACK__VERSION , true );
2020-12-10 14:04:11 +01:00
return sprintf (
'
< div class = " %1 $s " >
< a data - pin - do = " %2 $s " href = " %3 $s " ></ a >
</ div >
' ,
2023-12-07 09:44:11 +01:00
esc_attr ( Blocks :: classes ( Blocks :: get_block_feature ( __DIR__ ), $attr ) ),
2020-12-10 14:04:11 +01:00
esc_attr ( $type ),
esc_url ( $url )
);
2020-10-20 18:05:12 +02:00
}
2020-01-26 12:53:20 +01:00
}