Completed
Push — try/jetpack-stories-block-mobi... ( 2fea66 )
by
unknown
126:35 queued 116:47
created

extensions/blocks/pinterest/pinterest.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Pinterest Block.
4
 *
5
 * @since 8.0.0
6
 *
7
 * @package Jetpack
8
 */
9
10
namespace Automattic\Jetpack\Extensions\Pinterest;
11
12
use Automattic\Jetpack\Blocks;
13
use WP_Error;
14
15
const FEATURE_NAME = 'pinterest';
16
const BLOCK_NAME   = 'jetpack/' . FEATURE_NAME;
17
const URL_PATTERN  = '#^https?://(?:www\.)?(?:[a-z]{2}\.)?pinterest\.[a-z.]+/pin/(?P<pin_id>[^/]+)/?#i'; // Taken from AMP plugin, originally from Jetpack.
18
// This is the validate Pinterest URLs, converted from URL_REGEX in extensions/blocks/pinterest/index.js.
19
const PINTEREST_URL_REGEX = '/^https?:\/\/(?:www\.)?(?:[a-z]{2\.)?(?:pinterest\.[a-z.]+|pin\.it)\/([^\/]+)(\/[^\/]+)?/i';
20
// This looks for matches in /foo/ of https://www.pinterest.ca/foo/.
21
const REMAINING_URL_PATH_REGEX = '/^\/([^\/]+)\/?$/';
22
// This looks for matches with /foo/bar/ of https://www.pinterest.ca/foo/bar/.
23
const REMAINING_URL_PATH_WITH_SUBPATH_REGEX = '/^\/([^\/]+)\/([^\/]+)\/?$/';
24
25
/**
26
 * Determines the Pinterest embed type from the URL.
27
 *
28
 * @param string $url the URL to check.
29
 * @returns {string} The pin type. Empty string if it isn't a valid Pinterest URL.
30
 */
31
function pin_type( $url ) {
32
	if ( ! preg_match( PINTEREST_URL_REGEX, $url ) ) {
33
		return '';
34
	}
35
36
	$path = wp_parse_url( $url, PHP_URL_PATH );
37
38
	if ( ! $path ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $path of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
39
		return '';
40
	}
41
42
	if ( substr( $path, 0, 5 ) === '/pin/' ) {
43
		return 'embedPin';
44
	}
45
46
	if ( preg_match( REMAINING_URL_PATH_REGEX, $path ) ) {
47
		return 'embedUser';
48
	}
49
50
	if ( preg_match( REMAINING_URL_PATH_WITH_SUBPATH_REGEX, $path ) ) {
51
		return 'embedBoard';
52
	}
53
54
	return '';
55
}
56
57
/**
58
 * Registers the block for use in Gutenberg
59
 * This is done via an action so that we can disable
60
 * registration if we need to.
61
 */
62
function register_block() {
63
	Blocks::jetpack_register_block(
64
		BLOCK_NAME,
65
		array( 'render_callback' => __NAMESPACE__ . '\load_assets' )
66
	);
67
}
68
add_action( 'init', __NAMESPACE__ . '\register_block' );
69
70
/**
71
 * Fetch info for a Pin.
72
 *
73
 * This is using the same pin info API as AMP is using client-side in the amp-pinterest component.
74
 * Successful API responses are cached in a transient for 1 month. Unsuccessful responses are cached for 1 hour.
75
 *
76
 * @link https://github.com/ampproject/amphtml/blob/b5dea36e0b8bd012585d50839766a084f99a3685/extensions/amp-pinterest/0.1/pin-widget.js#L83-L97
77
 * @param string $pin_id Pin ID.
78
 * @return array|WP_Error Pin info or error on failure.
79
 */
80
function fetch_pin_info( $pin_id ) {
81
	$transient_id = substr( "jetpack_pin_info_{$pin_id}", 0, 172 );
82
83
	$info = get_transient( $transient_id );
84
	if ( is_array( $info ) || is_wp_error( $info ) ) {
85
		return $info;
86
	}
87
88
	$pin_info_api_url = add_query_arg(
89
		array(
90
			'pin_ids'     => rawurlencode( $pin_id ),
91
			'sub'         => 'wwww',
92
			'base_scheme' => 'https',
93
		),
94
		'https://widgets.pinterest.com/v3/pidgets/pins/info/'
95
	);
96
97
	$response = wp_remote_get( esc_url_raw( $pin_info_api_url ) );
98
	if ( is_wp_error( $response ) ) {
99
		set_transient( $transient_id, $response, HOUR_IN_SECONDS );
100
		return $response;
101
	}
102
103
	$error = null;
104
	$body  = json_decode( wp_remote_retrieve_body( $response ), true );
105
	if ( ! is_array( $body ) || ! isset( $body['status'] ) ) {
106
		$error = new WP_Error( 'bad_json_response', '', compact( 'pin_id' ) );
107
	} elseif ( 'success' !== $body['status'] || ! isset( $body['data'][0] ) ) {
108
		$error = new WP_Error( 'unsuccessful_request', '', compact( 'pin_id' ) );
109
	} elseif ( ! isset( $body['data'][0]['images']['237x'] ) ) {
110
		// See <https://github.com/ampproject/amphtml/blob/b5dea36e0b8bd012585d50839766a084f99a3685/extensions/amp-pinterest/0.1/pin-widget.js#L106>.
111
		$error = new WP_Error( 'missing_required_image', '', compact( 'pin_id' ) );
112
	}
113
114
	if ( $error ) {
115
		set_transient( $transient_id, $error, HOUR_IN_SECONDS );
116
		return $error;
117
	} else {
118
		$data = $body['data'][0];
119
		set_transient( $transient_id, $data, MONTH_IN_SECONDS );
120
		return $data;
121
	}
122
}
123
124
/**
125
 * Render a Pin using the amp-pinterest component.
126
 *
127
 * This does not render boards or user profiles.
128
 *
129
 * Since AMP components need to be statically sized to be valid (so as to avoid layout shifting), there are quite a few
130
 * hard-coded numbers as taken from the CSS for the AMP component.
131
 *
132
 * @param array $attr Block attributes.
133
 * @return string Markup for <amp-pinterest>.
134
 */
135
function render_amp_pin( $attr ) {
136
	$info = null;
137
	if ( preg_match( URL_PATTERN, $attr['url'], $matches ) ) {
138
		$info = fetch_pin_info( $matches['pin_id'] );
139
	}
140
141
	if ( is_array( $info ) ) {
142
		$image       = $info['images']['237x'];
143
		$title       = isset( $info['rich_metadata']['title'] ) ? $info['rich_metadata']['title'] : null;
144
		$description = isset( $info['rich_metadata']['description'] ) ? $info['rich_metadata']['description'] : null;
145
146
		// This placeholder will appear while waiting for the amp-pinterest component to initialize (or if it fails to initialize due to JS being disabled).
147
		$placeholder = sprintf(
148
			// The AMP_Img_Sanitizer will convert his to <amp-img> while also supplying `noscript > img` as fallback when JS is disabled.
149
			'<a href="%s" placeholder><img src="%s" alt="%s" layout="fill" object-fit="contain" object-position="top left"></a>',
150
			esc_url( $attr['url'] ),
151
			esc_url( $image['url'] ),
152
			esc_attr( $title )
153
		);
154
155
		$amp_padding     = 5;   // See <https://github.com/ampproject/amphtml/blob/b5dea36e0b8bd012585d50839766a084f99a3685/extensions/amp-pinterest/0.1/amp-pinterest.css#L269>.
156
		$amp_fixed_width = 237; // See <https://github.com/ampproject/amphtml/blob/b5dea36e0b8bd012585d50839766a084f99a3685/extensions/amp-pinterest/0.1/amp-pinterest.css#L270>.
157
		$pin_info_height = 60;  // Minimum Obtained by measuring the height of the .-amp-pinterest-embed-pin-text element.
158
159
		// Add height based on how much description there is. There are roughly 30 characters on a line of description text.
160
		$has_description = false;
161
		if ( ! empty( $info['description'] ) ) {
162
			$desc_padding_top = 5;  // See <https://github.com/ampproject/amphtml/blob/b5dea36e0b8bd012585d50839766a084f99a3685/extensions/amp-pinterest/0.1/amp-pinterest.css#L342>.
163
			$pin_info_height += $desc_padding_top;
164
165
			// Trim whitespace on description if there is any left, use to calculate the likely rows of text.
166
			$description = trim( $info['description'] );
167
			if ( strlen( $description ) > 0 ) {
168
				$has_description  = true;
169
				$desc_line_height = 17; // See <https://github.com/ampproject/amphtml/blob/b5dea36e0b8bd012585d50839766a084f99a3685/extensions/amp-pinterest/0.1/amp-pinterest.css#L341>.
170
				$pin_info_height += ceil( strlen( $description ) / 30 ) * $desc_line_height;
171
			}
172
		}
173
174
		if ( ! empty( $info['repin_count'] ) ) {
175
			$pin_stats_height = 16;  // See <https://github.com/ampproject/amphtml/blob/b5dea36e0b8bd012585d50839766a084f99a3685/extensions/amp-pinterest/0.1/amp-pinterest.css#L322>.
176
			$pin_info_height += $pin_stats_height;
177
		}
178
179
		// When Pin description is empty, make sure title and description from rich metadata are supplied for accessibility and discoverability.
180
		$title = $has_description ? '' : implode( "\n", array_filter( array( $title, $description ) ) );
181
182
		$amp_pinterest = sprintf(
183
			'<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>',
184
			esc_attr( 'line-height:1.5; font-size:21px' ), // Override styles from theme due to precise height calculations above.
185
			esc_url( $attr['url'] ),
186
			$amp_fixed_width + ( $amp_padding * 2 ),
187
			$image['height'] + $pin_info_height + ( $amp_padding * 2 ),
188
			esc_attr( $title ),
189
			$placeholder
190
		);
191
	} else {
192
		// Fallback embed when info is not available.
193
		$amp_pinterest = sprintf(
194
			'<amp-pinterest data-do="embedPin" data-url="%1$s" width="%2$d" height="%3$d">%4$s</amp-pinterest>',
195
			esc_url( $attr['url'] ),
196
			450, // Fallback width.
197
			750, // Fallback height.
198
			sprintf(
199
				'<a placeholder href="%s">%s</a>',
200
				esc_url( $attr['url'] ),
201
				esc_html( $attr['url'] )
202
			)
203
		);
204
	}
205
206
	return sprintf(
207
		'<div class="wp-block-jetpack-pinterest">%s</div>',
208
		$amp_pinterest
209
	);
210
}
211
212
/**
213
 * Pinterest block registration/dependency declaration.
214
 *
215
 * @param array  $attr    Array containing the Pinterest block attributes.
216
 * @param string $content String containing the Pinterest block content.
217
 *
218
 * @return string
219
 */
220
function load_assets( $attr, $content ) {
221
	if ( ! jetpack_is_frontend() ) {
222
		return $content;
223
	}
224
	if ( Blocks::is_amp_request() ) {
225
		return render_amp_pin( $attr );
226
	} else {
227
		$url  = $attr['url'];
228
		$type = pin_type( $url );
229
230
		if ( ! $type ) {
231
			return '';
232
		}
233
234
		wp_enqueue_script( 'pinterest-pinit', 'https://assets.pinterest.com/js/pinit.js', array(), JETPACK__VERSION, true );
235
		return sprintf(
236
			'
237
			<div class="%1$s">
238
				<a data-pin-do="%2$s" href="%3$s"></a>
239
			</div>
240
		',
241
			esc_attr( Blocks::classes( FEATURE_NAME, $attr ) ),
242
			esc_attr( $type ),
243
			esc_url( $url )
244
		);
245
	}
246
}
247