PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.1
16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 All 503 releases
jetpack / extensions / blocks / pinterest / pinterest.php

pinterest.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.1, at extensions/blocks/pinterest/pinterest.php

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