Brak opisu

pinterest.php 8.6KB

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