From f18b23a3a9378fb0a98856d436aa9ebf94e47429 Mon Sep 17 00:00:00 2001 From: Yury German Date: Sun, 23 Jan 2022 18:37:36 -0500 Subject: Updating Classic Editor, Google Authenticatior, Jetpack, Public Post Preview, Table of Contents, Wordpress Importer Signed-off-by: Yury German --- .../jetpack/extensions/blocks/amazon/amazon.php | 30 +- .../extensions/blocks/anchor-fm/anchor-fm.php | 191 ++++++++ .../blocks/business-hours/business-hours.php | 38 +- .../jetpack/extensions/blocks/button/button.php | 244 ++++++++++ .../extensions/blocks/calendly/calendly.php | 279 +++++++----- .../class-jetpack-contact-info-block.php | 2 +- .../blocks/contact-info/contact-info.php | 14 +- .../blocks/conversation/conversation.php | 51 +++ .../extensions/blocks/dialogue/dialogue.php | 51 +++ .../extensions/blocks/donations/donations.php | 256 +++++++++++ .../extensions/blocks/eventbrite/eventbrite.php | 211 +++++++-- plugins/jetpack/extensions/blocks/gif/gif.php | 40 +- .../blocks/google-calendar/google-calendar.php | 86 ++++ .../blocks/image-compare/image-compare.php | 90 ++++ .../blocks/instagram-gallery/instagram-gallery.php | 151 +++++++ .../extensions/blocks/mailchimp/mailchimp.php | 338 ++++++++------ plugins/jetpack/extensions/blocks/map/map.php | 138 ++++-- .../extensions/blocks/markdown/markdown.php | 19 +- .../extensions/blocks/opentable/opentable.php | 137 +++--- .../extensions/blocks/pinterest/pinterest.php | 236 +++++++++- .../blocks/podcast-player/podcast-player.php | 312 +++++++++++++ .../podcast-player/templates/playlist-track.php | 43 ++ .../templates/podcast-header-title.php | 59 +++ .../podcast-player/templates/podcast-header.php | 67 +++ .../podcast-player/templates/podcast-title.php | 36 ++ .../blocks/premium-content/_inc/access-check.php | 107 +++++ .../blocks/premium-content/_inc/legacy-buttons.php | 62 +++ .../class-jetpack-token-subscription-service.php | 50 +++ .../_inc/subscription-service/class-jwt.php | 441 ++++++++++++++++++ .../class-subscription-service.php | 57 +++ .../class-token-subscription-service.php | 264 +++++++++++ .../class-token-subscription.php | 23 + .../class-unconfigured-subscription-service.php | 58 +++ .../class-wpcom-offline-subscription-service.php | 77 ++++ .../class-wpcom-token-subscription-service.php | 46 ++ .../_inc/subscription-service/include.php | 82 ++++ .../blocks/premium-content/buttons/buttons.php | 46 ++ .../logged-out-view/logged-out-view.php | 73 +++ .../premium-content/login-button/login-button.php | 59 +++ .../blocks/premium-content/premium-content.php | 131 ++++++ .../subscriber-view/subscriber-view.php | 65 +++ .../extensions/blocks/rating-star/rating-meta.php | 30 +- .../extensions/blocks/rating-star/rating-star.php | 99 +++-- .../recurring-payments/recurring-payments.php | 4 +- .../blocks/repeat-visitor/repeat-visitor.php | 38 +- plugins/jetpack/extensions/blocks/revue/revue.php | 253 +++++++++++ .../blocks/send-a-message/send-a-message.php | 46 ++ .../whatsapp-button/whatsapp-button.php | 45 ++ .../blocks/simple-payments/simple-payments.php | 96 ++++ .../extensions/blocks/slideshow/slideshow.php | 71 +-- .../blocks/social-previews/social-previews.php | 28 ++ plugins/jetpack/extensions/blocks/story/story.php | 493 +++++++++++++++++++++ .../blocks/subscriptions/subscriptions.php | 47 ++ .../blocks/tiled-gallery/tiled-gallery.php | 45 +- .../jetpack/extensions/blocks/wordads/wordads.php | 42 +- .../extended-blocks/core-audio/core-audio.php | 27 ++ .../extended-blocks/core-cover/core-cover.php | 27 ++ .../extended-blocks/core-video/core-video.php | 27 ++ .../premium-content-container.php | 27 ++ .../extensions/plugins/publicize/publicize.php | 27 ++ 60 files changed, 5701 insertions(+), 531 deletions(-) create mode 100644 plugins/jetpack/extensions/blocks/anchor-fm/anchor-fm.php create mode 100644 plugins/jetpack/extensions/blocks/button/button.php create mode 100644 plugins/jetpack/extensions/blocks/conversation/conversation.php create mode 100644 plugins/jetpack/extensions/blocks/dialogue/dialogue.php create mode 100644 plugins/jetpack/extensions/blocks/donations/donations.php create mode 100644 plugins/jetpack/extensions/blocks/google-calendar/google-calendar.php create mode 100644 plugins/jetpack/extensions/blocks/image-compare/image-compare.php create mode 100644 plugins/jetpack/extensions/blocks/instagram-gallery/instagram-gallery.php create mode 100644 plugins/jetpack/extensions/blocks/podcast-player/podcast-player.php create mode 100644 plugins/jetpack/extensions/blocks/podcast-player/templates/playlist-track.php create mode 100644 plugins/jetpack/extensions/blocks/podcast-player/templates/podcast-header-title.php create mode 100644 plugins/jetpack/extensions/blocks/podcast-player/templates/podcast-header.php create mode 100644 plugins/jetpack/extensions/blocks/podcast-player/templates/podcast-title.php create mode 100644 plugins/jetpack/extensions/blocks/premium-content/_inc/access-check.php create mode 100644 plugins/jetpack/extensions/blocks/premium-content/_inc/legacy-buttons.php create mode 100644 plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-jetpack-token-subscription-service.php create mode 100644 plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-jwt.php create mode 100644 plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-subscription-service.php create mode 100644 plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-token-subscription-service.php create mode 100644 plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-token-subscription.php create mode 100644 plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-unconfigured-subscription-service.php create mode 100644 plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-wpcom-offline-subscription-service.php create mode 100644 plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-wpcom-token-subscription-service.php create mode 100644 plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/include.php create mode 100644 plugins/jetpack/extensions/blocks/premium-content/buttons/buttons.php create mode 100644 plugins/jetpack/extensions/blocks/premium-content/logged-out-view/logged-out-view.php create mode 100644 plugins/jetpack/extensions/blocks/premium-content/login-button/login-button.php create mode 100644 plugins/jetpack/extensions/blocks/premium-content/premium-content.php create mode 100644 plugins/jetpack/extensions/blocks/premium-content/subscriber-view/subscriber-view.php create mode 100644 plugins/jetpack/extensions/blocks/revue/revue.php create mode 100644 plugins/jetpack/extensions/blocks/send-a-message/send-a-message.php create mode 100644 plugins/jetpack/extensions/blocks/send-a-message/whatsapp-button/whatsapp-button.php create mode 100644 plugins/jetpack/extensions/blocks/simple-payments/simple-payments.php create mode 100644 plugins/jetpack/extensions/blocks/social-previews/social-previews.php create mode 100644 plugins/jetpack/extensions/blocks/story/story.php create mode 100644 plugins/jetpack/extensions/blocks/subscriptions/subscriptions.php create mode 100644 plugins/jetpack/extensions/extended-blocks/core-audio/core-audio.php create mode 100644 plugins/jetpack/extensions/extended-blocks/core-cover/core-cover.php create mode 100644 plugins/jetpack/extensions/extended-blocks/core-video/core-video.php create mode 100644 plugins/jetpack/extensions/extended-blocks/premium-content-container/premium-content-container.php create mode 100644 plugins/jetpack/extensions/plugins/publicize/publicize.php (limited to 'plugins/jetpack/extensions') diff --git a/plugins/jetpack/extensions/blocks/amazon/amazon.php b/plugins/jetpack/extensions/blocks/amazon/amazon.php index a368ab79..53af15d6 100644 --- a/plugins/jetpack/extensions/blocks/amazon/amazon.php +++ b/plugins/jetpack/extensions/blocks/amazon/amazon.php @@ -4,13 +4,29 @@ * * @since 8.x * - * @package Jetpack + * @package automattic/jetpack */ -jetpack_register_block( - 'jetpack/amazon', - array( 'render_callback' => 'jetpack_amazon_block_load_assets' ) -); +namespace Automattic\Jetpack\Extensions\Amazon; + +use Automattic\Jetpack\Blocks; +use Jetpack_Gutenberg; + +const FEATURE_NAME = 'amazon'; +const BLOCK_NAME = 'jetpack/' . FEATURE_NAME; + +/** + * 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( + BLOCK_NAME, + array( 'render_callback' => __NAMESPACE__ . '\load_assets' ) + ); +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); /** * Amazon block registration/dependency declaration. @@ -20,7 +36,7 @@ jetpack_register_block( * * @return string */ -function jetpack_amazon_block_load_assets( $attr, $content ) { - Jetpack_Gutenberg::load_assets_as_required( 'amazon' ); +function load_assets( $attr, $content ) { + Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME ); return $content; } diff --git a/plugins/jetpack/extensions/blocks/anchor-fm/anchor-fm.php b/plugins/jetpack/extensions/blocks/anchor-fm/anchor-fm.php new file mode 100644 index 00000000..f3e0bafe --- /dev/null +++ b/plugins/jetpack/extensions/blocks/anchor-fm/anchor-fm.php @@ -0,0 +1,191 @@ + true, + 'single' => true, + 'type' => 'string', + ) + ); + register_post_meta( + 'post', + 'jetpack_anchor_episode', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + ) + ); + register_post_meta( + 'post', + 'jetpack_anchor_spotify_show', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + ) + ); +} + +/** + * Checks URL params to determine the Anchor integration action to perform. + */ +function process_anchor_params() { + if ( + ! function_exists( 'get_current_screen' ) + || is_null( \get_current_screen() ) + ) { + return; + } + + // Return early if we are not in the block editor. + if ( ! wp_should_load_block_editor_scripts_and_styles() ) { + return; + } + + $post = get_post(); + if ( ! $post || ! $post->ID ) { + return; + } + + // phpcs:disable WordPress.Security.NonceVerification.Recommended + $podcast_id = isset( $_GET['anchor_podcast'] ) ? sanitize_text_field( wp_unslash( $_GET['anchor_podcast'] ) ) : null; + $episode_id = isset( $_GET['anchor_episode'] ) ? sanitize_text_field( wp_unslash( $_GET['anchor_episode'] ) ) : null; + $spotify_url = isset( $_GET['spotify_url'] ) ? wp_unslash( $_GET['spotify_url'] ) : null; + // phpcs:enable WordPress.Security.NonceVerification.Recommended + + $data = array( + 'actions' => array(), + ); + + // add / update Spotify Badge URL. + $valid_spotify_url = \Jetpack_Gutenberg::validate_block_embed_url( $spotify_url, array( 'open.spotify.com' ) ); + if ( $valid_spotify_url ) { + $data['spotifyShowUrl'] = $valid_spotify_url; + if ( get_post_meta( $post->ID, 'jetpack_anchor_spotify_show', true ) !== $valid_spotify_url ) { + update_post_meta( $post->ID, 'jetpack_anchor_spotify_show', $valid_spotify_url ); + } + } + + if ( ! empty( $podcast_id ) ) { + $feed = 'https://anchor.fm/s/' . $podcast_id . '/podcast/rss'; + $podcast_helper = new Jetpack_Podcast_Helper( $feed ); + $rss = $podcast_helper->load_feed(); + if ( ! \is_wp_error( $rss ) ) { + update_post_meta( $post->ID, 'jetpack_anchor_podcast', $podcast_id ); + + // If we haven't got an episode ID, try and get the latest episode. + if ( empty( $episode_id ) && $rss->get_item_quantity() ) { + $latest_episode = $rss->get_item( 0 ); + if ( $latest_episode ) { + $episode_id = $latest_episode->get_id(); + } + } + + if ( ! empty( $episode_id ) ) { + $track = $podcast_helper->get_track_data( $episode_id, true ); + if ( ! \is_wp_error( $track ) ) { + update_post_meta( $post->ID, 'jetpack_anchor_episode', $track['guid'] ); + + if ( 'post-new.php' === $GLOBALS['pagenow'] ) { + $data['actions'][] = array( + 'set-episode-title', + array( + 'title' => $track['title'], + ), + ); + + $self_links = $rss->get_links( 'self' ); + $cover = $rss->get_image_url(); + + // Add insert basic template action. + $data['actions'][] = array( + 'insert-episode-template', + array( + 'feedUrl' => ! empty( $self_links ) ? esc_url_raw( $self_links[0] ) : $feed, + 'coverImage' => ! empty( $cover ) ? esc_url( $cover ) : null, + 'episodeTrack' => $track, + 'spotifyImageUrl' => Assets::staticize_subdomain( 'https://wordpress.com/i/spotify-badge.svg' ), + 'spotifyShowUrl' => esc_url_raw( $valid_spotify_url ), + ), + ); + } + } else { + $retry_url = add_query_arg( + array( + 'anchor_episode' => $episode_id, + 'anchor_podcast' => $podcast_id, + 'spotify_url' => $valid_spotify_url ? rawurlencode( $spotify_url ) : false, + ), + admin_url( 'post-new.php' ) + ); + $data['actions'][] = array( + 'create-episode-error-notice', + array( + 'retry_url' => esc_url_raw( $retry_url ), + ), + ); + } + } + } + } + + // Add Spotify Badge template action. + if ( + $valid_spotify_url && ( + 'post-new.php' !== $GLOBALS['pagenow'] // Delegate badge insertion to podcast template. + ) + ) { + $data['actions'][] = array( + 'insert-spotify-badge', + array( + 'spotifyImageUrl' => Assets::staticize_subdomain( 'https://wordpress.com/i/spotify-badge.svg' ), + 'spotifyShowUrl' => esc_url_raw( $valid_spotify_url ), + ), + ); + } + + // Display an outbound link after publishing a post (only to English-speaking users since Anchor + // is English only). + if ( + 'post' === get_post_type() && + ! get_post_meta( $post->ID, 'jetpack_anchor_spotify_show', true ) && + 0 === strpos( get_user_locale(), 'en' ) + ) { + $data['actions'][] = 'show-post-publish-outbound-link'; + } + + wp_localize_script( 'jetpack-blocks-editor', 'Jetpack_AnchorFm', $data ); +} + +add_action( 'init', __NAMESPACE__ . '\register_extension' ); +add_action( 'enqueue_block_assets', __NAMESPACE__ . '\process_anchor_params' ); diff --git a/plugins/jetpack/extensions/blocks/business-hours/business-hours.php b/plugins/jetpack/extensions/blocks/business-hours/business-hours.php index d3afa01d..4de0593a 100644 --- a/plugins/jetpack/extensions/blocks/business-hours/business-hours.php +++ b/plugins/jetpack/extensions/blocks/business-hours/business-hours.php @@ -4,20 +4,36 @@ * * @since 7.1.0 * - * @package Jetpack + * @package automattic/jetpack */ -jetpack_register_block( - 'jetpack/business-hours', - array( 'render_callback' => 'jetpack_business_hours_render' ) -); +namespace Automattic\Jetpack\Extensions\Business_Hours; + +use Automattic\Jetpack\Blocks; +use Jetpack_Gutenberg; + +const FEATURE_NAME = 'business-hours'; +const BLOCK_NAME = 'jetpack/' . FEATURE_NAME; + +/** + * 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( + BLOCK_NAME, + array( 'render_callback' => __NAMESPACE__ . '\render' ) + ); +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); /** * Get's default days / hours to render a business hour block with no data provided. * * @return array */ -function jetpack_business_hours_get_default_days() { +function get_default_days() { return array( array( 'name' => 'Sun', @@ -82,11 +98,11 @@ function jetpack_business_hours_get_default_days() { * * @return string */ -function jetpack_business_hours_render( $attributes ) { +function render( $attributes ) { global $wp_locale; if ( empty( $attributes['days'] ) || ! is_array( $attributes['days'] ) ) { - $attributes['days'] = jetpack_business_hours_get_default_days(); + $attributes['days'] = get_default_days(); } $start_of_week = (int) get_option( 'start_of_week', 0 ); @@ -119,8 +135,8 @@ function jetpack_business_hours_render( $attributes ) { } $days_hours .= sprintf( '%1$s - %2$s', - date( $time_format, $opening ), - date( $time_format, $closing ) + gmdate( $time_format, $opening ), + gmdate( $time_format, $closing ) ); if ( $key + 1 < count( $day['hours'] ) ) { $days_hours .= ', '; @@ -136,7 +152,7 @@ function jetpack_business_hours_render( $attributes ) { $content .= ''; - Jetpack_Gutenberg::load_assets_as_required( 'business-hours' ); + Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME ); /** * Allows folks to filter the HTML content for the Business Hours block diff --git a/plugins/jetpack/extensions/blocks/button/button.php b/plugins/jetpack/extensions/blocks/button/button.php new file mode 100644 index 00000000..451a2938 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/button/button.php @@ -0,0 +1,244 @@ + __NAMESPACE__ . '\render_block' ) + ); +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); + +/** + * Button block render callback. + * + * @param array $attributes Array containing the Button block attributes. + * @param string $content The Button block content. + * + * @return string + */ +function render_block( $attributes, $content ) { + $save_in_post_content = get_attribute( $attributes, 'saveInPostContent' ); + + // The Jetpack Button block depends on the core button block styles. + // The following ensures that those styles are enqueued when rendering this block. + enqueue_existing_button_style_dependency( 'core/button' ); + enqueue_existing_button_style_dependency( 'core/buttons' ); + + Jetpack_Gutenberg::load_styles_as_required( FEATURE_NAME ); + + if ( $save_in_post_content || ! class_exists( 'DOMDocument' ) ) { + return $content; + } + + $element = get_attribute( $attributes, 'element' ); + $text = get_attribute( $attributes, 'text' ); + $unique_id = get_attribute( $attributes, 'uniqueId' ); + $url = get_attribute( $attributes, 'url' ); + $classes = Blocks::classes( FEATURE_NAME, $attributes, array( 'wp-block-button' ) ); + + $button_classes = get_button_classes( $attributes ); + $button_styles = get_button_styles( $attributes ); + $wrapper_styles = get_button_wrapper_styles( $attributes ); + + $wrapper_attributes = sprintf( ' class="%s" style="%s"', esc_attr( $classes ), esc_attr( $wrapper_styles ) ); + $button_attributes = sprintf( ' class="%s" style="%s"', esc_attr( $button_classes ), esc_attr( $button_styles ) ); + + if ( empty( $unique_id ) ) { + $button_attributes .= ' data-id-attr="placeholder"'; + } else { + $button_attributes .= sprintf( ' data-id-attr="%1$s" id="%1$s"', esc_attr( $unique_id ) ); + } + + if ( 'a' === $element ) { + $button_attributes .= sprintf( ' href="%s" target="_blank" role="button" rel="noopener noreferrer"', esc_url( $url ) ); + } elseif ( 'button' === $element ) { + $button_attributes .= ' type="submit"'; + } elseif ( 'input' === $element ) { + $button_attributes .= sprintf( ' type="submit" value="%s"', wp_strip_all_tags( $text, true ) ); + } + + $button = 'input' === $element + ? '<' . $element . $button_attributes . ' />' + : '<' . $element . $button_attributes . '>' . $text . ''; + + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + return '' . $button . ''; +} + +/** + * Get the Button block classes. + * + * @param array $attributes Array containing the block attributes. + * + * @return string + */ +function get_button_classes( $attributes ) { + $classes = array( 'wp-block-button__link' ); + $has_class_name = array_key_exists( 'className', $attributes ); + $has_named_text_color = array_key_exists( 'textColor', $attributes ); + $has_custom_text_color = array_key_exists( 'customTextColor', $attributes ); + $has_named_background_color = array_key_exists( 'backgroundColor', $attributes ); + $has_custom_background_color = array_key_exists( 'customBackgroundColor', $attributes ); + $has_named_gradient = array_key_exists( 'gradient', $attributes ); + $has_custom_gradient = array_key_exists( 'customGradient', $attributes ); + $has_border_radius = array_key_exists( 'borderRadius', $attributes ); + + if ( $has_class_name ) { + $classes[] = $attributes['className']; + } + + if ( $has_named_text_color || $has_custom_text_color ) { + $classes[] = 'has-text-color'; + } + if ( $has_named_text_color ) { + $classes[] = sprintf( 'has-%s-color', $attributes['textColor'] ); + } + + if ( + $has_named_background_color || + $has_custom_background_color || + $has_named_gradient || + $has_custom_gradient + ) { + $classes[] = 'has-background'; + } + if ( $has_named_background_color && ! $has_custom_gradient ) { + $classes[] = sprintf( 'has-%s-background-color', $attributes['backgroundColor'] ); + } + if ( $has_named_gradient ) { + $classes[] = sprintf( 'has-%s-gradient-background', $attributes['gradient'] ); + } + + // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison + if ( $has_border_radius && 0 == $attributes['borderRadius'] ) { + $classes[] = 'no-border-radius'; + } + + return implode( ' ', $classes ); +} + +/** + * Get the Button block styles. + * + * @param array $attributes Array containing the block attributes. + * + * @return string + */ +function get_button_styles( $attributes ) { + $styles = array(); + $has_named_text_color = array_key_exists( 'textColor', $attributes ); + $has_custom_text_color = array_key_exists( 'customTextColor', $attributes ); + $has_named_background_color = array_key_exists( 'backgroundColor', $attributes ); + $has_custom_background_color = array_key_exists( 'customBackgroundColor', $attributes ); + $has_named_gradient = array_key_exists( 'gradient', $attributes ); + $has_custom_gradient = array_key_exists( 'customGradient', $attributes ); + $has_border_radius = array_key_exists( 'borderRadius', $attributes ); + $has_width = array_key_exists( 'width', $attributes ); + + if ( ! $has_named_text_color && $has_custom_text_color ) { + $styles[] = sprintf( 'color: %s;', $attributes['customTextColor'] ); + } + + if ( ! $has_named_background_color && ! $has_named_gradient && $has_custom_gradient ) { + $styles[] = sprintf( 'background: %s;', $attributes['customGradient'] ); + } + + if ( + $has_custom_background_color && + ! $has_named_background_color && + ! $has_named_gradient && + ! $has_custom_gradient + ) { + $styles[] = sprintf( 'background-color: %s;', $attributes['customBackgroundColor'] ); + } + + // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison + if ( $has_border_radius && 0 != $attributes['borderRadius'] ) { + $styles[] = sprintf( 'border-radius: %spx;', $attributes['borderRadius'] ); + } + + if ( $has_width ) { + $styles[] = sprintf( 'width: %s;', $attributes['width'] ); + $styles[] = 'max-width: 100%'; + } + + return implode( ' ', $styles ); +} + +/** + * Get the Button wrapper block styles. + * + * @param array $attributes Array containing the block attributes. + * + * @return string + */ +function get_button_wrapper_styles( $attributes ) { + $styles = array(); + $has_width = array_key_exists( 'width', $attributes ); + + if ( $has_width ) { + $styles[] = 'max-width: 100%'; + } + + return implode( ' ', $styles ); +} + +/** + * Get filtered attributes. + * + * @param array $attributes Array containing the Button block attributes. + * @param string $attribute_name String containing the attribute name to get. + * + * @return string + */ +function get_attribute( $attributes, $attribute_name ) { + if ( isset( $attributes[ $attribute_name ] ) ) { + return $attributes[ $attribute_name ]; + } + + $default_attributes = array( + 'url' => '#', + 'element' => 'a', + 'saveInPostContent' => false, + ); + + if ( isset( $default_attributes[ $attribute_name ] ) ) { + return $default_attributes[ $attribute_name ]; + } +} + +/** + * Enqueue style for an existing block. + * + * The Jetpack Button block depends on styles from the core button block. + * In case that block is not already within the post content, we can use + * this function to ensure the block's style assets are enqueued. + * + * @param string $block_name Block type name including namespace. + */ +function enqueue_existing_button_style_dependency( $block_name ) { + $existing_block = \WP_Block_Type_Registry::get_instance()->get_registered( $block_name ); + if ( isset( $existing_block ) && ! empty( $existing_block->style ) ) { + wp_enqueue_style( $existing_block->style ); + } +} diff --git a/plugins/jetpack/extensions/blocks/calendly/calendly.php b/plugins/jetpack/extensions/blocks/calendly/calendly.php index 0875a04e..42653ff5 100644 --- a/plugins/jetpack/extensions/blocks/calendly/calendly.php +++ b/plugins/jetpack/extensions/blocks/calendly/calendly.php @@ -4,71 +4,32 @@ * * @since 8.2.0 * - * @package Jetpack + * @package automattic/jetpack */ -namespace Jetpack\Calendly_Block; +namespace Automattic\Jetpack\Extensions\Calendly; + +use Automattic\Jetpack\Blocks; +use Jetpack_Gutenberg; const FEATURE_NAME = 'calendly'; const BLOCK_NAME = 'jetpack/' . FEATURE_NAME; -/** - * Check if the block should be available on the site. - * - * @return bool - */ -function is_available() { - if ( - defined( 'IS_WPCOM' ) - && IS_WPCOM - && function_exists( 'has_any_blog_stickers' ) - ) { - if ( has_any_blog_stickers( - array( 'premium-plan', 'business-plan', 'ecommerce-plan' ), - get_current_blog_id() - ) ) { - return true; - } - return false; - } - - return true; -} - /** * 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() { - if ( is_available() ) { - jetpack_register_block( - BLOCK_NAME, - array( 'render_callback' => 'Jetpack\Calendly_Block\load_assets' ) - ); - } -} -add_action( 'init', 'Jetpack\Calendly_Block\register_block' ); - -/** - * Set the availability of the block as the editor - * is loaded - */ -function set_availability() { - if ( is_available() ) { - \Jetpack_Gutenberg::set_extension_available( BLOCK_NAME ); - } else { - \Jetpack_Gutenberg::set_extension_unavailable( - BLOCK_NAME, - 'missing_plan', - array( - 'required_feature' => 'calendly', - 'required_plan' => 'value_bundle', - ) - ); - } + Blocks::jetpack_register_block( + BLOCK_NAME, + array( + 'render_callback' => __NAMESPACE__ . '\load_assets', + 'plan_check' => true, + ) + ); } -add_action( 'init', 'Jetpack\Calendly_Block\set_availability' ); +add_action( 'init', __NAMESPACE__ . '\register_block' ); /** * Calendly block registration/dependency declaration. @@ -79,7 +40,14 @@ add_action( 'init', 'Jetpack\Calendly_Block\set_availability' ); * @return string */ function load_assets( $attr, $content ) { - $url = get_attribute( $attr, 'url' ); + + if ( is_admin() ) { + return; + } + $url = Jetpack_Gutenberg::validate_block_embed_url( + get_attribute( $attr, 'url' ), + array( 'calendly.com' ) + ); if ( empty( $url ) ) { return; } @@ -87,27 +55,23 @@ function load_assets( $attr, $content ) { /* * Enqueue necessary scripts and styles. */ - \Jetpack_Gutenberg::load_assets_as_required( 'calendly' ); - wp_enqueue_script( - 'jetpack-calendly-external-js', - 'https://assets.calendly.com/assets/external/widget.js', - null, - JETPACK__VERSION, - false - ); + Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME ); - $style = get_attribute( $attr, 'style' ); - $hide_event_type_details = get_attribute( $attr, 'hideEventTypeDetails' ); - $background_color = get_attribute( $attr, 'backgroundColor' ); - $text_color = get_attribute( $attr, 'textColor' ); - $primary_color = get_attribute( $attr, 'primaryColor' ); - $submit_button_text = get_attribute( $attr, 'submitButtonText' ); - $submit_button_text_color = get_attribute( $attr, 'customTextButtonColor' ); - $submit_button_background_color = get_attribute( $attr, 'customBackgroundButtonColor' ); - $classes = \Jetpack_Gutenberg::block_classes( 'calendly', $attr ); - $block_id = wp_unique_id( 'calendly-block-' ); - - $url = add_query_arg( + $style = get_attribute( $attr, 'style' ); + $hide_event_type_details = get_attribute( $attr, 'hideEventTypeDetails' ); + $background_color = get_attribute( $attr, 'backgroundColor' ); + $text_color = get_attribute( $attr, 'textColor' ); + $primary_color = get_attribute( $attr, 'primaryColor' ); + $classes = Blocks::classes( FEATURE_NAME, $attr, array( 'calendly-style-' . $style ) ); + $block_id = wp_unique_id( 'calendly-block-' ); + $is_amp_request = Blocks::is_amp_request(); + + if ( ! wp_script_is( 'jetpack-calendly-external-js' ) && ! $is_amp_request ) { + enqueue_calendly_js(); + } + + $base_url = $url; + $url = add_query_arg( array( 'hide_event_type_details' => (int) $hide_event_type_details, 'background_color' => sanitize_hex_color_no_hash( $background_color ), @@ -118,40 +82,44 @@ function load_assets( $attr, $content ) { ); if ( 'link' === $style ) { - wp_enqueue_style( 'jetpack-calendly-external-css', 'https://assets.calendly.com/assets/external/widget.css', null, JETPACK__VERSION ); - - /* - * If we have some additional styles from the editor - * (a custom text color, custom bg color, or both ) - * Let's add that CSS inline. - */ - if ( ! empty( $submit_button_text_color ) || ! empty( $submit_button_background_color ) ) { - $inline_styles = sprintf( - '#%1$s .wp-block-button__link{%2$s%3$s}', - esc_attr( $block_id ), - ! empty( $submit_button_text_color ) - ? 'color:#' . sanitize_hex_color_no_hash( $submit_button_text_color ) . ';' - : '', - ! empty( $submit_button_background_color ) - ? 'background-color:#' . sanitize_hex_color_no_hash( $submit_button_background_color ) . ';' - : '' - ); - wp_add_inline_style( 'jetpack-calendly-external-css', $inline_styles ); + if ( ! wp_style_is( 'jetpack-calendly-external-css' ) ) { + wp_enqueue_style( 'jetpack-calendly-external-css', 'https://assets.calendly.com/assets/external/widget.css', null, JETPACK__VERSION ); } - $content = sprintf( - '', - esc_attr( $classes ), - esc_attr( $block_id ), - esc_js( $url ), - wp_kses_post( $submit_button_text ) - ); + // Render deprecated version of Calendly block if needed. New markup block button class before rendering here. + if ( false === strpos( $content, 'wp-block-jetpack-button' ) ) { + $content = deprecated_render_button_v1( $attr, $block_id, $classes, $url ); + } else { + $content = str_replace( 'calendly-widget-id', esc_attr( $block_id ), $content ); + $content = str_replace( $base_url, $url, $content ); + } + + if ( ! $is_amp_request ) { + wp_add_inline_script( + 'jetpack-calendly-external-js', + sprintf( "calendly_attach_link_events( '%s' )", esc_js( $block_id ) ) + ); + } } else { // Inline style. - $content = sprintf( - '
', - esc_attr( $classes ), - esc_url( $url ) - ); + if ( $is_amp_request ) { + $content = sprintf( + '', + esc_attr( Blocks::classes( FEATURE_NAME, $attr ) ), + esc_attr( $block_id ), + esc_js( $url ), + wp_kses_post( get_attribute( $attr, 'submitButtonText' ) ) + ); + } else { + $content = sprintf( + '
', + esc_attr( $classes ), + esc_attr( $block_id ) + ); + $script = <<%5$s', + esc_attr( $classes ), + esc_attr( $block_id ), + ! empty( $submit_button_classes ) ? esc_attr( $submit_button_classes ) : 'wp-block-button__link', + esc_js( $url ), + wp_kses_post( $submit_button_text ) + ); +} diff --git a/plugins/jetpack/extensions/blocks/contact-info/class-jetpack-contact-info-block.php b/plugins/jetpack/extensions/blocks/contact-info/class-jetpack-contact-info-block.php index cecf6949..74386291 100644 --- a/plugins/jetpack/extensions/blocks/contact-info/class-jetpack-contact-info-block.php +++ b/plugins/jetpack/extensions/blocks/contact-info/class-jetpack-contact-info-block.php @@ -2,7 +2,7 @@ /** * Class Jetpack_Contact_Info_Block * - * @package Jetpack + * @package automattic/jetpack */ /** diff --git a/plugins/jetpack/extensions/blocks/contact-info/contact-info.php b/plugins/jetpack/extensions/blocks/contact-info/contact-info.php index c7414517..37d969ee 100644 --- a/plugins/jetpack/extensions/blocks/contact-info/contact-info.php +++ b/plugins/jetpack/extensions/blocks/contact-info/contact-info.php @@ -4,17 +4,19 @@ * * @since 7.1.0 * - * @package Jetpack + * @package automattic/jetpack */ -jetpack_register_block( +use Automattic\Jetpack\Blocks; + +Blocks::jetpack_register_block( 'jetpack/contact-info', array( 'render_callback' => array( 'Jetpack_Contact_Info_Block', 'render' ), ) ); -jetpack_register_block( +Blocks::jetpack_register_block( 'jetpack/address', array( 'parent' => array( 'jetpack/contact-info' ), @@ -22,7 +24,7 @@ jetpack_register_block( ) ); -jetpack_register_block( +Blocks::jetpack_register_block( 'jetpack/email', array( 'parent' => array( 'jetpack/contact-info' ), @@ -30,11 +32,11 @@ jetpack_register_block( ) ); -jetpack_register_block( +Blocks::jetpack_register_block( 'jetpack/phone', array( 'parent' => array( 'jetpack/contact-info' ), 'render_callback' => array( 'Jetpack_Contact_Info_Block', 'render_phone' ), ) ); -require_once dirname( __FILE__ ) . '/class-jetpack-contact-info-block.php'; +require_once __DIR__ . '/class-jetpack-contact-info-block.php'; diff --git a/plugins/jetpack/extensions/blocks/conversation/conversation.php b/plugins/jetpack/extensions/blocks/conversation/conversation.php new file mode 100644 index 00000000..b9de4caa --- /dev/null +++ b/plugins/jetpack/extensions/blocks/conversation/conversation.php @@ -0,0 +1,51 @@ + __NAMESPACE__ . '\render_block', + $provides => array( + 'jetpack/conversation-participants' => 'participants', + 'jetpack/conversation-showTimestamps' => 'showTimestamps', + ), + ) + ); +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); + +/** + * Conversation block registration/dependency declaration. + * + * @param array $attr Array containing the Conversation block attributes. + * @param string $content String containing the Conversation block content. + * + * @return string + */ +function render_block( $attr, $content ) { + Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME ); + return $content; +} diff --git a/plugins/jetpack/extensions/blocks/dialogue/dialogue.php b/plugins/jetpack/extensions/blocks/dialogue/dialogue.php new file mode 100644 index 00000000..4ee09653 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/dialogue/dialogue.php @@ -0,0 +1,51 @@ + __NAMESPACE__ . '\render_block', + $uses => array( + 'jetpack/conversation-participants', + 'jetpack/conversation-showTimestamps', + ), + ) + ); +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); + +/** + * Dialogue block registration/dependency declaration. + * + * @param array $attrs Array containing the Dialogue block attributes. + * @param string $content String containing the Dialogue block content. + * + * @return string + */ +function render_block( $attrs, $content ) { + Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME ); + + return $content; +} diff --git a/plugins/jetpack/extensions/blocks/donations/donations.php b/plugins/jetpack/extensions/blocks/donations/donations.php new file mode 100644 index 00000000..e93a1fe4 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/donations/donations.php @@ -0,0 +1,256 @@ + __NAMESPACE__ . '\render_block', + 'plan_check' => true, + 'attributes' => array( + 'currency' => array( + 'type' => 'string', + 'default' => 'USD', + ), + 'oneTimeDonation' => array( + 'type' => 'object', + 'default' => array( + 'show' => true, + 'planId' => null, + 'amounts' => array( 5, 15, 100 ), + 'heading' => __( 'Make a one-time donation', 'jetpack' ), + 'extraText' => __( 'Your contribution is appreciated.', 'jetpack' ), + 'buttonText' => __( 'Donate', 'jetpack' ), + ), + ), + 'monthlyDonation' => array( + 'type' => 'object', + 'default' => array( + 'show' => true, + 'planId' => null, + 'amounts' => array( 5, 15, 100 ), + 'heading' => __( 'Make a monthly donation', 'jetpack' ), + 'extraText' => __( 'Your contribution is appreciated.', 'jetpack' ), + 'buttonText' => __( 'Donate monthly', 'jetpack' ), + ), + ), + 'annualDonation' => array( + 'type' => 'object', + 'default' => array( + 'show' => true, + 'planId' => null, + 'amounts' => array( 5, 15, 100 ), + 'heading' => __( 'Make a yearly donation', 'jetpack' ), + 'extraText' => __( 'Your contribution is appreciated.', 'jetpack' ), + 'buttonText' => __( 'Donate yearly', 'jetpack' ), + ), + ), + 'showCustomAmount' => array( + 'type' => 'boolean', + 'default' => true, + ), + 'chooseAmountText' => array( + 'type' => 'string', + 'default' => __( 'Choose an amount', 'jetpack' ), + ), + 'customAmountText' => array( + 'type' => 'string', + 'default' => __( 'Or enter a custom amount', 'jetpack' ), + ), + 'fallbackLinkUrl' => array( + 'type' => 'string', + ), + ), + ) + ); +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); + +/** + * Donations block dynamic rendering. + * + * @param array $attr Array containing the Donations block attributes. + * @param string $content String containing the Donations block content. + * + * @return string + */ +function render_block( $attr, $content ) { + // Keep content as-is if rendered in other contexts than frontend (i.e. feed, emails, API, etc.). + if ( ! jetpack_is_frontend() ) { + return $content; + } + + Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME, array( 'thickbox' ) ); + add_thickbox(); + + require_once JETPACK__PLUGIN_DIR . 'modules/memberships/class-jetpack-memberships.php'; + jetpack_require_lib( 'class-jetpack-currencies' ); + + $donations = array( + 'one-time' => array_merge( + array( + 'title' => __( 'One-Time', 'jetpack' ), + 'class' => 'donations__one-time-item', + ), + $attr['oneTimeDonation'] + ), + ); + if ( $attr['monthlyDonation']['show'] ) { + $donations['1 month'] = array_merge( + array( + 'title' => __( 'Monthly', 'jetpack' ), + 'class' => 'donations__monthly-item', + ), + $attr['monthlyDonation'] + ); + } + if ( $attr['annualDonation']['show'] ) { + $donations['1 year'] = array_merge( + array( + 'title' => __( 'Yearly', 'jetpack' ), + 'class' => 'donations__annual-item', + ), + $attr['annualDonation'] + ); + } + + $currency = $attr['currency']; + $nav = ''; + $headings = ''; + $amounts = ''; + $extra_text = ''; + $buttons = ''; + foreach ( $donations as $interval => $donation ) { + $plan_id = (int) $donation['planId']; + $plan = get_post( $plan_id ); + if ( ! $plan || is_wp_error( $plan ) ) { + continue; + } + + if ( count( $donations ) > 1 ) { + if ( ! $nav ) { + $nav .= '
'; + } + $nav .= sprintf( + '
%2$s
', + esc_attr( $interval ), + esc_html( $donation['title'] ) + ); + } + $headings .= sprintf( + '

%2$s

', + esc_attr( $donation['class'] ), + wp_kses_post( $donation['heading'] ) + ); + $amounts .= sprintf( + '
', + esc_attr( $donation['class'] ) + ); + foreach ( $donation['amounts'] as $amount ) { + $amounts .= sprintf( + '
%2$s
', + esc_attr( $amount ), + esc_html( \Jetpack_Currencies::format_price( $amount, $currency ) ) + ); + } + $amounts .= '
'; + $extra_text .= sprintf( + '

%2$s

', + esc_attr( $donation['class'] ), + wp_kses_post( $donation['extraText'] ) + ); + $buttons .= sprintf( + '%3$s', + esc_attr( $donation['class'] ), + esc_url( \Jetpack_Memberships::get_instance()->get_subscription_url( $plan_id ) ), + wp_kses_post( $donation['buttonText'] ) + ); + } + if ( $nav ) { + $nav .= '
'; + } + + $custom_amount = ''; + if ( $attr['showCustomAmount'] ) { + $custom_amount .= sprintf( + '

%s

', + wp_kses_post( $attr['customAmountText'] ) + ); + $default_custom_amount = \Jetpack_Memberships::SUPPORTED_CURRENCIES[ $currency ] * 100; + $custom_amount .= sprintf( + '
+ %1$s +
+
', + esc_html( \Jetpack_Currencies::CURRENCIES[ $attr['currency'] ]['symbol'] ), + esc_attr( $attr['currency'] ), + esc_attr( \Jetpack_Currencies::format_price( $default_custom_amount, $currency, false ) ) + ); + } + + return sprintf( + ' +
+
+ %2$s +
+
+ %3$s +

%4$s

+ %5$s + %6$s +
+ %7$s + %8$s +
+
+
+', + esc_attr( Blocks::classes( FEATURE_NAME, $attr ) ), + $nav, + $headings, + $attr['chooseAmountText'], + $amounts, + $custom_amount, + $extra_text, + $buttons + ); +} + +/** + * Determine if AMP should be disabled on posts having Donations blocks. + * + * @param bool $skip Skipped. + * @param int $post_id Post ID. + * @param WP_Post $post Post. + * + * @return bool Whether to skip the post from AMP. + */ +function amp_skip_post( $skip, $post_id, $post ) { + // When AMP is on standard mode, there are no non-AMP posts to link to where the donation can be completed, so let's + // prevent the post from being available in AMP. + if ( function_exists( 'amp_is_canonical' ) && \amp_is_canonical() && has_block( BLOCK_NAME, $post->post_content ) ) { + return true; + } + return $skip; +} +add_filter( 'amp_skip_post', __NAMESPACE__ . '\amp_skip_post', 10, 3 ); diff --git a/plugins/jetpack/extensions/blocks/eventbrite/eventbrite.php b/plugins/jetpack/extensions/blocks/eventbrite/eventbrite.php index 0135837e..b68ba64d 100644 --- a/plugins/jetpack/extensions/blocks/eventbrite/eventbrite.php +++ b/plugins/jetpack/extensions/blocks/eventbrite/eventbrite.php @@ -4,15 +4,48 @@ * * @since 8.2.0 * - * @package Jetpack + * @package automattic/jetpack */ -jetpack_register_block( - 'jetpack/eventbrite', - array( - 'render_callback' => 'jetpack_render_eventbrite_block', - ) -); +namespace Automattic\Jetpack\Extensions\Eventbrite; + +use Automattic\Jetpack\Blocks; +use Jetpack_Gutenberg; + +const FEATURE_NAME = 'eventbrite'; +const BLOCK_NAME = 'jetpack/' . FEATURE_NAME; + +/** + * 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( + BLOCK_NAME, + array( 'render_callback' => __NAMESPACE__ . '\render_block' ) + ); +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); + +/** + * Get current URL. + * + * @return string Current URL. + */ +function get_current_url() { + if ( isset( $_SERVER['HTTP_HOST'] ) ) { + $host = wp_unslash( $_SERVER['HTTP_HOST'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + } else { + $host = wp_parse_url( home_url(), PHP_URL_HOST ); + } + if ( isset( $_SERVER['REQUEST_URI'] ) ) { + $path = wp_unslash( $_SERVER['REQUEST_URI'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + } else { + $path = '/'; + } + return esc_url_raw( ( is_ssl() ? 'https' : 'http' ) . '://' . $host . $path ); +} /** * Eventbrite block registration/dependency delclaration. @@ -20,22 +53,75 @@ jetpack_register_block( * @param array $attr Eventbrite block attributes. * @param string $content Rendered embed element (without scripts) from the block editor. * - * @return string + * @return string Rendered block. */ -function jetpack_render_eventbrite_block( $attr, $content ) { +function render_block( $attr, $content ) { if ( is_admin() || empty( $attr['eventId'] ) || empty( $attr['url'] ) ) { return ''; } + $attr['url'] = Jetpack_Gutenberg::validate_block_embed_url( + $attr['url'], + array( '#^https?:\/\/(?:[0-9a-z]+\.)?eventbrite\.(?:com|co\.uk|com\.ar|com\.au|be|com\.br|ca|cl|co|dk|de|es|fi|fr|hk|ie|it|com\.mx|nl|co\.nz|at|com\.pe|pt|ch|sg|se)\/e\/[^\/]*?(?:\d+)\/?(?:\?[^\/]*)?$#' ), + true + ); + $widget_id = wp_unique_id( 'eventbrite-widget-' ); - wp_enqueue_script( 'eventbrite-widget', 'https://www.eventbrite.com/static/widgets/eb_widgets.js', array(), JETPACK__VERSION, true ); + // Show the embedded version. + if ( empty( $attr['useModal'] ) && ( empty( $attr['style'] ) || 'modal' !== $attr['style'] ) ) { + return render_embed_block( $widget_id, Blocks::is_amp_request(), $attr ); + } else { + return render_modal_block( $widget_id, Blocks::is_amp_request(), $attr, $content ); + } +} - // Add CSS to hide direct link. - Jetpack_Gutenberg::load_assets_as_required( 'eventbrite' ); +/** + * Render block with embed style. + * + * @param string $widget_id Widget ID to use. + * @param bool $is_amp Whether AMP page. + * @param array $attr Eventbrite block attributes. + * @return string Rendered block. + */ +function render_embed_block( $widget_id, $is_amp, $attr ) { + + // $content contains a fallback link to the event that's saved in the post_content. + // Append a div that will hold the iframe embed created by the Eventbrite widget.js. + $classes = Blocks::classes( FEATURE_NAME, $attr ); + + $classes .= ' wp-block-jetpack-eventbrite--embed'; + + $direct_link = sprintf( + '%s', + esc_url( $attr['url'] ), + $is_amp ? 'placeholder fallback' : '', + esc_html__( 'Register on Eventbrite', 'jetpack' ) + ); + + if ( $is_amp ) { + $embed = sprintf( + '%s', + esc_url( + add_query_arg( + array( + 'eid' => $attr['eventId'], + 'parent' => rawurlencode( get_current_url() ), + ), + 'https://www.eventbrite.com/checkout-external' + ) + ), + esc_html__( 'Expand', 'jetpack' ), + $direct_link + ); + } else { + $embed = $direct_link; + + wp_enqueue_script( 'eventbrite-widget', 'https://www.eventbrite.com/static/widgets/eb_widgets.js', array(), JETPACK__VERSION, true ); + + // Add CSS to hide direct link. + Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME ); - // Show the embedded version. - if ( empty( $attr['useModal'] ) ) { wp_add_inline_script( 'eventbrite-widget', "window.EBWidgets.createWidget( { @@ -44,22 +130,84 @@ function jetpack_render_eventbrite_block( $attr, $content ) { iframeContainerId: '" . esc_js( $widget_id ) . "', } );" ); + } + + return sprintf( + '
%3$s
', + esc_attr( $widget_id ), + esc_attr( $classes ), + $embed + ); +} + +/** + * Render block with modal style. + * + * @param string $widget_id Widget ID to use. + * @param bool $is_amp Whether AMP page. + * @param array $attr Eventbrite block attributes. + * @param string $content Rendered embed element (without scripts) from the block editor. + * @return string Rendered block. + */ +function render_modal_block( $widget_id, $is_amp, $attr, $content ) { + + if ( $is_amp ) { + $lightbox_id = "{$widget_id}-lightbox"; + + // Add CSS to for lightbox. + Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME ); + + $content = preg_replace( + '/\shref="#" target="_blank/', + sprintf( ' on="%s" ', esc_attr( "tap:{$lightbox_id}.open" ) ), + $content + ); + + $iframe_src = add_query_arg( + array( + // Note that modal=1 is intentionally omitted here since we need to put the close button inside the amp-lightbox. + 'eid' => $attr['eventId'], + 'parent' => rawurlencode( get_current_url() ), + ), + 'https://www.eventbrite.com/checkout-external' + ); - // $content contains a fallback link to the event that's saved in the post_content. - // Append a div that will hold the iframe embed created by the Eventbrite widget.js. - $content .= sprintf( - '
', - esc_attr( $widget_id ) + $lightbox = sprintf( + '%3$s', + esc_attr( $lightbox_id ), + esc_attr( "tap:{$lightbox_id}.close" ), + sprintf( + ' +
+
+ + + + + + + + +
+
+ ', + esc_url( $iframe_src ), + esc_attr( "tap:{$lightbox_id}.close" ), + esc_attr__( 'Close', 'jetpack' ) + ) ); - return sprintf( - '%s', - $content, - esc_url( $attr['url'] ), - esc_html__( 'Register on Eventbrite', 'jetpack' ) + $content = preg_replace( + ':(?=
\s*$):', + $lightbox, + $content ); + + return $content; } + wp_enqueue_script( 'eventbrite-widget', 'https://www.eventbrite.com/static/widgets/eb_widgets.js', array(), JETPACK__VERSION, true ); + // Show the modal version. wp_add_inline_script( 'eventbrite-widget', @@ -95,7 +243,20 @@ function jetpack_render_eventbrite_block( $attr, $content ) { ); // Replace the placeholder id saved in the post_content with a unique id used by widget.js. - $content = preg_replace( '/eventbrite-widget-\d+/', $widget_id, $content ); + $content = str_replace( 'eventbrite-widget-id', esc_attr( $widget_id ), $content ); + + // Fallback for block version deprecated/v2. + $content = preg_replace( '/eventbrite-widget-\d+/', esc_attr( $widget_id ), $content ); + + // Inject URL to event in case the JS for the lightbox fails to load. + $content = preg_replace( + '/\shref="#"/', + sprintf( + ' href="%s" rel="noopener noreferrer" target="_blank"', + esc_url( $attr['url'] ) + ), + $content + ); return $content; } diff --git a/plugins/jetpack/extensions/blocks/gif/gif.php b/plugins/jetpack/extensions/blocks/gif/gif.php index 1ababeeb..cb860e9a 100644 --- a/plugins/jetpack/extensions/blocks/gif/gif.php +++ b/plugins/jetpack/extensions/blocks/gif/gif.php @@ -4,15 +4,29 @@ * * @since 7.0.0 * - * @package Jetpack + * @package automattic/jetpack */ -jetpack_register_block( - 'jetpack/gif', - array( - 'render_callback' => 'jetpack_gif_block_render', - ) -); +namespace Automattic\Jetpack\Extensions\Gif; + +use Automattic\Jetpack\Blocks; +use Jetpack_Gutenberg; + +const FEATURE_NAME = 'gif'; +const BLOCK_NAME = 'jetpack/' . FEATURE_NAME; + +/** + * 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( + BLOCK_NAME, + array( 'render_callback' => __NAMESPACE__ . '\render_block' ) + ); +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); /** * Gif block registration/dependency declaration. @@ -21,10 +35,12 @@ jetpack_register_block( * * @return string */ -function jetpack_gif_block_render( $attr ) { +function render_block( $attr ) { $padding_top = isset( $attr['paddingTop'] ) ? $attr['paddingTop'] : 0; $style = 'padding-top:' . $padding_top; - $giphy_url = isset( $attr['giphyUrl'] ) ? $attr['giphyUrl'] : null; + $giphy_url = isset( $attr['giphyUrl'] ) + ? Jetpack_Gutenberg::validate_block_embed_url( $attr['giphyUrl'], array( 'giphy.com' ) ) + : null; $search_text = isset( $attr['searchText'] ) ? $attr['searchText'] : ''; $caption = isset( $attr['caption'] ) ? $attr['caption'] : null; @@ -32,7 +48,7 @@ function jetpack_gif_block_render( $attr ) { return null; } - $classes = Jetpack_Gutenberg::block_classes( 'gif', $attr ); + $classes = Blocks::classes( FEATURE_NAME, $attr ); $placeholder = sprintf( '%s', esc_url( $giphy_url ), esc_attr( $search_text ) ); @@ -40,7 +56,7 @@ function jetpack_gif_block_render( $attr ) { ?>
- +
@@ -59,7 +75,7 @@ function jetpack_gif_block_render( $attr ) { __NAMESPACE__ . '\load_assets', + ) + ); +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); + +/** + * Google Calendar block registration/dependency declaration. + * + * @param array $attr Array containing the Google Calendar block attributes. + * @return string + */ +function load_assets( $attr ) { + $height = isset( $attr['height'] ) ? $attr['height'] : '600'; + $url = isset( $attr['url'] ) + ? Jetpack_Gutenberg::validate_block_embed_url( $attr['url'], array( 'calendar.google.com' ) ) : + ''; + $classes = Blocks::classes( FEATURE_NAME, $attr ); + + Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME ); + + if ( empty( $url ) ) { + return ''; + } + + $sandbox = 'allow-scripts allow-same-origin allow-popups'; + if ( Blocks::is_amp_request() ) { + $noscript_src = str_replace( + '//calendar.google.com/calendar/embed', + '//calendar.google.com/calendar/htmlembed', + $url + ); + + $iframe = sprintf( + '%4$s%5$s', + esc_url( $url ), + absint( $height ), + esc_attr( $sandbox ), + sprintf( + '%s', + esc_url( $url ), + esc_html__( 'Google Calendar', 'jetpack' ) + ), + sprintf( + '', + esc_url( $noscript_src ), + esc_attr( $sandbox ) + ) + ); + } else { + $iframe = sprintf( + '', + esc_url( $url ), + absint( $height ), + esc_attr( $sandbox ) + ); + } + + return sprintf( '
%s
', esc_attr( $classes ), $iframe ); +} diff --git a/plugins/jetpack/extensions/blocks/image-compare/image-compare.php b/plugins/jetpack/extensions/blocks/image-compare/image-compare.php new file mode 100644 index 00000000..54683841 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/image-compare/image-compare.php @@ -0,0 +1,90 @@ + __NAMESPACE__ . '\load_assets' ) + ); +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); + +/** + * Image Compare block registration/dependency declaration. + * + * @param array $attr Array containing the image-compare block attributes. + * @param string $content String containing the image-compare block content. + * + * @return string + */ +function load_assets( $attr, $content ) { + Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME ); + wp_localize_script( + 'jetpack-block-' . sanitize_title_with_dashes( FEATURE_NAME ), + 'imageCompareHandle', + array( + 'msg' => __( 'Slide to compare images', 'jetpack' ), + ) + ); + if ( Blocks::is_amp_request() ) { + $content = preg_replace( + '#
#s', + render_amp( $attr ), + $content + ); + } + + return $content; +} + +/** + * Render image compare block for AMP + * + * @param array $attr Array containing the image-compare block attributes. + * + * @return string Markup for amp-image-slider. + */ +function render_amp( $attr ) { + $img_before = $attr['imageBefore']; + $img_after = $attr['imageAfter']; + + $width = ! empty( $img_before['width'] ) ? absint( $img_before['width'] ) : 0; + $height = ! empty( $img_before['height'] ) ? absint( $img_before['height'] ) : 0; + + // As fallback, give 1:1 aspect ratio. + if ( ! $width || ! $height ) { + $width = 1; + $height = 1; + } + + return sprintf( + ' ', + esc_attr( $width ), + esc_attr( $height ), + absint( $img_before['id'] ), + esc_url( $img_before['url'] ), + esc_attr( $img_before['alt'] ), + absint( $img_after['id'] ), + esc_url( $img_after['url'] ), + esc_attr( $img_after['alt'] ) + ); +} diff --git a/plugins/jetpack/extensions/blocks/instagram-gallery/instagram-gallery.php b/plugins/jetpack/extensions/blocks/instagram-gallery/instagram-gallery.php new file mode 100644 index 00000000..586c71e1 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/instagram-gallery/instagram-gallery.php @@ -0,0 +1,151 @@ + __NAMESPACE__ . '\render_block' ) + ); + } +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); + +/** + * Instagram Gallery block render callback. + * + * @param array $attributes Array containing the Instagram Gallery block attributes. + * @param string $content The Instagram Gallery block content. + * + * @return string + */ +function render_block( $attributes, $content ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + if ( ! array_key_exists( 'accessToken', $attributes ) ) { + return ''; + } + + $access_token = $attributes['accessToken']; + $columns = get_instagram_gallery_attribute( 'columns', $attributes ); + $count = get_instagram_gallery_attribute( 'count', $attributes ); + $is_stacked_on_mobile = get_instagram_gallery_attribute( 'isStackedOnMobile', $attributes ); + $spacing = get_instagram_gallery_attribute( 'spacing', $attributes ); + + $grid_classes = Blocks::classes( + FEATURE_NAME, + $attributes, + array( + 'wp-block-jetpack-instagram-gallery__grid', + 'wp-block-jetpack-instagram-gallery__grid-columns-' . $columns, + ( $is_stacked_on_mobile ? 'is-stacked-on-mobile' : null ), + ) + ); + + $grid_style = sprintf( + 'grid-gap: %1$spx; --latest-instagram-posts-spacing: %1$spx;', + $spacing + ); + + if ( ! class_exists( 'Jetpack_Instagram_Gallery_Helper' ) ) { + \jetpack_require_lib( 'class-jetpack-instagram-gallery-helper' ); + } + $gallery = Jetpack_Instagram_Gallery_Helper::get_instagram_gallery( $access_token, $count ); + + if ( is_wp_error( $gallery ) || ! property_exists( $gallery, 'images' ) || 'ERROR' === $gallery->images ) { + if ( ! current_user_can( 'edit_post', get_the_ID() ) ) { + return ''; + } + + $connection_unavailable = is_wp_error( $gallery ) && 'instagram_connection_unavailable' === $gallery->get_error_code(); + + $error_message = $connection_unavailable + ? $gallery->get_error_message() + : esc_html__( 'An error occurred in the Latest Instagram Posts block. Please try again later.', 'jetpack' ); + + $message = $error_message + . '
' + . esc_html__( '(Only administrators and the post author will see this message.)', 'jetpack' ); + return Jetpack_Gutenberg::notice( $message, 'error', Blocks::classes( FEATURE_NAME, $attributes ) ); + } + + if ( empty( $gallery->images ) ) { + return ''; + } + + $images = array_slice( $gallery->images, 0, $count ); + + Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME ); + + ob_start(); + ?> + + + +
+ + + <?php echo esc_attr( $image->title ? $image->title : $image->link ); ?> + + +
+ + 3, + 'count' => 9, + 'isStackedOnMobile' => true, + 'spacing' => 10, + ); + + if ( array_key_exists( $attribute, $default_attributes ) ) { + return $default_attributes[ $attribute ]; + } +} diff --git a/plugins/jetpack/extensions/blocks/mailchimp/mailchimp.php b/plugins/jetpack/extensions/blocks/mailchimp/mailchimp.php index 80641a1c..1640e136 100644 --- a/plugins/jetpack/extensions/blocks/mailchimp/mailchimp.php +++ b/plugins/jetpack/extensions/blocks/mailchimp/mailchimp.php @@ -4,39 +4,185 @@ * * @since 7.1.0 * - * @package Jetpack + * @package automattic/jetpack */ -if ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) || Jetpack::is_active() ) { - jetpack_register_block( - 'jetpack/mailchimp', - array( - 'render_callback' => 'jetpack_mailchimp_block_load_assets', - ) - ); +namespace Automattic\Jetpack\Extensions\Mailchimp; + +use Automattic\Jetpack\Blocks; +use Jetpack; +use Jetpack_Gutenberg; +use Jetpack_Options; + +const FEATURE_NAME = 'mailchimp'; +const BLOCK_NAME = 'jetpack/' . FEATURE_NAME; + +/** + * 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() { + if ( + ( defined( 'IS_WPCOM' ) && IS_WPCOM ) + || Jetpack::is_connection_ready() + ) { + Blocks::jetpack_register_block( + BLOCK_NAME, + array( + 'render_callback' => __NAMESPACE__ . '\load_assets', + ) + ); + } } +add_action( 'init', __NAMESPACE__ . '\register_block' ); /** * Mailchimp block registration/dependency declaration. * - * @param array $attr - Array containing the map block attributes. + * @param array $attr - Array containing the Mailchimp block attributes. + * @param string $content - Mailchimp block content. * * @return string */ -function jetpack_mailchimp_block_load_assets( $attr ) { +function load_assets( $attr, $content ) { - if ( ! jetpack_mailchimp_verify_connection() ) { + if ( ! verify_connection() ) { return null; } - $values = array(); + $values = get_attributes_with_defaults( $attr ); $blog_id = ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ? get_current_blog_id() : Jetpack_Options::get_option( 'id' ); - Jetpack_Gutenberg::load_assets_as_required( 'mailchimp' ); + Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME ); + $classes = Blocks::classes( FEATURE_NAME, $attr ); + $amp_form_action = sprintf( 'https://public-api.wordpress.com/rest/v1.1/sites/%s/email_follow/amp/subscribe/', $blog_id ); + $is_amp_request = Blocks::is_amp_request(); + + ob_start(); + ?> + +
+
+ action-xhr="" + method="post" + id="mailchimp_form" + target="_top" + on="submit-success:AMP.setState( { mailing_list_status: 'subscribed', mailing_list_email: event.response.email } )" + + > +

+ +

+ + + + + + + + + + + +
+ +
+
+ +
+
+ +
+ + + +
+ + +
+ +
+
+ +
+ + + +
+ esc_html__( 'Enter your email', 'jetpack' ), - 'submitButtonText' => esc_html__( 'Join my email list', 'jetpack' ), 'consentText' => esc_html__( 'By clicking submit, you agree to share your email address with the site owner and Mailchimp to receive marketing, updates, and other emails from the site owner. Use the unsubscribe link in those emails to opt out at any time.', 'jetpack' ), 'processingLabel' => esc_html__( 'Processing…', 'jetpack' ), 'successLabel' => esc_html__( 'Success! You\'re on the list.', 'jetpack' ), @@ -45,15 +191,43 @@ function jetpack_mailchimp_block_load_assets( $attr ) { 'signupFieldTag' => '', 'signupFieldValue' => '', ); + foreach ( $defaults as $id => $default ) { $values[ $id ] = isset( $attr[ $id ] ) ? $attr[ $id ] : $default; } - $values['submitButtonText'] = empty( $values['submitButtonText'] ) ? $defaults['submitButtonText'] : $values['submitButtonText']; + return $values; +} - $classes = Jetpack_Gutenberg::block_classes( 'mailchimp', $attr ); +/** + * Renders the Mailchimp block button using inner block content if available + * otherwise generating the HTML button from deprecated attributes. + * + * @param array $attr Attributes for the Mailchimp block. + * @param string $content Mailchimp block content. + * + * @return string + */ +function render_button( $attr, $content ) { + if ( ! empty( $content ) ) { + $block_id = wp_unique_id( 'mailchimp-button-block-' ); + return str_replace( 'mailchimp-widget-id', $block_id, $content ); + } + + return render_deprecated_button( $attr ); +} +/** + * Renders HTML button from deprecated Mailchimp block attributes. + * + * @param array $attr Mailchimp block attributes. + * @return string + */ +function render_deprecated_button( $attr ) { + $default = esc_html__( 'Join my email list', 'jetpack' ); + $text = empty( $attr['submitButtonText'] ) ? $default : $attr['submitButtonText']; $button_styles = array(); + if ( ! empty( $attr['customBackgroundButtonColor'] ) ) { array_push( $button_styles, @@ -63,6 +237,7 @@ function jetpack_mailchimp_block_load_assets( $attr ) { ) ); } + if ( ! empty( $attr['customTextButtonColor'] ) ) { array_push( $button_styles, @@ -72,127 +247,18 @@ function jetpack_mailchimp_block_load_assets( $attr ) { ) ); } - $button_styles = implode( ';', $button_styles ); - $amp_form_action = sprintf( 'https://public-api.wordpress.com/rest/v1.1/sites/%s/email_follow/amp/subscribe/', $blog_id ); - $is_amp_request = class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request(); - ob_start(); - ?> + $button_styles = implode( ';', $button_styles ); + $button_classes = 'components-button is-button is-primary '; -
-
-
- action-xhr="" - method="post" - id="mailchimp_form" - target="_top" - - on="submit-success:AMP.setState( { mailing_list_status: 'subscribed', mailing_list_email: event.response.email } )" - - - > -

- -

- - - - - - -

- -

- - - - -
- -
-
- -
-
- -
- - - -
- - -
- -
-
- -
- - - -
-
-

', + esc_attr( $button_classes ), + esc_attr( $button_styles ), + wp_kses_post( $text ) + ); } diff --git a/plugins/jetpack/extensions/blocks/map/map.php b/plugins/jetpack/extensions/blocks/map/map.php index d3b881be..3925b487 100644 --- a/plugins/jetpack/extensions/blocks/map/map.php +++ b/plugins/jetpack/extensions/blocks/map/map.php @@ -4,40 +4,58 @@ * * @since 6.8.0 * - * @package Jetpack + * @package automattic/jetpack */ -jetpack_register_block( - 'jetpack/map', - array( - 'render_callback' => 'jetpack_map_block_load_assets', - ) -); +namespace Automattic\Jetpack\Extensions\Map; + +use Automattic\Jetpack\Blocks; +use Automattic\Jetpack\Status\Host; +use Automattic\Jetpack\Tracking; +use Jetpack; +use Jetpack_Gutenberg; +use Jetpack_Mapbox_Helper; + +const FEATURE_NAME = 'map'; +const BLOCK_NAME = 'jetpack/' . FEATURE_NAME; + +if ( ! class_exists( 'Jetpack_Mapbox_Helper' ) ) { + \jetpack_require_lib( 'class-jetpack-mapbox-helper' ); +} + +/** + * 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( + BLOCK_NAME, + array( + 'render_callback' => __NAMESPACE__ . '\load_assets', + ) + ); +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); /** - * Return the site's own Mapbox API key if set, or the WordPress.com's one otherwise. + * Record a Tracks event every time the Map block is loaded on WordPress.com and Atomic. * - * @return string + * @param string $access_token_source The Mapbox API access token source. */ -function jetpack_get_mapbox_api_key() { - if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { - $endpoint = sprintf( - 'https://public-api.wordpress.com/wpcom/v2/sites/%d/service-api-keys/mapbox', - get_current_blog_id() - ); - } else { - $endpoint = rest_url( 'wpcom/v2/service-api-keys/mapbox' ); +function wpcom_load_event( $access_token_source ) { + if ( 'wpcom' !== $access_token_source ) { + return; } - $response = wp_remote_get( esc_url_raw( $endpoint ) ); - $response_code = wp_remote_retrieve_response_code( $response ); - - if ( 200 === $response_code ) { - $response_body = json_decode( wp_remote_retrieve_body( $response ) ); - return $response_body->service_api_key; + $event_name = 'map_block_mapbox_wpcom_key_load'; + if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { + jetpack_require_lib( 'tracks/client' ); + tracks_record_event( wp_get_current_user(), $event_name ); + } elseif ( ( new Host() )->is_woa_site() && Jetpack::is_connection_ready() ) { + $tracking = new Tracking(); + $tracking->record_user_event( $event_name ); } - - return Jetpack_Options::get_option( 'mapbox_api_key' ); } /** @@ -48,10 +66,12 @@ function jetpack_get_mapbox_api_key() { * * @return string */ -function jetpack_map_block_load_assets( $attr, $content ) { - $api_key = jetpack_get_mapbox_api_key(); +function load_assets( $attr, $content ) { + $access_token = Jetpack_Mapbox_Helper::get_access_token(); - if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) { + wpcom_load_event( $access_token['source'] ); + + if ( Blocks::is_amp_request() ) { static $map_block_counter = array(); $id = get_the_ID(); @@ -79,15 +99,15 @@ function jetpack_map_block_load_assets( $attr, $content ) { ); } - Jetpack_Gutenberg::load_assets_as_required( 'map' ); + Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME ); - return preg_replace( '/
post_content ); @@ -113,7 +133,7 @@ function jetpack_map_block_render_single_block_page() { @$post_html->loadHTML( $content ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged libxml_use_internal_errors( false ); - $xpath = new DOMXPath( $post_html ); + $xpath = new \DOMXPath( $post_html ); $container = $xpath->query( '//div[ contains( @class, "wp-block-jetpack-map" ) ]' )->item( $map_block_counter - 1 ); /* Check that we have a block matching the counter position */ @@ -126,7 +146,7 @@ function jetpack_map_block_render_single_block_page() { add_filter( 'jetpack_is_amp_request', '__return_false' ); - Jetpack_Gutenberg::load_assets_as_required( 'map' ); + Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME ); wp_scripts()->do_items(); wp_styles()->do_items(); @@ -136,14 +156,58 @@ function jetpack_map_block_render_single_block_page() { /* Put together a new complete document containing only the requested block markup and the scripts/styles needed to render it */ $block_markup = $post_html->saveHTML( $container ); - $api_key = jetpack_get_mapbox_api_key(); + $access_token = Jetpack_Mapbox_Helper::get_access_token(); $page_html = sprintf( '%s%s', $head_content, - preg_replace( '/(?<= $points, + 'zoom' => 8, + 'mapCenter' => array( + 'lng' => $points[0]['coordinates']['longitude'], + 'lat' => $points[0]['coordinates']['latitude'], + ), + ); + + $list_items = array_map( + function ( $point ) { + $link = add_query_arg( + array( + 'api' => 1, + 'query' => $point['coordinates']['latitude'] . ',' . $point['coordinates']['longitude'], + ), + 'https://www.google.com/maps/search/' + ); + return sprintf( '
  • %s
  • ', esc_url( $link ), $point['title'] ); + }, + $points + ); -add_action( 'wp', 'jetpack_map_block_render_single_block_page' ); + $map_block = '' . PHP_EOL; + $map_block .= sprintf( + '
    ', + esc_html( wp_json_encode( $map_block_data['points'] ) ), + (int) $map_block_data['zoom'], + esc_html( wp_json_encode( $map_block_data['mapCenter'] ) ) + ); + $map_block .= '
      ' . implode( "\n", $list_items ) . '
    '; + $map_block .= '
    ' . PHP_EOL; + $map_block .= ''; + + return $map_block; +} diff --git a/plugins/jetpack/extensions/blocks/markdown/markdown.php b/plugins/jetpack/extensions/blocks/markdown/markdown.php index 27978dcb..13a81fc8 100644 --- a/plugins/jetpack/extensions/blocks/markdown/markdown.php +++ b/plugins/jetpack/extensions/blocks/markdown/markdown.php @@ -4,7 +4,22 @@ * * @since 6.8.0 * - * @package Jetpack + * @package automattic/jetpack */ -jetpack_register_block( 'jetpack/markdown' ); +namespace Automattic\Jetpack\Extensions\Markdown; + +use Automattic\Jetpack\Blocks; + +const FEATURE_NAME = 'markdown'; +const BLOCK_NAME = 'jetpack/' . FEATURE_NAME; + +/** + * 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( BLOCK_NAME ); +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); diff --git a/plugins/jetpack/extensions/blocks/opentable/opentable.php b/plugins/jetpack/extensions/blocks/opentable/opentable.php index 1ea9cef1..e92f9308 100644 --- a/plugins/jetpack/extensions/blocks/opentable/opentable.php +++ b/plugins/jetpack/extensions/blocks/opentable/opentable.php @@ -4,71 +4,32 @@ * * @since 8.2 * - * @package Jetpack + * @package automattic/jetpack */ -namespace Jetpack\OpenTable_Block; +namespace Automattic\Jetpack\Extensions\OpenTable; + +use Automattic\Jetpack\Blocks; +use Jetpack_Gutenberg; const FEATURE_NAME = 'opentable'; const BLOCK_NAME = 'jetpack/' . FEATURE_NAME; -/** - * Check if the block should be available on the site. - * - * @return bool - */ -function is_available() { - if ( - defined( 'IS_WPCOM' ) - && IS_WPCOM - && function_exists( 'has_any_blog_stickers' ) - ) { - if ( has_any_blog_stickers( - array( 'premium-plan', 'business-plan', 'ecommerce-plan' ), - get_current_blog_id() - ) ) { - return true; - } - return false; - } - - return true; -} - /** * 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() { - if ( is_available() ) { - jetpack_register_block( - BLOCK_NAME, - array( 'render_callback' => 'Jetpack\OpenTable_Block\load_assets' ) - ); - } -} -add_action( 'init', 'Jetpack\OpenTable_Block\register_block' ); - -/** - * Set the availability of the block as the editor - * is loaded. - */ -function set_availability() { - if ( is_available() ) { - \Jetpack_Gutenberg::set_extension_available( BLOCK_NAME ); - } else { - \Jetpack_Gutenberg::set_extension_unavailable( - BLOCK_NAME, - 'missing_plan', - array( - 'required_feature' => 'opentable', - 'required_plan' => 'premium-plan', - ) - ); - } + Blocks::jetpack_register_block( + BLOCK_NAME, + array( + 'render_callback' => __NAMESPACE__ . '\load_assets', + 'plan_check' => true, + ) + ); } -add_action( 'jetpack_register_gutenberg_extensions', 'Jetpack\OpenTable_Block\set_availability' ); +add_action( 'init', __NAMESPACE__ . '\register_block' ); /** * Adds an inline script which updates the block editor settings to @@ -79,7 +40,7 @@ add_action( 'jetpack_register_gutenberg_extensions', 'Jetpack\OpenTable_Block\se function add_language_setting() { wp_add_inline_script( 'jetpack-blocks-editor', sprintf( "wp.data.dispatch( 'core/block-editor' ).updateSettings( { siteLocale: '%s' } )", str_replace( '_', '-', get_locale() ) ), 'before' ); } -add_action( 'enqueue_block_assets', 'Jetpack\OpenTable_Block\add_language_setting' ); +add_action( 'enqueue_block_assets', __NAMESPACE__ . '\add_language_setting' ); /** * OpenTable block registration/dependency declaration. @@ -89,23 +50,69 @@ add_action( 'enqueue_block_assets', 'Jetpack\OpenTable_Block\add_language_settin * @return string */ function load_assets( $attributes ) { - \Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME ); - $classes = array( sprintf( 'wp-block-jetpack-%s-theme-%s', FEATURE_NAME, get_attribute( $attributes, 'style' ) ) ); - if ( count( $attributes['rid'] ) > 1 ) { + Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME ); + + $classes = array(); + $class_name = get_attribute( $attributes, 'className' ); + $style = get_attribute( $attributes, 'style' ); + + if ( 'wide' === $style && jetpack_is_mobile() ) { + $attributes = array_merge( $attributes, array( 'style' => 'standard' ) ); + $classes[] = 'is-style-mobile'; + } + + // Handles case of deprecated version using theme instead of block styles. + if ( ! $class_name || strpos( $class_name, 'is-style-' ) === false ) { + $classes[] = sprintf( 'is-style-%s', $style ); + } + + if ( array_key_exists( 'rid', $attributes ) && is_array( $attributes['rid'] ) && count( $attributes['rid'] ) > 1 ) { $classes[] = 'is-multi'; } - $classes = \Jetpack_Gutenberg::block_classes( - FEATURE_NAME, - $attributes, - $classes - ); + if ( array_key_exists( 'negativeMargin', $attributes ) && $attributes['negativeMargin'] ) { + $classes[] = 'has-no-margin'; + } + $classes = Blocks::classes( FEATURE_NAME, $attributes, $classes ); $content = '
    '; - // The OpenTable script uses multiple `rid` paramters, - // so we can't use WordPress to output it, as WordPress attempts to validate it and removes them. - // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript - $content .= ''; + + $script_url = build_embed_url( $attributes ); + + if ( Blocks::is_amp_request() ) { + // Extract params from URL since it had jetpack_opentable_block_url filters applied. + $url_query = \wp_parse_url( $script_url, PHP_URL_QUERY ) . '&overlay=false&disablega=false'; + + $src = "https://www.opentable.com/widget/reservation/canvas?$url_query"; + + $params = array(); + wp_parse_str( $url_query, $params ); + + // Note an iframe is similarly constructed in the block edit function. + $content .= sprintf( + '%s', + esc_url( $src ), + sprintf( + '%s', + esc_url( + add_query_arg( + array( + 'rid' => $params['rid'], + ), + 'https://www.opentable.com/restref/client/' + ) + ), + esc_html__( 'Make a reservation', 'jetpack' ) + ) + ); + } else { + // The OpenTable script uses multiple `rid` paramters, + // so we can't use WordPress to output it, as WordPress attempts to validate it and removes them. + // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript + $content .= ''; + } + $content .= '
    '; + return $content; } diff --git a/plugins/jetpack/extensions/blocks/pinterest/pinterest.php b/plugins/jetpack/extensions/blocks/pinterest/pinterest.php index a3b6e616..b12ba8b0 100644 --- a/plugins/jetpack/extensions/blocks/pinterest/pinterest.php +++ b/plugins/jetpack/extensions/blocks/pinterest/pinterest.php @@ -4,13 +4,210 @@ * * @since 8.0.0 * - * @package Jetpack + * @package automattic/jetpack */ -jetpack_register_block( - 'jetpack/pinterest', - array( 'render_callback' => 'jetpack_pinterest_block_load_assets' ) -); +namespace Automattic\Jetpack\Extensions\Pinterest; + +use Automattic\Jetpack\Blocks; +use WP_Error; + +const FEATURE_NAME = 'pinterest'; +const BLOCK_NAME = 'jetpack/' . FEATURE_NAME; +const URL_PATTERN = '#^https?://(?:www\.)?(?:[a-z]{2}\.)?pinterest\.[a-z.]+/pin/(?P[^/]+)/?#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. + * @returns {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 ( substr( $path, 0, 5 ) === '/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( + BLOCK_NAME, + 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 . + $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 . + */ +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 while also supplying `noscript > img` as fallback when JS is disabled. + '%s', + esc_url( $attr['url'] ), + esc_url( $image['url'] ), + esc_attr( $title ) + ); + + $amp_padding = 5; // See . + $amp_fixed_width = 237; // See . + $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 . + $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 . + $pin_info_height += ceil( strlen( $description ) / 30 ) * $desc_line_height; + } + } + + if ( ! empty( $info['repin_count'] ) ) { + $pin_stats_height = 16; // See . + $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( + '%6$s', + 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( + '%4$s', + esc_url( $attr['url'] ), + 450, // Fallback width. + 750, // Fallback height. + sprintf( + '%s', + esc_url( $attr['url'] ), + esc_html( $attr['url'] ) + ) + ); + } + + return sprintf( + '
    %s
    ', + $amp_pinterest + ); +} /** * Pinterest block registration/dependency declaration. @@ -20,7 +217,30 @@ jetpack_register_block( * * @return string */ -function jetpack_pinterest_block_load_assets( $attr, $content ) { - wp_enqueue_script( 'pinterest-pinit', 'https://assets.pinterest.com/js/pinit.js', array(), JETPACK__VERSION, true ); - return $content; +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( + ' +
    + +
    + ', + esc_attr( Blocks::classes( FEATURE_NAME, $attr ) ), + esc_attr( $type ), + esc_url( $url ) + ); + } } diff --git a/plugins/jetpack/extensions/blocks/podcast-player/podcast-player.php b/plugins/jetpack/extensions/blocks/podcast-player/podcast-player.php new file mode 100644 index 00000000..9b3af865 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/podcast-player/podcast-player.php @@ -0,0 +1,312 @@ + array( + 'url' => array( + 'type' => 'string', + ), + 'itemsToShow' => array( + 'type' => 'integer', + 'default' => 5, + ), + 'showCoverArt' => array( + 'type' => 'boolean', + 'default' => true, + ), + 'showEpisodeTitle' => array( + 'type' => 'boolean', + 'default' => true, + ), + 'showEpisodeDescription' => array( + 'type' => 'boolean', + 'default' => true, + ), + ), + 'render_callback' => __NAMESPACE__ . '\render_block', + // Since Gutenberg #31873. + 'style' => 'wp-mediaelement', + + ) + ); +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); + +/** + * Returns the error message wrapped in HTML if current user + * has the capability to edit the post. Public visitors will + * never see errors. + * + * @param string $message The error message to display. + * @return string + */ +function render_error( $message ) { + // Suppress errors for users unable to address them. + if ( ! current_user_can( 'edit_posts' ) ) { + return ''; + } + return '

    ' . esc_html( $message ) . '

    '; +} + +/** + * Podcast Player block registration/dependency declaration. + * + * @param array $attributes Array containing the Podcast Player block attributes. + * @param string $content Fallback content - a direct link to RSS, as rendered by save.js. + * @return string + */ +function render_block( $attributes, $content ) { + // Don't render an interactive version of the block outside the frontend context. + if ( ! jetpack_is_frontend() ) { + return $content; + } + + // Test for empty URLS. + if ( empty( $attributes['url'] ) ) { + return render_error( __( 'No Podcast URL provided. Please enter a valid Podcast RSS feed URL.', 'jetpack' ) ); + } + + // Test for invalid URLs. + if ( ! wp_http_validate_url( $attributes['url'] ) ) { + return render_error( __( 'Your podcast URL is invalid and couldn\'t be embedded. Please double check your URL.', 'jetpack' ) ); + } + + if ( isset( $attributes['selectedEpisodes'] ) && count( $attributes['selectedEpisodes'] ) ) { + $guids = array_map( + function ( $episode ) { + return $episode['guid']; + }, + $attributes['selectedEpisodes'] + ); + $player_args = array( 'guids' => $guids ); + } else { + $player_args = array(); + } + + // Sanitize the URL. + $attributes['url'] = esc_url_raw( $attributes['url'] ); + $player_data = ( new Jetpack_Podcast_Helper( $attributes['url'] ) )->get_player_data( $player_args ); + + if ( is_wp_error( $player_data ) ) { + return render_error( $player_data->get_error_message() ); + } + + return render_player( $player_data, $attributes ); +} + +/** + * Renders the HTML for the Podcast player and tracklist. + * + * @param array $player_data The player data details. + * @param array $attributes Array containing the Podcast Player block attributes. + * @return string The HTML for the podcast player. + */ +function render_player( $player_data, $attributes ) { + // If there are no tracks (it is possible) then display appropriate user facing error message. + if ( empty( $player_data['tracks'] ) ) { + return render_error( __( 'No tracks available to play.', 'jetpack' ) ); + } + + // Only use the amount of tracks requested. + $player_data['tracks'] = array_slice( + $player_data['tracks'], + 0, + absint( $attributes['itemsToShow'] ) + ); + + // Generate a unique id for the block instance. + $instance_id = wp_unique_id( 'jetpack-podcast-player-block-' . get_the_ID() . '-' ); + $player_data['playerId'] = $instance_id; + + // Generate object to be used as props for PodcastPlayer. + $player_props = array_merge( + // Add all attributes. + array( 'attributes' => $attributes ), + // Add all player data. + $player_data + ); + + $primary_colors = get_colors( 'primary', $attributes, 'color' ); + $secondary_colors = get_colors( 'secondary', $attributes, 'color' ); + $background_colors = get_colors( 'background', $attributes, 'background-color' ); + + $player_classes_name = trim( "{$secondary_colors['class']} {$background_colors['class']}" ); + $player_inline_style = trim( "{$secondary_colors['style']} ${background_colors['style']}" ); + $player_inline_style .= get_css_vars( $attributes ); + + $block_classname = Blocks::classes( FEATURE_NAME, $attributes, array( 'is-default' ) ); + $is_amp = Blocks::is_amp_request(); + + ob_start(); + ?> +
    +
    + $primary_colors, + 'player_id' => $player_data['playerId'], + ) + ) + ); + ?> + 1 ) : ?> +
      + $attachment ) : ?> + 0 === $track_index, + 'attachment' => $attachment, + 'primary_colors' => $primary_colors, + 'secondary_colors' => $secondary_colors, + ) + ); + ?> + +
    + +
    + + + +
    + array( 'class' => 'has-secondary', 'style' => 'border-color: #333' ) + * + * @param string $name Color attribute name, for instance `primary`, `secondary`, ... + * @param array $attrs Block attributes. + * @param string $property Color CSS property, fo instance `color`, `background-color`, ... + * @return array Colors array. + */ +function get_colors( $name, $attrs, $property ) { + $attr_color = "{$name}Color"; + $attr_custom = 'custom' . ucfirst( $attr_color ); + + $color = isset( $attrs[ $attr_color ] ) ? $attrs[ $attr_color ] : null; + $custom_color = isset( $attrs[ $attr_custom ] ) ? $attrs[ $attr_custom ] : null; + + $colors = array( + 'class' => '', + 'style' => '', + ); + + if ( $color || $custom_color ) { + $colors['class'] .= "has-{$name}"; + + if ( $color ) { + $colors['class'] .= " has-{$color}-{$property}"; + } elseif ( $custom_color ) { + $colors['style'] .= "{$property}: {$custom_color};"; + } + } + + return $colors; +} + +/** + * It generates a string with CSS variables according to the + * block colors, prefixing each one with `--jetpack-podcast-player'. + * + * @param array $attrs Podcast Block attributes object. + * @return string CSS variables depending on block colors. + */ +function get_css_vars( $attrs ) { + $colors_name = array( 'primary', 'secondary', 'background' ); + + $inline_style = ''; + foreach ( $colors_name as $color ) { + $hex_color = 'hex' . ucfirst( $color ) . 'Color'; + if ( ! empty( $attrs[ $hex_color ] ) ) { + $inline_style .= " --jetpack-podcast-player-{$color}: {$attrs[ $hex_color ]};"; + } + } + return $inline_style; +} + +/** + * Render the given template in server-side. + * Important note: + * The $template_props array will be extracted. + * This means it will create a var for each array item. + * Keep it mind when using this param to pass + * properties to the template. + * + * @param string $name Template name, available in `./templates` folder. + * @param array $template_props Template properties. Optional. + * @param bool $print Render template. True as default. + * @return false|string HTML markup or false. + */ +function render( $name, $template_props = array(), $print = true ) { + if ( ! strpos( $name, '.php' ) ) { + $name = $name . '.php'; + } + + $template_path = __DIR__ . '/templates/' . $name; + + if ( ! file_exists( $template_path ) ) { + return ''; + } + + if ( $print ) { + include $template_path; + } else { + ob_start(); + include $template_path; + $markup = ob_get_contents(); + ob_end_clean(); + + return $markup; + } +} diff --git a/plugins/jetpack/extensions/blocks/podcast-player/templates/playlist-track.php b/plugins/jetpack/extensions/blocks/podcast-player/templates/playlist-track.php new file mode 100644 index 00000000..0789816e --- /dev/null +++ b/plugins/jetpack/extensions/blocks/podcast-player/templates/playlist-track.php @@ -0,0 +1,43 @@ + + +
  • + + > + + + + +
  • diff --git a/plugins/jetpack/extensions/blocks/podcast-player/templates/podcast-header-title.php b/plugins/jetpack/extensions/blocks/podcast-player/templates/podcast-header-title.php new file mode 100644 index 00000000..763aa83a --- /dev/null +++ b/plugins/jetpack/extensions/blocks/podcast-player/templates/podcast-header-title.php @@ -0,0 +1,59 @@ + + +

    + + > + + + + + + + + + + - + + $template_props['title'], + 'link' => $template_props['link'], + ) + ); + ?> + +

    diff --git a/plugins/jetpack/extensions/blocks/podcast-player/templates/podcast-header.php b/plugins/jetpack/extensions/blocks/podcast-player/templates/podcast-header.php new file mode 100644 index 00000000..b6167df2 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/podcast-player/templates/podcast-header.php @@ -0,0 +1,67 @@ + + +
    +
    + +
    + +
    + + + $template_props['player_id'], + 'title' => $template_props['title'], + 'link' => $template_props['link'], + 'track' => $track, + 'primary_colors' => $template_props['primary_colors'], + ) + ); + } + ?> +
    + + +
    + +
    + + +
    +
    +
    +
    diff --git a/plugins/jetpack/extensions/blocks/podcast-player/templates/podcast-title.php b/plugins/jetpack/extensions/blocks/podcast-player/templates/podcast-title.php new file mode 100644 index 00000000..ce5b59ca --- /dev/null +++ b/plugins/jetpack/extensions/blocks/podcast-player/templates/podcast-title.php @@ -0,0 +1,36 @@ + + + + + + + + diff --git a/plugins/jetpack/extensions/blocks/premium-content/_inc/access-check.php b/plugins/jetpack/extensions/blocks/premium-content/_inc/access-check.php new file mode 100644 index 00000000..77ab96a1 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/premium-content/_inc/access-check.php @@ -0,0 +1,107 @@ +ID && current_user_can( 'edit_post', get_the_ID() ); +} + +/** + * Determines if the current user can view the protected content of the given block. + * + * @param array $attributes Block attributes. + * @param object $block Block to check. + * + * @return bool Whether the use can view the content. + */ +function current_visitor_can_access( $attributes, $block ) { + /** + * If the current WordPress install has as signed in user + * they can see the content. + */ + if ( current_user_can_edit() ) { + return true; + } + + $selected_plan_id = null; + + if ( isset( $attributes['selectedPlanId'] ) ) { + $selected_plan_id = (int) $attributes['selectedPlanId']; + } + + if ( isset( $block ) && isset( $block->context['premium-content/planId'] ) ) { + $selected_plan_id = (int) $block->context['premium-content/planId']; + } + + if ( empty( $selected_plan_id ) ) { + return false; + } + + $paywall = subscription_service(); + $can_view = $paywall->visitor_can_view_content( array( $selected_plan_id ) ); + + if ( $can_view ) { + /** + * Fires when a visitor can view protected content on a site. + * + * @since 9.4.0 + */ + do_action( 'jetpack_earn_remove_cache_headers' ); + } + + return $can_view; +} diff --git a/plugins/jetpack/extensions/blocks/premium-content/_inc/legacy-buttons.php b/plugins/jetpack/extensions/blocks/premium-content/_inc/legacy-buttons.php new file mode 100644 index 00000000..1124160f --- /dev/null +++ b/plugins/jetpack/extensions/blocks/premium-content/_inc/legacy-buttons.php @@ -0,0 +1,62 @@ +%4$s
    ', + subscription_service()->access_url(), + empty( $attributes['buttonClasses'] ) ? 'wp-block-button__link' : esc_attr( $attributes['buttonClasses'] ), + esc_attr( $button_styles ), + empty( $attributes['loginButtonText'] ) ? __( 'Log In', 'jetpack' ) : $attributes['loginButtonText'] + ); + + $subscribe_button = \Jetpack_Memberships::get_instance()->render_button( + array( + 'planId' => empty( $block->context['premium-content/planId'] ) ? 0 : $block->context['premium-content/planId'], + 'submitButtonClasses' => empty( $attributes['buttonClasses'] ) ? 'wp-block-button__link' : esc_attr( $attributes['buttonClasses'] ), + 'customTextButtonColor' => empty( $attributes['customTextButtonColor'] ) ? '' : esc_attr( $attributes['customTextButtonColor'] ), + 'customBackgroundButtonColor' => empty( $attributes['customBackgroundButtonColor'] ) ? '' : esc_attr( $attributes['customBackgroundButtonColor'] ), + 'submitButtonText' => empty( $attributes['subscribeButtonText'] ) ? __( 'Subscribe', 'jetpack' ) : esc_attr( $attributes['subscribeButtonText'] ), + ), + $content, + $block + ); + + return "
    {$subscribe_button}{$login_button}
    "; +} diff --git a/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-jetpack-token-subscription-service.php b/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-jetpack-token-subscription-service.php new file mode 100644 index 00000000..2dc8cfb3 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-jetpack-token-subscription-service.php @@ -0,0 +1,50 @@ +get_access_token(); + if ( ! isset( $token->secret ) ) { + return false; + } + return $token->secret; + } +} diff --git a/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-jwt.php b/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-jwt.php new file mode 100644 index 00000000..f2d1f1d8 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-jwt.php @@ -0,0 +1,441 @@ + + * @author Anant Narayanan + * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD + * @link https://github.com/firebase/php-jwt + */ +class JWT { + /** + * When checking nbf, iat or expiration times, + * we want to provide some extra leeway time to + * account for clock skew. + * + * @var int $leeway The leeway value. + */ + public static $leeway = 0; + + /** + * Allow the current timestamp to be specified. + * Useful for fixing a value within unit testing. + * + * Will default to PHP time() value if null. + * + * @var string $timestamp The timestamp. + */ + public static $timestamp = null; + + /** + * Supported algorithms. + * + * @var array $supported_algs Supported algorithms. + */ + public static $supported_algs = array( + 'HS256' => array( 'hash_hmac', 'SHA256' ), + 'HS512' => array( 'hash_hmac', 'SHA512' ), + 'HS384' => array( 'hash_hmac', 'SHA384' ), + 'RS256' => array( 'openssl', 'SHA256' ), + 'RS384' => array( 'openssl', 'SHA384' ), + 'RS512' => array( 'openssl', 'SHA512' ), + ); + + /** + * Decodes a JWT string into a PHP object. + * + * @param string $jwt The JWT. + * @param string|array $key The key, or map of keys. + * If the algorithm used is asymmetric, this is the public key. + * @param array $allowed_algs List of supported verification algorithms. + * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'. + * + * @return object The JWT's payload as a PHP object + * + * @throws UnexpectedValueException Provided JWT was invalid. + * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed. + * @throws InvalidArgumentException Provided JWT is trying to be used before it's eligible as defined by 'nbf'. + * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat'. + * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim. + * + * @uses json_decode + * @uses urlsafe_b64_decode + */ + public static function decode( $jwt, $key, array $allowed_algs = array() ) { + $timestamp = is_null( static::$timestamp ) ? time() : static::$timestamp; + + if ( empty( $key ) ) { + throw new InvalidArgumentException( 'Key may not be empty' ); + } + + $tks = explode( '.', $jwt ); + if ( count( $tks ) !== 3 ) { + throw new UnexpectedValueException( 'Wrong number of segments' ); + } + + list( $headb64, $bodyb64, $cryptob64 ) = $tks; + + $header = static::json_decode( static::urlsafe_b64_decode( $headb64 ) ); + if ( null === $header ) { + throw new UnexpectedValueException( 'Invalid header encoding' ); + } + + $payload = static::json_decode( static::urlsafe_b64_decode( $bodyb64 ) ); + if ( null === $payload ) { + throw new UnexpectedValueException( 'Invalid claims encoding' ); + } + + $sig = static::urlsafe_b64_decode( $cryptob64 ); + if ( false === $sig ) { + throw new UnexpectedValueException( 'Invalid signature encoding' ); + } + + if ( empty( $header->alg ) ) { + throw new UnexpectedValueException( 'Empty algorithm' ); + } + + if ( empty( static::$supported_algs[ $header->alg ] ) ) { + throw new UnexpectedValueException( 'Algorithm not supported' ); + } + + if ( ! in_array( $header->alg, $allowed_algs, true ) ) { + throw new UnexpectedValueException( 'Algorithm not allowed' ); + } + + if ( is_array( $key ) || $key instanceof \ArrayAccess ) { + if ( isset( $header->kid ) ) { + if ( ! isset( $key[ $header->kid ] ) ) { + throw new UnexpectedValueException( '"kid" invalid, unable to lookup correct key' ); + } + $key = $key[ $header->kid ]; + } else { + throw new UnexpectedValueException( '"kid" empty, unable to lookup correct key' ); + } + } + + // Check the signature. + if ( ! static::verify( "$headb64.$bodyb64", $sig, $key, $header->alg ) ) { + throw new SignatureInvalidException( 'Signature verification failed' ); + } + + // Check if the nbf if it is defined. This is the time that the + // token can actually be used. If it's not yet that time, abort. + if ( isset( $payload->nbf ) && $payload->nbf > ( $timestamp + static::$leeway ) ) { + throw new BeforeValidException( + 'Cannot handle token prior to ' . gmdate( DateTime::ISO8601, $payload->nbf ) + ); + } + + // Check that this token has been created before 'now'. This prevents + // using tokens that have been created for later use (and haven't + // correctly used the nbf claim). + if ( isset( $payload->iat ) && $payload->iat > ( $timestamp + static::$leeway ) ) { + throw new BeforeValidException( + 'Cannot handle token prior to ' . gmdate( DateTime::ISO8601, $payload->iat ) + ); + } + + // Check if this token has expired. + if ( isset( $payload->exp ) && ( $timestamp - static::$leeway ) >= $payload->exp ) { + throw new ExpiredException( 'Expired token' ); + } + + return $payload; + } + + /** + * Converts and signs a PHP object or array into a JWT string. + * + * @param object|array $payload PHP object or array. + * @param string $key The secret key. + * If the algorithm used is asymmetric, this is the private key. + * @param string $alg The signing algorithm. + * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'. + * @param mixed $key_id The key ID. + * @param array $head An array with header elements to attach. + * + * @return string A signed JWT + * + * @uses json_encode + * @uses urlsafe_b64_decode + */ + public static function encode( $payload, $key, $alg = 'HS256', $key_id = null, $head = null ) { + $header = array( + 'typ' => 'JWT', + 'alg' => $alg, + ); + + if ( null !== $key_id ) { + $header['kid'] = $key_id; + } + + if ( isset( $head ) && is_array( $head ) ) { + $header = array_merge( $head, $header ); + } + + $segments = array(); + $segments[] = static::urlsafe_b64_decode( static::json_encode( $header ) ); + $segments[] = static::urlsafe_b64_decode( static::json_encode( $payload ) ); + $signing_input = implode( '.', $segments ); + + $signature = static::sign( $signing_input, $key, $alg ); + $segments[] = static::urlsafe_b64_decode( $signature ); + + return implode( '.', $segments ); + } + + /** + * Sign a string with a given key and algorithm. + * + * @param string $msg The message to sign. + * @param string|resource $key The secret key. + * @param string $alg The signing algorithm. + * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'. + * + * @return string An encrypted message + * + * @throws DomainException Unsupported algorithm was specified. + */ + public static function sign( $msg, $key, $alg = 'HS256' ) { + if ( empty( static::$supported_algs[ $alg ] ) ) { + throw new DomainException( 'Algorithm not supported' ); + } + list($function, $algorithm) = static::$supported_algs[ $alg ]; + switch ( $function ) { + case 'hash_hmac': + return hash_hmac( $algorithm, $msg, $key, true ); + case 'openssl': + $signature = ''; + $success = openssl_sign( $msg, $signature, $key, $algorithm ); + if ( ! $success ) { + throw new DomainException( 'OpenSSL unable to sign data' ); + } else { + return $signature; + } + } + } + + /** + * Verify a signature with the message, key and method. Not all methods + * are symmetric, so we must have a separate verify and sign method. + * + * @param string $msg The original message (header and body). + * @param string $signature The original signature. + * @param string|resource $key For HS*, a string key works. for RS*, must be a resource of an openssl public key. + * @param string $alg The algorithm. + * + * @return bool + * + * @throws DomainException Invalid Algorithm or OpenSSL failure. + */ + private static function verify( $msg, $signature, $key, $alg ) { + if ( empty( static::$supported_algs[ $alg ] ) ) { + throw new DomainException( 'Algorithm not supported' ); + } + + list($function, $algorithm) = static::$supported_algs[ $alg ]; + switch ( $function ) { + case 'openssl': + $success = openssl_verify( $msg, $signature, $key, $algorithm ); + + if ( 1 === $success ) { + return true; + } elseif ( 0 === $success ) { + return false; + } + + // returns 1 on success, 0 on failure, -1 on error. + throw new DomainException( + 'OpenSSL error: ' . openssl_error_string() + ); + case 'hash_hmac': + default: + $hash = hash_hmac( $algorithm, $msg, $key, true ); + + if ( function_exists( 'hash_equals' ) ) { + return hash_equals( $signature, $hash ); + } + + $len = min( static::safe_strlen( $signature ), static::safe_strlen( $hash ) ); + + $status = 0; + + for ( $i = 0; $i < $len; $i++ ) { + $status |= ( ord( $signature[ $i ] ) ^ ord( $hash[ $i ] ) ); + } + + $status |= ( static::safe_strlen( $signature ) ^ static::safe_strlen( $hash ) ); + + return ( 0 === $status ); + } + } + + /** + * Decode a JSON string into a PHP object. + * + * @param string $input JSON string. + * + * @return object Object representation of JSON string + * + * @throws DomainException Provided string was invalid JSON. + */ + public static function json_decode( $input ) { + if ( version_compare( PHP_VERSION, '5.4.0', '>=' ) && ! ( defined( 'JSON_C_VERSION' ) && PHP_INT_SIZE > 4 ) ) { + /** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you + * to specify that large ints (like Steam Transaction IDs) should be treated as + * strings, rather than the PHP default behaviour of converting them to floats. + */ + $obj = json_decode( $input, false, 512, JSON_BIGINT_AS_STRING ); + } else { + /** Not all servers will support that, however, so for older versions we must + * manually detect large ints in the JSON string and quote them (thus converting + *them to strings) before decoding, hence the preg_replace() call. + */ + $max_int_length = strlen( (string) PHP_INT_MAX ) - 1; + $json_without_bigints = preg_replace( '/:\s*(-?\d{' . $max_int_length . ',})/', ': "$1"', $input ); + $obj = json_decode( $json_without_bigints ); + } + + $errno = json_last_error(); + + if ( $errno && function_exists( 'json_last_error' ) ) { + static::handle_json_error( $errno ); + } elseif ( null === $obj && 'null' !== $input ) { + throw new DomainException( 'Null result with non-null input' ); + } + return $obj; + } + + /** + * Encode a PHP object into a JSON string. + * + * @param object|array $input A PHP object or array. + * + * @return string JSON representation of the PHP object or array. + * + * @throws DomainException Provided object could not be encoded to valid JSON. + */ + public static function json_encode( $input ) { + $json = wp_json_encode( $input ); + $errno = json_last_error(); + + if ( $errno && function_exists( 'json_last_error' ) ) { + static::handle_json_error( $errno ); + } elseif ( 'null' === $json && null !== $input ) { + throw new DomainException( 'Null result with non-null input' ); + } + return $json; + } + + /** + * Decode a string with URL-safe Base64. + * + * @param string $input A Base64 encoded string. + * + * @return string A decoded string + */ + public static function urlsafe_b64_decode( $input ) { + $remainder = strlen( $input ) % 4; + if ( $remainder ) { + $padlen = 4 - $remainder; + $input .= str_repeat( '=', $padlen ); + } + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode + return base64_decode( strtr( $input, '-_', '+/' ) ); + } + + /** + * Encode a string with URL-safe Base64. + * + * @param string $input The string you want encoded. + * + * @return string The base64 encode of what you passed in + */ + public static function urlsafe_b64_encode( $input ) { + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + return str_replace( '=', '', strtr( base64_encode( $input ), '+/', '-_' ) ); + } + + /** + * Helper method to create a JSON error. + * + * @param int $errno An error number from json_last_error(). + * @throws DomainException . + * + * @return void + */ + private static function handle_json_error( $errno ) { + $messages = array( + JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', + JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON', + JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', + JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', + JSON_ERROR_UTF8 => 'Malformed UTF-8 characters', + ); + throw new DomainException( + isset( $messages[ $errno ] ) + ? $messages[ $errno ] + : 'Unknown JSON error: ' . $errno + ); + } + + /** + * Get the number of bytes in cryptographic strings. + * + * @param string $str . + * + * @return int + */ + private static function safe_strlen( $str ) { + if ( function_exists( 'mb_strlen' ) ) { + return mb_strlen( $str, '8bit' ); + } + return strlen( $str ); + } +} + +// phpcs:disable +if ( ! class_exists( 'SignatureInvalidException' ) ) { + /** + * SignatureInvalidException + * + * @package Automattic\Jetpack\Extensions\Premium_Content + */ + class SignatureInvalidException extends \UnexpectedValueException { } +} +if ( ! class_exists( 'ExpiredException' ) ) { + /** + * ExpiredException + * + * @package Automattic\Jetpack\Extensions\Premium_Content + */ + class ExpiredException extends \UnexpectedValueException { } +} +if ( ! class_exists( 'BeforeValidException' ) ) { + /** + * BeforeValidException + * + * @package Automattic\Jetpack\Extensions\Premium_Content + */ + class BeforeValidException extends \UnexpectedValueException { } +} +// phpcs:enable diff --git a/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-subscription-service.php b/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-subscription-service.php new file mode 100644 index 00000000..b433182a --- /dev/null +++ b/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-subscription-service.php @@ -0,0 +1,57 @@ +token_from_request(); + if ( null !== $token ) { + $this->set_token_cookie( $token ); + } + } + + /** + * The user is visiting with a subscriber token cookie. + * + * This is theoretically where the cookie JWT signature verification + * thing will happen. + * + * How to obtain one of these (or what exactly it is) is + * still a WIP (see api/auth branch) + * + * @inheritDoc + * + * @param array $valid_plan_ids List of valid plan IDs. + */ + public function visitor_can_view_content( $valid_plan_ids ) { + + // URL token always has a precedence, so it can overwrite the cookie when new data available. + $token = $this->token_from_request(); + if ( $token ) { + $this->set_token_cookie( $token ); + } else { + $token = $this->token_from_cookie(); + } + + $is_valid_token = true; + + if ( empty( $token ) ) { + // no token, no access. + $is_valid_token = false; + } else { + $payload = $this->decode_token( $token ); + if ( empty( $payload ) ) { + $is_valid_token = false; + } + } + + if ( $is_valid_token ) { + $subscriptions = (array) $payload['subscriptions']; + } elseif ( is_user_logged_in() ) { + /* + * If there is no token, but the user is logged in, + * get current subscriptions and determine if the user has + * a valid subscription to match the plan ID. + */ + + /** + * Filter the subscriptions attached to a specific user on a given site. + * + * @since 9.4.0 + * + * @param array $subscriptions Array of subscriptions. + * @param int $user_id The user's ID. + * @param int $site_id ID of the current site. + */ + $subscriptions = apply_filters( + 'earn_get_user_subscriptions_for_site_id', + array(), + wp_get_current_user()->ID, + $this->get_site_id() + ); + + if ( empty( $subscriptions ) ) { + return false; + } + // format the subscriptions so that they can be validated. + $subscriptions = self::abbreviate_subscriptions( $subscriptions ); + } else { + return false; + } + + return $this->validate_subscriptions( $valid_plan_ids, $subscriptions ); + } + + /** + * Decode the given token. + * + * @param string $token Token to decode. + * + * @return array|false + */ + public function decode_token( $token ) { + try { + $key = $this->get_key(); + return $key ? (array) JWT::decode( $token, $key, array( 'HS256' ) ) : false; + } catch ( \Exception $exception ) { + return false; + } + } + + /** + * Get the key for decoding the auth token. + * + * @return string|false + */ + abstract public function get_key(); + + /** + * Get the ID of the current site. + * + * @return int + */ + abstract public function get_site_id(); + + // phpcs:disable + /** + * Get the URL to access the protected content. + * + * @param string $mode Access mode (either "subscribe" or "login"). + */ + public function access_url( $mode = 'subscribe' ) { + global $wp; + $permalink = get_permalink(); + if ( empty( $permalink ) ) { + $permalink = add_query_arg( $wp->query_vars, home_url( $wp->request ) ); + } + + $login_url = $this->get_rest_api_token_url( $this->get_site_id(), $permalink ); + return $login_url; + } + // phpcs:enable + + /** + * Get the token stored in the auth cookie. + * + * @return ?string + */ + private function token_from_cookie() { + if ( isset( $_COOKIE[ self::JWT_AUTH_TOKEN_COOKIE_NAME ] ) ) { + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + return $_COOKIE[ self::JWT_AUTH_TOKEN_COOKIE_NAME ]; + } + } + + /** + * Store the auth cookie. + * + * @param string $token Auth token. + * @return void + */ + private function set_token_cookie( $token ) { + if ( ! empty( $token ) ) { + setcookie( self::JWT_AUTH_TOKEN_COOKIE_NAME, $token, 0, '/' ); + } + } + + /** + * Get the token if present in the current request. + * + * @return ?string + */ + private function token_from_request() { + $token = null; + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( isset( $_GET['token'] ) ) { + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended + if ( preg_match( '/^[a-zA-Z0-9\-_]+?\.[a-zA-Z0-9\-_]+?\.([a-zA-Z0-9\-_]+)?$/', $_GET['token'], $matches ) ) { + // token matches a valid JWT token pattern. + $token = reset( $matches ); + } + } + return $token; + } + + /** + * Return true if any ID/date pairs are valid. Otherwise false. + * + * @param int[] $valid_plan_ids List of valid plan IDs. + * @param array $token_subscriptions : ID must exist in the provided $valid_subscriptions parameter. + * The provided end date needs to be greater than now(). + * + * @return bool + */ + protected function validate_subscriptions( $valid_plan_ids, $token_subscriptions ) { + // Create a list of product_ids to compare against. + $product_ids = array(); + foreach ( $valid_plan_ids as $plan_id ) { + $product_id = (int) get_post_meta( $plan_id, 'jetpack_memberships_product_id', true ); + if ( isset( $product_id ) ) { + $product_ids[] = $product_id; + } + } + + foreach ( $token_subscriptions as $product_id => $token_subscription ) { + if ( in_array( $product_id, $product_ids, true ) ) { + $end = is_int( $token_subscription->end_date ) ? $token_subscription->end_date : strtotime( $token_subscription->end_date ); + if ( $end > time() ) { + return true; + } + } + } + return false; + } + + /** + * Get the URL of the JWT endpoint. + * + * @param int $site_id Site ID. + * @param string $redirect_url URL to redirect after checking the token validity. + * @return string URL of the JWT endpoint. + */ + private function get_rest_api_token_url( $site_id, $redirect_url ) { + return sprintf( '%smemberships/jwt?site_id=%d&redirect_url=%s', self::REST_URL_ORIGIN, $site_id, rawurlencode( $redirect_url ) ); + } + + /** + * Report the subscriptions as an ID => [ 'end_date' => ]. mapping + * + * @param array $subscriptions_from_bd List of subscriptions from BD. + * + * @return array + */ + public static function abbreviate_subscriptions( $subscriptions_from_bd ) { + $subscriptions = array(); + foreach ( $subscriptions_from_bd as $subscription ) { + // We are picking the expiry date that is the most in the future. + if ( + 'active' === $subscription['status'] && ( + ! isset( $subscriptions[ $subscription['product_id'] ] ) || + empty( $subscription['end_date'] ) || // Special condition when subscription has no expiry date - we will default to a year from now for the purposes of the token. + strtotime( $subscription['end_date'] ) > strtotime( (string) $subscriptions[ $subscription['product_id'] ]->end_date ) + ) + ) { + $subscriptions[ $subscription['product_id'] ] = new \stdClass(); + $subscriptions[ $subscription['product_id'] ]->end_date = empty( $subscription['end_date'] ) ? ( time() + 365 * 24 * 3600 ) : $subscription['end_date']; + } + } + return $subscriptions; + } +} diff --git a/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-token-subscription.php b/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-token-subscription.php new file mode 100644 index 00000000..d9c81e8c --- /dev/null +++ b/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-token-subscription.php @@ -0,0 +1,23 @@ +ID, $this->get_site_id() ); + if ( empty( $subscriptions ) ) { + return false; + } + // format the subscriptions so that they can be validated. + $subscriptions = self::abbreviate_subscriptions( $subscriptions ); + return $this->validate_subscriptions( $valid_plan_ids, $subscriptions ); + } + + /** + * Report the subscriptions as an ID => [ 'end_date' => ]. mapping + * + * @param array $subscriptions_from_bd . + * + * @return array + */ + public static function abbreviate_subscriptions( $subscriptions_from_bd ) { + $subscriptions = array(); + foreach ( $subscriptions_from_bd as $subscription ) { + // We are picking the expiry date that is the most in the future. + if ( + 'active' === $subscription['status'] && ( + ! isset( $subscriptions[ $subscription['product_id'] ] ) || + empty( $subscription['end_date'] ) || // Special condition when subscription has no expiry date - we will default to a year from now for the purposes of the token. + strtotime( $subscription['end_date'] ) > strtotime( (string) $subscriptions[ $subscription['product_id'] ]->end_date ) + ) + ) { + $subscriptions[ $subscription['product_id'] ] = new \stdClass(); + $subscriptions[ $subscription['product_id'] ]->end_date = empty( $subscription['end_date'] ) ? ( time() + 365 * 24 * 3600 ) : $subscription['end_date']; + } + } + return $subscriptions; + } +} diff --git a/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-wpcom-token-subscription-service.php b/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-wpcom-token-subscription-service.php new file mode 100644 index 00000000..1bb80b6b --- /dev/null +++ b/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-wpcom-token-subscription-service.php @@ -0,0 +1,46 @@ +initialize(); + } +} +add_action( 'init', 'Automattic\Jetpack\Extensions\Premium_Content\paywall_initialize', 9 ); + +/** + * Gets the service handling the premium content subscriptions. + * + * @return Subscription_Service Service that will handle the premium content subscriptions. + */ +function subscription_service() { + /** + * Filter the Jetpack_Token_Subscription_Service class. + * + * @since 9.4.0 + * + * @param null|Jetpack_Token_Subscription_Service $interface Registered Subscription_Service. + */ + $interface = apply_filters( PAYWALL_FILTER, null ); + if ( ! $interface instanceof Jetpack_Token_Subscription_Service ) { + _doing_it_wrong( __FUNCTION__, 'No Subscription_Service registered for the ' . esc_html( PAYWALL_FILTER ) . ' filter', 'jetpack' ); + } + return $interface; +} + +/** + * Gets the default service handling the premium content. + * + * @param Subscription_Service $service If set, this service will be used by default. + * @return Subscription_Service Service that will handle the premium content. + */ +function default_service( $service ) { + if ( null !== $service ) { + return $service; + } + + if ( WPCOM_Offline_Subscription_Service::available() ) { + return new WPCOM_Offline_Subscription_Service(); + } + + if ( WPCOM_Token_Subscription_Service::available() ) { + return new WPCOM_Token_Subscription_Service(); + } + + if ( Jetpack_Token_Subscription_Service::available() ) { + return new Jetpack_Token_Subscription_Service(); + } + + return new Unconfigured_Subscription_Service(); +} +add_filter( PAYWALL_FILTER, 'Automattic\Jetpack\Extensions\Premium_Content\default_service' ); diff --git a/plugins/jetpack/extensions/blocks/premium-content/buttons/buttons.php b/plugins/jetpack/extensions/blocks/premium-content/buttons/buttons.php new file mode 100644 index 00000000..4cb602ff --- /dev/null +++ b/plugins/jetpack/extensions/blocks/premium-content/buttons/buttons.php @@ -0,0 +1,46 @@ +is_woa_site() ) { + Blocks::jetpack_register_block( + BUTTONS_NAME, + array( + 'render_callback' => __NAMESPACE__ . '\render_buttons_block', + ) + ); + } +} +add_action( 'init', __NAMESPACE__ . '\register_buttons_block' ); + +/** + * Render callback. + * + * @param array $attributes Array containing the block attributes. + * @param string $content String containing the block content. + * + * @return string + */ +function render_buttons_block( $attributes, $content ) { + Jetpack_Gutenberg::load_styles_as_required( BUTTONS_NAME ); + + return $content; +} diff --git a/plugins/jetpack/extensions/blocks/premium-content/logged-out-view/logged-out-view.php b/plugins/jetpack/extensions/blocks/premium-content/logged-out-view/logged-out-view.php new file mode 100644 index 00000000..80a68dab --- /dev/null +++ b/plugins/jetpack/extensions/blocks/premium-content/logged-out-view/logged-out-view.php @@ -0,0 +1,73 @@ +is_woa_site() ) { + // Determine required `context` key based on Gutenberg version. + $deprecated = function_exists( 'gutenberg_get_post_from_context' ); + $uses = $deprecated ? 'context' : 'uses_context'; + + Blocks::jetpack_register_block( + LOGGEDOUT_VIEW_NAME, + array( + 'render_callback' => __NAMESPACE__ . '\render_loggedout_view_block', + $uses => array( 'premium-content/planId' ), + ) + ); + } +} +add_action( 'init', __NAMESPACE__ . '\register_loggedout_view_block' ); + +/** + * Render callback. + * + * @param array $attributes Array containing the block attributes. + * @param string $content String containing the block content. + * @param object $block Object containing block details. + * + * @return string + */ +function render_loggedout_view_block( $attributes, $content, $block = null ) { + if ( ! pre_render_checks() ) { + return ''; + } + + $visitor_has_access = current_visitor_can_access( $attributes, $block ); + + if ( $visitor_has_access ) { + // The viewer has access to premium content, so the viewer shouldn't see the logged out view. + return ''; + } + + Jetpack_Gutenberg::load_styles_as_required( LOGGEDOUT_VIEW_NAME ); + + // Old versions of the block were rendering the subscribe/login button server-side, so we need to still support them. + if ( ! empty( $attributes['buttonClasses'] ) ) { + require_once __DIR__ . '/../_inc/legacy-buttons.php'; + + $buttons = create_legacy_buttons_markup( $attributes, $content, $block ); + return $content . $buttons; + } + + return $content; +} diff --git a/plugins/jetpack/extensions/blocks/premium-content/login-button/login-button.php b/plugins/jetpack/extensions/blocks/premium-content/login-button/login-button.php new file mode 100644 index 00000000..3238d8a9 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/premium-content/login-button/login-button.php @@ -0,0 +1,59 @@ +is_woa_site() ) { + Blocks::jetpack_register_block( + LOGIN_BUTTON_NAME, + array( + 'render_callback' => __NAMESPACE__ . '\render_login_button_block', + ) + ); + } +} +add_action( 'init', __NAMESPACE__ . '\register_login_button_block' ); + +/** + * Render callback. + * + * @param array $attributes Array containing the block attributes. + * @param string $content String containing the block content. + * + * @return string + */ +function render_login_button_block( $attributes, $content ) { + if ( ! pre_render_checks() ) { + return ''; + } + + if ( is_user_logged_in() ) { + // The viewer is logged it, so they shouldn't see the login button. + return ''; + } + + Jetpack_Gutenberg::load_styles_as_required( LOGIN_BUTTON_NAME ); + + $url = subscription_service()->access_url(); + + return preg_replace( '/(<]*)>/i', '$1 href="' . esc_url( $url ) . '">', $content ); +} diff --git a/plugins/jetpack/extensions/blocks/premium-content/premium-content.php b/plugins/jetpack/extensions/blocks/premium-content/premium-content.php new file mode 100644 index 00000000..41915978 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/premium-content/premium-content.php @@ -0,0 +1,131 @@ +is_woa_site() ) { + // Determine required `context` key based on Gutenberg version. + $deprecated = function_exists( 'gutenberg_get_post_from_context' ); + $provides = $deprecated ? 'providesContext' : 'provides_context'; + + Blocks::jetpack_register_block( + FEATURE_NAME, + array( + 'render_callback' => __NAMESPACE__ . '\render_block', + 'plan_check' => true, + 'attributes' => array( + 'isPremiumContentChild' => array( + 'type' => 'boolean', + 'default' => true, + ), + ), + $provides => array( + 'premium-content/planId' => 'selectedPlanId', + 'isPremiumContentChild' => 'isPremiumContentChild', + ), + ) + ); + } +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); + +/** + * Render callback. + * + * @param array $attributes Array containing the block attributes. + * @param string $content String containing the block content. + * + * @return string + */ +function render_block( $attributes, $content ) { + if ( ! pre_render_checks() ) { + return ''; + } + + if ( + ! membership_checks() + // Only display Stripe nudge if Upgrade nudge isn't displaying. + && required_plan_checks() + ) { + $stripe_nudge = render_stripe_nudge(); + return $stripe_nudge . $content; + } + + // We don't use FEATURE_NAME here because styles are not in /container folder. + Jetpack_Gutenberg::load_styles_as_required( 'premium-content' ); + return $content; +} + +/** + * Server-side rendering for the stripe connection nudge. + * + * @return string Final content to render. + */ +function render_stripe_nudge() { + if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { + \jetpack_require_lib( 'memberships' ); + $blog_id = get_current_blog_id(); + $settings = (array) \get_memberships_settings_for_site( $blog_id ); + + return stripe_nudge( + $settings['connect_url'], + __( 'Connect to Stripe to use this block on your site.', 'jetpack' ), + __( 'Connect', 'jetpack' ) + ); + } elseif ( ( new Host() )->is_woa_site() ) { + // On WoA sites, the Stripe connection url is not easily available + // server-side, so we redirect them to the post in the editor in order + // to connect. + return stripe_nudge( + get_edit_post_link( get_the_ID() ), + __( 'Connect to Stripe in the editor to use this block on your site.', 'jetpack' ), + __( 'Edit post', 'jetpack' ) + ); + } + + // The Premium Content block is not supported on Jetpack sites. + return ''; +} + +/** + * Render the stripe nudge. + * + * @param string $checkout_url Url for the CTA. + * @param string $description Text of the nudge. + * @param string $button_text Text of the button. + * + * @return string Final content to render. + */ +function stripe_nudge( $checkout_url, $description, $button_text ) { + \jetpack_require_lib( 'components' ); + return \Jetpack_Components::render_frontend_nudge( + array( + 'checkoutUrl' => $checkout_url, + 'description' => $description, + 'buttonText' => $button_text, + ) + ); +} diff --git a/plugins/jetpack/extensions/blocks/premium-content/subscriber-view/subscriber-view.php b/plugins/jetpack/extensions/blocks/premium-content/subscriber-view/subscriber-view.php new file mode 100644 index 00000000..28b13255 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/premium-content/subscriber-view/subscriber-view.php @@ -0,0 +1,65 @@ +is_woa_site() ) { + // Determine required `context` key based on Gutenberg version. + $deprecated = function_exists( 'gutenberg_get_post_from_context' ); + $uses = $deprecated ? 'context' : 'uses_context'; + + Blocks::jetpack_register_block( + SUBSCRIBER_VIEW_NAME, + array( + 'render_callback' => __NAMESPACE__ . '\render_subscriber_view_block', + $uses => array( 'premium-content/planId' ), + ) + ); + } +} +add_action( 'init', __NAMESPACE__ . '\register_subscriber_view_block' ); + +/** + * Render callback. + * + * @param array $attributes Array containing the block attributes. + * @param string $content String containing the block content. + * @param object $block Object containing the full block. + * + * @return string + */ +function render_subscriber_view_block( $attributes, $content, $block = null ) { + if ( ! pre_render_checks() ) { + return ''; + } + + $visitor_has_access = current_visitor_can_access( $attributes, $block ); + + if ( $visitor_has_access ) { + Jetpack_Gutenberg::load_styles_as_required( SUBSCRIBER_VIEW_NAME ); + + // The viewer has access to premium content, so the viewer can see the subscriber view content. + return $content; + } + + return ''; +} diff --git a/plugins/jetpack/extensions/blocks/rating-star/rating-meta.php b/plugins/jetpack/extensions/blocks/rating-star/rating-meta.php index dfbf3003..3d843101 100644 --- a/plugins/jetpack/extensions/blocks/rating-star/rating-meta.php +++ b/plugins/jetpack/extensions/blocks/rating-star/rating-meta.php @@ -4,7 +4,7 @@ * * @since 8.0.0 * - * @package Jetpack + * @package automattic/jetpack */ if ( ! function_exists( 'jetpack_rating_meta_get_symbol_low_fidelity' ) ) { @@ -14,7 +14,7 @@ if ( ! function_exists( 'jetpack_rating_meta_get_symbol_low_fidelity' ) ) { * @return string */ function jetpack_rating_meta_get_symbol_low_fidelity() { - return '⭐'; + return ''; } } @@ -61,6 +61,26 @@ if ( ! function_exists( 'jetpack_rating_meta_get_symbol_high_fidelity' ) ) { } } +if ( ! function_exists( 'jetpack_rating_get_schema_for_symbol' ) ) { + /** + * Returns an itemprop and content for rating symbols + * + * @param integer $position the position of the symbol. + * @param integer $max_rating the maximum symbol score. + * + * @return string + */ + function jetpack_rating_get_schema_for_symbol( $position, $max_rating ) { + $schema = ''; + if ( 1 === $position ) { + $schema = 'itemprop="worstRating" content="0.5"'; + } elseif ( $max_rating === $position ) { + $schema = 'itemprop="bestRating" content="' . esc_attr( $max_rating ) . '"'; + } + return $schema; + } +} + if ( ! function_exists( 'jetpack_rating_meta_get_symbols' ) ) { /** * Returns the symbol for the block. @@ -74,7 +94,7 @@ if ( ! function_exists( 'jetpack_rating_meta_get_symbols' ) ) { // These are hidden by default, then unhid when CSS loads. $symbols_hifi = array(); for ( $pos = 1; $pos <= $attributes['maxRating']; $pos++ ) { - $symbols_hifi[] = '' . jetpack_rating_meta_get_symbol_high_fidelity( $attributes, $pos ) . ''; + $symbols_hifi[] = '' . jetpack_rating_meta_get_symbol_high_fidelity( $attributes, $pos ) . ''; } // Output fallback symbols for low fidelity contexts, like AMP, @@ -99,9 +119,11 @@ if ( ! function_exists( 'jetpack_rating_meta_render_block' ) ) { function jetpack_rating_meta_render_block( $attributes ) { $classname = empty( $attributes['className'] ) ? '' : ' ' . $attributes['className']; return sprintf( - '
    %2$s
    ', + '
    %2$s%3$s
    ', esc_attr( 'wp-block-jetpack-rating-' . $attributes['ratingStyle'] . $classname ), jetpack_rating_meta_get_symbols( $attributes ), + // translators: %1$s is awarded rating score, %2$s is the best possible rating. + '' . sprintf( __( 'Rating: %1$s out of %2$s.', 'jetpack' ), esc_attr( $attributes['rating'] ), esc_attr( $attributes['maxRating'] ) ) . '', ( isset( $attributes['align'] ) ) ? esc_attr( $attributes['align'] ) : '' ); } diff --git a/plugins/jetpack/extensions/blocks/rating-star/rating-star.php b/plugins/jetpack/extensions/blocks/rating-star/rating-star.php index 8b351e9b..3a5597dd 100644 --- a/plugins/jetpack/extensions/blocks/rating-star/rating-star.php +++ b/plugins/jetpack/extensions/blocks/rating-star/rating-star.php @@ -4,12 +4,59 @@ * * @since 8.0.0 * - * @package Jetpack + * @package automattic/jetpack */ +namespace Automattic\Jetpack\Extensions\Rating_Star; + +use Automattic\Jetpack\Blocks; +use Jetpack_Gutenberg; + +const FEATURE_NAME = 'rating-star'; +const BLOCK_NAME = 'jetpack/' . FEATURE_NAME; + // Load generic function definitions. require_once __DIR__ . '/rating-meta.php'; +/** + * 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( + BLOCK_NAME, + array( + 'render_callback' => __NAMESPACE__ . '\render_block', + 'attributes' => array( + 'rating' => array( + 'type' => 'number', + 'default' => 1, + ), + 'maxRating' => array( + 'type' => 'number', + 'default' => 5, + ), + 'color' => array( + 'type' => 'string', + ), + 'ratingStyle' => array( + 'type' => 'string', + 'default' => 'star', + ), + 'className' => array( + 'type' => 'string', + ), + 'align' => array( + 'type' => 'string', + 'default' => 'left', + ), + ), + ) + ); +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); + /** * Dynamic rendering of the block. * @@ -17,50 +64,22 @@ require_once __DIR__ . '/rating-meta.php'; * * @return string */ -function jetpack_rating_star_render_block( $attributes ) { +function render_block( $attributes ) { // Tell Jetpack to load the assets registered via jetpack_register_block. - Jetpack_Gutenberg::load_assets_as_required( 'rating-star' ); + Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME ); return jetpack_rating_meta_render_block( $attributes ); } /** - * The following filter is added only to support the old 0.6.2 version of the AMP plugin. - * This entire section can be removed once we're on version a newer version. - * Confirmed that version 1.4.1 (or presumably newer) does not need this filter. + * Older versions of AMP (0.6.2) are unable to render the markup, so we hide it + * Newer versions of AMP (1.4.1+) seem OK, but need the screen-reader text hidden */ -function jetpack_rating_star_amp_add_inline_css() { - echo '.wp-block-jetpack-rating-star span { display: none; }'; +function amp_add_inline_css() { + if ( defined( 'AMP__VERSION' ) && version_compare( AMP__VERSION, '1.4.1', '>=' ) ) { + echo '.wp-block-jetpack-rating-star span.screen-reader-text { border: 0; clip: rect(1px, 1px, 1px, 1px); clip-path: inset(50%); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; word-wrap: normal; }'; + } else { + echo '.wp-block-jetpack-rating-star span:not([aria-hidden="true"]) { display: none; }'; + } } -add_action( 'amp_post_template_css', 'jetpack_rating_star_amp_add_inline_css', 11 ); - -jetpack_register_block( - 'jetpack/rating-star', - array( - 'render_callback' => 'jetpack_rating_star_render_block', - 'attributes' => array( - 'rating' => array( - 'type' => 'number', - 'default' => 1, - ), - 'maxRating' => array( - 'type' => 'number', - 'default' => 5, - ), - 'color' => array( - 'type' => 'string', - ), - 'ratingStyle' => array( - 'type' => 'string', - 'default' => 'star', - ), - 'className' => array( - 'type' => 'string', - ), - 'align' => array( - 'type' => 'string', - 'default' => 'left', - ), - ), - ) -); +add_action( 'amp_post_template_css', __NAMESPACE__ . '\amp_add_inline_css', 11 ); diff --git a/plugins/jetpack/extensions/blocks/recurring-payments/recurring-payments.php b/plugins/jetpack/extensions/blocks/recurring-payments/recurring-payments.php index d5d461e9..7740379e 100644 --- a/plugins/jetpack/extensions/blocks/recurring-payments/recurring-payments.php +++ b/plugins/jetpack/extensions/blocks/recurring-payments/recurring-payments.php @@ -4,10 +4,10 @@ * * @since 7.3.0 * - * @package Jetpack + * @package automattic/jetpack */ -if ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) || Jetpack::is_active() ) { +if ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) || Jetpack::is_connection_ready() ) { require_once JETPACK__PLUGIN_DIR . '/modules/memberships/class-jetpack-memberships.php'; Jetpack_Memberships::get_instance()->register_gutenberg_block(); } diff --git a/plugins/jetpack/extensions/blocks/repeat-visitor/repeat-visitor.php b/plugins/jetpack/extensions/blocks/repeat-visitor/repeat-visitor.php index 2facf626..ba3c16c2 100644 --- a/plugins/jetpack/extensions/blocks/repeat-visitor/repeat-visitor.php +++ b/plugins/jetpack/extensions/blocks/repeat-visitor/repeat-visitor.php @@ -4,15 +4,29 @@ * * @since 7.2.0 * - * @package Jetpack + * @package automattic/jetpack */ -jetpack_register_block( - 'jetpack/repeat-visitor', - array( - 'render_callback' => 'jetpack_repeat_visitor_block_render', - ) -); +namespace Automattic\Jetpack\Extensions\Repeat_Visitor; + +use Automattic\Jetpack\Blocks; +use Jetpack_Gutenberg; + +const FEATURE_NAME = 'repeat-visitor'; +const BLOCK_NAME = 'jetpack/' . FEATURE_NAME; + +/** + * 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( + BLOCK_NAME, + array( 'render_callback' => __NAMESPACE__ . '\render_block' ) + ); +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); /** * Repeat Visitor block dependency declaration. @@ -22,14 +36,14 @@ jetpack_register_block( * * @return string */ -function jetpack_repeat_visitor_block_render( $attributes, $content ) { - Jetpack_Gutenberg::load_assets_as_required( 'repeat-visitor' ); +function render_block( $attributes, $content ) { + Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME ); - $classes = Jetpack_Gutenberg::block_classes( 'repeat-visitor', $attributes ); + $classes = Blocks::classes( FEATURE_NAME, $attributes ); - $count = isset( $_COOKIE['jp-visit-counter'] ) ? intval( $_COOKIE['jp-visit-counter'] ) : 0; + $count = isset( $_COOKIE['jp-visit-counter'] ) ? (int) $_COOKIE['jp-visit-counter'] : 0; $criteria = isset( $attributes['criteria'] ) ? $attributes['criteria'] : 'after-visits'; - $threshold = isset( $attributes['threshold'] ) ? intval( $attributes['threshold'] ) : 3; + $threshold = isset( $attributes['threshold'] ) ? (int) $attributes['threshold'] : 3; if ( ( 'after-visits' === $criteria && $count >= $threshold ) || diff --git a/plugins/jetpack/extensions/blocks/revue/revue.php b/plugins/jetpack/extensions/blocks/revue/revue.php new file mode 100644 index 00000000..d0313af0 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/revue/revue.php @@ -0,0 +1,253 @@ + __NAMESPACE__ . '\render_block' ) + ); +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); + +/** + * Revue block render callback. + * + * @param array $attributes Array containing the Revue block attributes. + * @param string $content The Revue block content. + * + * @return string + */ +function render_block( $attributes, $content ) { + if ( ! array_key_exists( 'revueUsername', $attributes ) ) { + return ''; + } + + $email_label = get_revue_attribute( 'emailLabel', $attributes ); + $email_placeholder = get_revue_attribute( 'emailPlaceholder', $attributes ); + $first_name_label = get_revue_attribute( 'firstNameLabel', $attributes ); + $first_name_placeholder = get_revue_attribute( 'firstNamePlaceholder', $attributes ); + $first_name_show = get_revue_attribute( 'firstNameShow', $attributes ); + $last_name_label = get_revue_attribute( 'lastNameLabel', $attributes ); + $last_name_placeholder = get_revue_attribute( 'lastNamePlaceholder', $attributes ); + $last_name_show = get_revue_attribute( 'lastNameShow', $attributes ); + $url = sprintf( 'https://www.getrevue.co/profile/%s/add_subscriber', $attributes['revueUsername'] ); + $base_class = Blocks::classes( FEATURE_NAME, array() ) . '__'; + $classes = Blocks::classes( FEATURE_NAME, $attributes ); + + Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME ); + + ob_start(); + ?> + +
    +
    +
    + +
    + +
    + +
    + +
    + +
    + +
    +
    +

    + +

    +

    + +

    +
    +
    + + __( 'Subscribe', 'jetpack' ), + 'emailLabel' => __( 'Email address', 'jetpack' ), + 'emailPlaceholder' => __( 'Enter your email address', 'jetpack' ), + 'firstNameLabel' => __( 'First name', 'jetpack' ), + 'firstNamePlaceholder' => __( 'Enter your first name', 'jetpack' ), + 'firstNameShow' => true, + 'lastNameLabel' => __( 'Last name', 'jetpack' ), + 'lastNamePlaceholder' => __( 'Enter your last name', 'jetpack' ), + 'lastNameShow' => true, + ); + + if ( array_key_exists( $attribute, $default_attributes ) ) { + return $default_attributes[ $attribute ]; + } +} + +/** + * DEPRECATED V1 + */ + +/** + * Create the Revue subscribe button. + * + * @param array $attributes Array containing the Revue block attributes. + * + * @return string + */ +function get_deprecated_v1_revue_button( $attributes ) { + $classes = array( 'wp-block-button__link' ); + $styles = array(); + + $text = get_revue_attribute( 'text', $attributes ); + $has_class_name = array_key_exists( 'className', $attributes ); + $has_named_text_color = array_key_exists( 'textColor', $attributes ); + $has_custom_text_color = array_key_exists( 'customTextColor', $attributes ); + $has_named_background_color = array_key_exists( 'backgroundColor', $attributes ); + $has_custom_background_color = array_key_exists( 'customBackgroundColor', $attributes ); + $has_named_gradient = array_key_exists( 'gradient', $attributes ); + $has_custom_gradient = array_key_exists( 'customGradient', $attributes ); + $has_border_radius = array_key_exists( 'borderRadius', $attributes ); + + if ( $has_class_name ) { + $classes[] = $attributes['className']; + } + + if ( $has_named_text_color || $has_custom_text_color ) { + $classes[] = 'has-text-color'; + } + if ( $has_named_text_color ) { + $classes[] = sprintf( 'has-%s-color', $attributes['textColor'] ); + } elseif ( $has_custom_text_color ) { + $styles[] = sprintf( 'color: %s;', $attributes['customTextColor'] ); + } + + if ( + $has_named_background_color || + $has_custom_background_color || + $has_named_gradient || + $has_custom_gradient + ) { + $classes[] = 'has-background'; + } + if ( $has_named_background_color && ! $has_custom_gradient ) { + $classes[] = sprintf( 'has-%s-background-color', $attributes['backgroundColor'] ); + } + if ( $has_named_gradient ) { + $classes[] = sprintf( 'has-%s-gradient-background', $attributes['gradient'] ); + } elseif ( $has_custom_gradient ) { + $styles[] = sprintf( 'background: %s;', $attributes['customGradient'] ); + } + if ( + $has_custom_background_color && + ! $has_named_background_color && + ! $has_named_gradient && + ! $has_custom_gradient + ) { + $styles[] = sprintf( 'background-color: %s;', $attributes['customBackgroundColor'] ); + } + + if ( $has_border_radius ) { + // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison + if ( 0 == $attributes['borderRadius'] ) { + $classes[] = 'no-border-radius'; + } else { + $styles[] = sprintf( 'border-radius: %spx;', $attributes['borderRadius'] ); + } + } + + ob_start(); + ?> + +
    + +
    + + __NAMESPACE__ . '\render_block', + 'plan_check' => true, + ) + ); +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); + +/** + * Render callback. + * + * @param array $attributes Array containing the block attributes. + * @param string $content String containing the block content. + * + * @return string + */ +function render_block( $attributes, $content ) { + Jetpack_Gutenberg::load_styles_as_required( FEATURE_NAME ); + + return $content; +} diff --git a/plugins/jetpack/extensions/blocks/send-a-message/whatsapp-button/whatsapp-button.php b/plugins/jetpack/extensions/blocks/send-a-message/whatsapp-button/whatsapp-button.php new file mode 100644 index 00000000..fa30a228 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/send-a-message/whatsapp-button/whatsapp-button.php @@ -0,0 +1,45 @@ + __NAMESPACE__ . '\render_block', + 'plan_check' => true, + ) + ); +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); + +/** + * Render callback. + * + * @param array $attributes Array containing the block attributes. + * @param string $content String containing the block content. + * + * @return string + */ +function render_block( $attributes, $content ) { + Jetpack_Gutenberg::load_styles_as_required( PARENT_NAME ); + + return $content; +} diff --git a/plugins/jetpack/extensions/blocks/simple-payments/simple-payments.php b/plugins/jetpack/extensions/blocks/simple-payments/simple-payments.php new file mode 100644 index 00000000..b30d3045 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/simple-payments/simple-payments.php @@ -0,0 +1,96 @@ + __NAMESPACE__ . '\render_block', + 'plan_check' => true, + ) + ); +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); + +/** + * Pay with PayPal block dynamic rendering. + * + * @param array $attr Array containing the block attributes. + * @param string $content String containing the block content. + * + * @return string + */ +function render_block( $attr, $content ) { + // Do nothing if block content is a `simple-payment` shortcode. + if ( preg_match( '/\[simple-payment(.*)]/', $content ) ) { + return $content; + } + + // Keep content as-is if rendered in other contexts than frontend (i.e. feed, emails, API, etc.). + if ( ! jetpack_is_frontend() ) { + return $content; + } + + $simple_payments = Jetpack_Simple_Payments::getInstance(); + $simple_payments->enqueue_frontend_assets(); + + // For AMP requests, make sure the purchase link redirects to the non-AMP post URL. + if ( Blocks::is_amp_request() ) { + $content = preg_replace( + '#(.*)#i', + '$1rel="$2 noamphtml"$3', + $content + ); + return $content; + } + + // Augment block UI with a PayPal button if rendered on the frontend. + $product_id = $attr['productId']; + $dom_id = wp_unique_id( "jetpack-simple-payments-{$product_id}_" ); + $is_multiple = get_post_meta( $product_id, 'spay_multiple', true ) || '0'; + + $simple_payments->setup_paypal_checkout_button( $product_id, $dom_id, $is_multiple ); + + $purchase_box = $simple_payments->output_purchase_box( $dom_id, $is_multiple ); + $content = preg_replace( '#post_content ) ) { + return true; + } + return $skip; +} +add_filter( 'amp_skip_post', __NAMESPACE__ . '\amp_skip_post', 10, 3 ); diff --git a/plugins/jetpack/extensions/blocks/slideshow/slideshow.php b/plugins/jetpack/extensions/blocks/slideshow/slideshow.php index ba18661c..bef76ed3 100644 --- a/plugins/jetpack/extensions/blocks/slideshow/slideshow.php +++ b/plugins/jetpack/extensions/blocks/slideshow/slideshow.php @@ -4,15 +4,29 @@ * * @since 7.1.0 * - * @package Jetpack + * @package automattic/jetpack */ -jetpack_register_block( - 'jetpack/slideshow', - array( - 'render_callback' => 'jetpack_slideshow_block_load_assets', - ) -); +namespace Automattic\Jetpack\Extensions\Slideshow; + +use Automattic\Jetpack\Blocks; +use Jetpack_Gutenberg; + +const FEATURE_NAME = 'slideshow'; +const BLOCK_NAME = 'jetpack/' . FEATURE_NAME; + +/** + * 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( + BLOCK_NAME, + array( 'render_callback' => __NAMESPACE__ . '\load_assets' ) + ); +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); /** * Slideshow block registration/dependency declaration. @@ -22,10 +36,10 @@ jetpack_register_block( * * @return string */ -function jetpack_slideshow_block_load_assets( $attr, $content ) { - Jetpack_Gutenberg::load_assets_as_required( 'slideshow' ); - if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) { - return jetpack_slideshow_block_render_amp( $attr ); +function load_assets( $attr, $content ) { + Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME ); + if ( Blocks::is_amp_request() ) { + return render_amp( $attr ); } return $content; } @@ -37,27 +51,30 @@ function jetpack_slideshow_block_load_assets( $attr, $content ) { * * @return string */ -function jetpack_slideshow_block_render_amp( $attr ) { +function render_amp( $attr ) { + if ( empty( $attr['ids'] ) ) { + return ''; + } + static $wp_block_jetpack_slideshow_id = 0; $wp_block_jetpack_slideshow_id++; - $ids = empty( $attr['ids'] ) ? array() : $attr['ids']; - $autoplay = empty( $attr['autoplay'] ) ? false : $attr['autoplay']; - - $extras = array( + $ids = $attr['ids']; + $autoplay = empty( $attr['autoplay'] ) ? false : true; + $extras = array( 'wp-amp-block', $autoplay ? 'wp-block-jetpack-slideshow__autoplay' : null, $autoplay ? 'wp-block-jetpack-slideshow__autoplay-playing' : null, ); - $classes = Jetpack_Gutenberg::block_classes( 'slideshow', $attr, $extras ); + $classes = Blocks::classes( FEATURE_NAME, $attr, $extras ); return sprintf( '
    %3$s%4$s%5$s
    ', esc_attr( $classes ), absint( $wp_block_jetpack_slideshow_id ), - jetpack_slideshow_block_amp_carousel( $attr, $wp_block_jetpack_slideshow_id ), - $autoplay ? jetpack_slideshow_block_autoplay_ui( $wp_block_jetpack_slideshow_id ) : '', - jetpack_slideshow_block_bullets( $ids, $wp_block_jetpack_slideshow_id ) + amp_carousel( $attr, $wp_block_jetpack_slideshow_id ), + $autoplay ? autoplay_ui( $wp_block_jetpack_slideshow_id ) : '', + bullets( $ids, $wp_block_jetpack_slideshow_id ) ); } @@ -69,7 +86,7 @@ function jetpack_slideshow_block_render_amp( $attr ) { * * @return string amp-carousel markup. */ -function jetpack_slideshow_block_amp_carousel( $attr, $block_ordinal ) { +function amp_carousel( $attr, $block_ordinal ) { $ids = empty( $attr['ids'] ) ? array() : $attr['ids']; $first_image = wp_get_attachment_metadata( $ids[0] ); $delay = empty( $attr['delay'] ) ? 3 : absint( $attr['delay'] ); @@ -84,7 +101,7 @@ function jetpack_slideshow_block_amp_carousel( $attr, $block_ordinal ) { esc_attr__( 'Previous Slide', 'jetpack' ), $autoplay ? 'autoplay delay=' . esc_attr( $delay * 1000 ) : '', absint( $block_ordinal ), - implode( '', jetpack_slideshow_block_slides( $ids, $width, $height ) ) + implode( '', slides( $ids, $width, $height ) ) ); } @@ -97,9 +114,9 @@ function jetpack_slideshow_block_amp_carousel( $attr, $block_ordinal ) { * * @return array Array of slides markup. */ -function jetpack_slideshow_block_slides( $ids = array(), $width = 400, $height = 300 ) { +function slides( $ids = array(), $width = 400, $height = 300 ) { return array_map( - function( $id ) use ( $width, $height ) { + function ( $id ) use ( $width, $height ) { $caption = wp_get_attachment_caption( $id ); $figcaption = $caption ? sprintf( '', @@ -132,9 +149,9 @@ function jetpack_slideshow_block_slides( $ids = array(), $width = 400, $height = * * @return array Array of bullets markup. */ -function jetpack_slideshow_block_bullets( $ids = array(), $block_ordinal = 0 ) { +function bullets( $ids = array(), $block_ordinal = 0 ) { $buttons = array_map( - function( $index ) { + function ( $index ) { $aria_label = sprintf( /* translators: %d: Slide number. */ __( 'Go to slide %d', 'jetpack' ), @@ -164,7 +181,7 @@ function jetpack_slideshow_block_bullets( $ids = array(), $block_ordinal = 0 ) { * * @return string Autoplay UI markup. */ -function jetpack_slideshow_block_autoplay_ui( $block_ordinal = 0 ) { +function autoplay_ui( $block_ordinal = 0 ) { $block_id = sprintf( 'wp-block-jetpack-slideshow__%d', absint( $block_ordinal ) diff --git a/plugins/jetpack/extensions/blocks/social-previews/social-previews.php b/plugins/jetpack/extensions/blocks/social-previews/social-previews.php new file mode 100644 index 00000000..fb9b53d9 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/social-previews/social-previews.php @@ -0,0 +1,28 @@ + true, + ) + ); +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); diff --git a/plugins/jetpack/extensions/blocks/story/story.php b/plugins/jetpack/extensions/blocks/story/story.php new file mode 100644 index 00000000..b24e5f07 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/story/story.php @@ -0,0 +1,493 @@ + __NAMESPACE__ . '\render_block' ) + ); +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); + +/** + * Compare 2 urls and return true if they likely correspond to the same resource. + * Ignore scheme, ports, query params and hashes and only compare hostname and pathname. + * + * @param string $url1 - First url used in comparison. + * @param string $url2 - Second url used in comparison. + * + * @returns boolean + */ +function is_same_resource( $url1, $url2 ) { + $url1_parsed = wp_parse_url( $url1 ); + $url2_parsed = wp_parse_url( $url2 ); + return isset( $url1_parsed['host'] ) && + isset( $url2_parsed['host'] ) && + isset( $url1_parsed['path'] ) && + isset( $url2_parsed['path'] ) && + $url1_parsed['host'] === $url2_parsed['host'] && + $url1_parsed['path'] === $url2_parsed['path']; +} + +/** + * Enrich media files retrieved from the story block attributes + * with extra information we can retrieve from the media library. + * + * @param array $media_files - List of media, each as an array containing the media attributes. + * + * @returns array $media_files + */ +function enrich_media_files( $media_files ) { + return array_filter( + array_map( + function ( $media_file ) { + if ( 'image' === $media_file['type'] ) { + return enrich_image_meta( $media_file ); + } + // VideoPress videos can sometimes have type 'file', and mime 'video/videopress' or 'video/mp4'. + // Let's fix `type` for those. + if ( 'file' === $media_file['type'] && 'video' === substr( $media_file['mime'], 0, 5 ) ) { + $media_file['type'] = 'video'; + } + if ( 'video' !== $media_file['type'] ) { // we only support images and videos at this point. + return null; + } + return enrich_video_meta( $media_file ); + }, + $media_files + ) + ); +} + +/** + * Enrich image information with extra data we can retrieve from the media library. + * Add missing `width`, `height`, `srcset`, `sizes`, `title`, `alt` and `caption` properties to the image. + * + * @param array $media_file - An array containing the media attributes for a specific image. + * + * @returns array $media_file_enriched + */ +function enrich_image_meta( $media_file ) { + $attachment_id = isset( $media_file['id'] ) ? $media_file['id'] : null; + $image = wp_get_attachment_image_src( $attachment_id, 'full', false ); + if ( ! $image ) { + return $media_file; + } + list( $src, $width, $height ) = $image; + // Bail if url stored in block attributes is different than the media library one for that id. + if ( isset( $media_file['url'] ) && ! is_same_resource( $media_file['url'], $src ) ) { + return $media_file; + } + $image_meta = wp_get_attachment_metadata( $attachment_id ); + if ( ! is_array( $image_meta ) ) { + return $media_file; + } + $size_array = array( absint( $width ), absint( $height ) ); + return array_merge( + $media_file, + array( + 'width' => absint( $width ), + 'height' => absint( $height ), + 'srcset' => wp_calculate_image_srcset( $size_array, $src, $image_meta, $attachment_id ), + 'sizes' => IMAGE_BREAKPOINTS, + 'title' => get_the_title( $attachment_id ), + 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), + 'caption' => wp_get_attachment_caption( $attachment_id ), + ) + ); +} + +/** + * Enrich video information with extra data we can retrieve from the media library. + * Add missing `width`, `height`, `alt`, `url`, `title`, `caption` and `poster` properties to the image. + * + * @param array $media_file - An array containing the media attributes for a specific video. + * + * @returns array $media_file_enriched + */ +function enrich_video_meta( $media_file ) { + $attachment_id = isset( $media_file['id'] ) ? $media_file['id'] : null; + $video_meta = wp_get_attachment_metadata( $attachment_id ); + if ( ! $video_meta ) { + return $media_file; + } + + $video_url = ! empty( $video_meta['original']['url'] ) ? $video_meta['original']['url'] : wp_get_attachment_url( $attachment_id ); + + // Set the poster attribute for the video tag if a poster image is available. + $poster_url = null; + if ( ! empty( $video_meta['videopress']['poster'] ) ) { + $poster_url = $video_meta['videopress']['poster']; + } elseif ( ! empty( $video_meta['thumb'] ) ) { + $poster_url = str_replace( wp_basename( $video_url ), $video_meta['thumb'], $video_url ); + } + + if ( $poster_url ) { + // Use the global content width for thumbnail resize so we match the `w=` query parameter + // that jetpack is going to add when "Enable site accelerator" is enabled for images. + $content_width = (int) Jetpack::get_content_width(); + $new_width = $content_width > 0 ? $content_width : EMBED_SIZE[0]; + $poster_url = add_query_arg( 'w', $new_width, $poster_url ); + } + + return array_merge( + $media_file, + array( + 'width' => absint( ! empty( $video_meta['width'] ) ? $video_meta['width'] : $media_file['width'] ), + 'height' => absint( ! empty( $video_meta['height'] ) ? $video_meta['height'] : $media_file['height'] ), + 'alt' => ! empty( $video_meta['videopress']['description'] ) ? $video_meta['videopress']['description'] : $media_file['alt'], + 'url' => $video_url, + 'title' => get_the_title( $attachment_id ), + 'caption' => wp_get_attachment_caption( $attachment_id ), + 'poster' => $poster_url, + ) + ); +} + +/** + * Render an image inside a slide + * + * @param array $media - Image information. + * + * @returns string + */ +function render_image( $media ) { + if ( empty( $media['id'] ) || empty( $media['url'] ) ) { + return __( 'Error retrieving media', 'jetpack' ); + } + $image = wp_get_attachment_image_src( $media['id'], 'full', false ); + if ( $image ) { + list( $src, $width, $height ) = $image; + } + + // if image does not match. + if ( ! $image || isset( $media['url'] ) && ! is_same_resource( $media['url'], $src ) ) { + $width = isset( $media['width'] ) ? $media['width'] : null; + $height = isset( $media['height'] ) ? $media['height'] : null; + $title = isset( $media['title'] ) ? $media['title'] : ''; + $alt = isset( $media['alt'] ) ? $media['alt'] : ''; + return sprintf( + '%2$s', + esc_attr( $title ), + esc_attr( $alt ), + $width && $height ? get_image_crop_class( $width, $height ) : '', + esc_attr( $media['url'] ) + ); + } + + $crop_class = get_image_crop_class( $width, $height ); + // need to specify the size of the embed so it picks an image that is large enough for the `src` attribute + // `sizes` is optimized for 1080x1920 (9:16) images + // Note that the Story block does not have thumbnail support, it will load the right + // image based on the viewport size only. + return wp_get_attachment_image( + $media['id'], + EMBED_SIZE, + false, + array( + 'class' => sprintf( 'wp-story-image wp-image-%d %s', $media['id'], $crop_class ), + 'sizes' => IMAGE_BREAKPOINTS, + 'title' => get_the_title( $media['id'] ), + ) + ); +} + +/** + * Return the css crop class if image width and height requires it + * + * @param int $width - Image width. + * @param int $height - Image height. + * + * @returns string The CSS class which will display a cropped image + */ +function get_image_crop_class( $width, $height ) { + $crop_class = ''; + $media_aspect_ratio = $width / $height; + $target_aspect_ratio = EMBED_SIZE[0] / EMBED_SIZE[1]; + if ( $media_aspect_ratio >= $target_aspect_ratio ) { + // image wider than canvas. + $media_too_wide_to_crop = $media_aspect_ratio > $target_aspect_ratio / ( 1 - CROP_UP_TO ); + if ( ! $media_too_wide_to_crop ) { + $crop_class = 'wp-story-crop-wide'; + } + } else { + // image narrower than canvas. + $media_too_narrow_to_crop = $media_aspect_ratio < $target_aspect_ratio * ( 1 - CROP_UP_TO ); + if ( ! $media_too_narrow_to_crop ) { + $crop_class = 'wp-story-crop-narrow'; + } + } + return $crop_class; +} + +/** + * Returns a URL for the site icon. + * + * @param int $size - Size for (square) sitei icon. + * @param string $fallback - Fallback URL to use if no site icon is found. + * + * @returns string + */ +function get_blavatar_or_site_icon_url( $size, $fallback ) { + $image_array = Jetpack_PostImages::from_blavatar( get_the_ID(), $size ); + if ( ! empty( $image_array ) ) { + return $image_array[0]['src']; + } else { + return $fallback; + } +} + +/** + * Render a video inside a slide + * + * @param array $media - Video information. + * + * @returns string + */ +function render_video( $media ) { + if ( empty( $media['id'] ) || empty( $media['mime'] ) || empty( $media['url'] ) ) { + return __( 'Error retrieving media', 'jetpack' ); + } + + if ( ! empty( $media['poster'] ) ) { + return render_image( + array_merge( + $media, + array( + 'type' => 'image', + 'url' => $media['poster'], + ) + ) + ); + } + + return sprintf( + '', + esc_attr( get_the_title( $media['id'] ) ), + esc_attr( $media['mime'] ), + $media['id'], + esc_attr( $media['url'] ) + ); +} + +/** + * Pick a thumbnail to render a static/embedded story + * + * @param array $media_files - list of Media files. + * + * @returns string + */ +function render_static_slide( $media_files ) { + $media_template = ''; + if ( empty( $media_files ) ) { + return ''; + } + + // find an image to showcase. + foreach ( $media_files as $media ) { + switch ( $media['type'] ) { + case 'image': + $media_template = render_image( $media ); + break 2; + case 'video': + // ignore videos without a poster image. + if ( empty( $media['poster'] ) ) { + continue 2; + } + $media_template = render_video( $media ); + break 2; + } + } + + // if no "static" media was found for the thumbnail try to render a video tag without poster. + if ( empty( $media_template ) && ! empty( $media_files ) ) { + $media_template = render_video( $media_files[0] ); + } + + return sprintf( + '
    +
    %s
    +
    ', + $media_template + ); +} + +/** + * Render the top right icon on top of the story embed + * + * @param array $settings - The block settings. + * + * @returns string + */ +function render_top_right_icon( $settings ) { + $show_slide_count = isset( $settings['showSlideCount'] ) ? $settings['showSlideCount'] : false; + $slide_count = isset( $settings['slides'] ) ? count( $settings['slides'] ) : 0; + if ( $show_slide_count ) { + // Render the story block icon along with the slide count. + return sprintf( + '
    + + %d +
    ', + $slide_count + ); + } else { + // Render the Fullscreen Gridicon. + return ( + '
    + + + + + +
    ' + ); + } +} + +/** + * Render a pagination bullet + * + * @param int $slide_index - The slide index it corresponds to. + * @param string $class_name - Optional css class name(s) to customize the bullet element. + * + * @returns string + */ +function render_pagination_bullet( $slide_index, $class_name = '' ) { + return sprintf( + '
    +
    +
    ', + esc_attr( $class_name ), + /* translators: %d is the slide number (1, 2, 3...) */ + sprintf( __( 'Go to slide %d', 'jetpack' ), $slide_index ) + ); +} + +/** + * Render pagination on top of the story embed + * + * @param array $settings - The block settings. + * + * @returns string + */ +function render_pagination( $settings ) { + $show_slide_count = isset( $settings['showSlideCount'] ) ? $settings['showSlideCount'] : false; + if ( $show_slide_count ) { + return ''; + } + $slide_count = isset( $settings['slides'] ) ? count( $settings['slides'] ) : 0; + $bullet_count = min( $slide_count, MAX_BULLETS ); + $bullet_ellipsis = $slide_count > $bullet_count + ? render_pagination_bullet( $bullet_count + 1, 'wp-story-pagination-ellipsis' ) + : ''; + return sprintf( + '
    + %s +
    ', + join( "\n", array_map( __NAMESPACE__ . '\render_pagination_bullet', range( 1, $bullet_count ) ) ) . $bullet_ellipsis + ); +} + +/** + * Render story block + * + * @param array $attributes - Block attributes. + * + * @returns string + */ +function render_block( $attributes ) { + // Let's use a counter to have a different id for each story rendered in the same context. + static $story_block_counter = 0; + + Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME ); + + $media_files = isset( $attributes['mediaFiles'] ) ? enrich_media_files( $attributes['mediaFiles'] ) : array(); + $settings_from_attributes = isset( $attributes['settings'] ) ? $attributes['settings'] : array(); + + $settings = array_merge( + $settings_from_attributes, + array( + 'slides' => $media_files, + ) + ); + + return sprintf( + '
    ', + esc_attr( Blocks::classes( FEATURE_NAME, $attributes, array( 'wp-story', 'aligncenter' ) ) ), + esc_attr( 'wp-story-' . get_the_ID() . '-' . strval( ++$story_block_counter ) ), + filter_var( wp_json_encode( $settings ), FILTER_SANITIZE_SPECIAL_CHARS ), + get_permalink() . '?wp-story-load-in-fullscreen=true&wp-story-play-on-load=true', + __( 'Play story in new tab', 'jetpack' ), + __( 'Site icon', 'jetpack' ), + esc_attr( get_blavatar_or_site_icon_url( 80, includes_url( 'images/w-logo-blue.png' ) ) ), + esc_html( get_the_title() ), + render_static_slide( $media_files ), + render_top_right_icon( $settings ), + render_pagination( $settings ) + ); +} diff --git a/plugins/jetpack/extensions/blocks/subscriptions/subscriptions.php b/plugins/jetpack/extensions/blocks/subscriptions/subscriptions.php new file mode 100644 index 00000000..9fa2ab3c --- /dev/null +++ b/plugins/jetpack/extensions/blocks/subscriptions/subscriptions.php @@ -0,0 +1,47 @@ + __NAMESPACE__ . '\render_block' ) + ); + } +} +add_action( 'init', __NAMESPACE__ . '\register_block', 9 ); + +/** + * Subscriptions block render callback. + * + * @param array $attributes Array containing the block attributes. + * @param string $content String containing the block content. + * + * @return string + */ +function render_block( $attributes, $content ) { + Jetpack_Gutenberg::load_styles_as_required( FEATURE_NAME ); + + return $content; +} diff --git a/plugins/jetpack/extensions/blocks/tiled-gallery/tiled-gallery.php b/plugins/jetpack/extensions/blocks/tiled-gallery/tiled-gallery.php index 7900e9b6..bd7cb88b 100644 --- a/plugins/jetpack/extensions/blocks/tiled-gallery/tiled-gallery.php +++ b/plugins/jetpack/extensions/blocks/tiled-gallery/tiled-gallery.php @@ -1,19 +1,30 @@ array( __CLASS__, 'render' ), - ) - ); + if ( + ( defined( 'IS_WPCOM' ) && IS_WPCOM ) + || Jetpack::is_connection_ready() + || ( new Status() )->is_offline_mode() + ) { + Blocks::jetpack_register_block( + self::BLOCK_NAME, + array( + 'render_callback' => array( __CLASS__, 'render' ), + ) + ); + } } /** @@ -40,7 +57,7 @@ class Jetpack_Tiled_Gallery_Block { * @return string */ public static function render( $attr, $content ) { - Jetpack_Gutenberg::load_assets_as_required( 'tiled-gallery' ); + Jetpack_Gutenberg::load_assets_as_required( self::FEATURE_NAME ); $is_squareish_layout = self::is_squareish_layout( $attr ); @@ -63,8 +80,8 @@ class Jetpack_Tiled_Gallery_Block { foreach ( $images[0] as $image_html ) { if ( - preg_match( '/data-width="([0-9]+)"/', $image_html, $img_height ) - && preg_match( '/data-height="([0-9]+)"/', $image_html, $img_width ) + preg_match( '/data-width="([0-9]+)"/', $image_html, $img_width ) + && preg_match( '/data-height="([0-9]+)"/', $image_html, $img_height ) && preg_match( '/src="([^"]+)"/', $image_html, $img_src ) ) { // Drop img src query string so it can be used as a base to add photon params @@ -169,4 +186,4 @@ class Jetpack_Tiled_Gallery_Block { } } -Jetpack_Tiled_Gallery_Block::register(); +Tiled_Gallery::register(); diff --git a/plugins/jetpack/extensions/blocks/wordads/wordads.php b/plugins/jetpack/extensions/blocks/wordads/wordads.php index 9f67d58b..0b76558c 100644 --- a/plugins/jetpack/extensions/blocks/wordads/wordads.php +++ b/plugins/jetpack/extensions/blocks/wordads/wordads.php @@ -4,10 +4,23 @@ * * @since 7.1.0 * - * @package Jetpack + * @package automattic/jetpack */ -class Jetpack_WordAds_Gutenblock { - const BLOCK_NAME = 'jetpack/wordads'; + +namespace Automattic\Jetpack\Extensions; + +use Automattic\Jetpack\Blocks; +use Jetpack; +use Jetpack_Gutenberg; + +/** + * Jetpack's Ads Block class. + * + * @since 7.1.0 + */ +class WordAds { + const FEATURE_NAME = 'wordads'; + const BLOCK_NAME = 'jetpack/' . self::FEATURE_NAME; /** * Check if site is on WP.com Simple. @@ -44,10 +57,10 @@ class Jetpack_WordAds_Gutenblock { */ public static function register() { if ( self::is_available() ) { - jetpack_register_block( + Blocks::jetpack_register_block( self::BLOCK_NAME, array( - 'render_callback' => array( 'Jetpack_WordAds_Gutenblock', 'gutenblock_render' ), + 'render_callback' => array( __CLASS__, 'gutenblock_render' ), ) ); } @@ -61,7 +74,7 @@ class Jetpack_WordAds_Gutenblock { Jetpack_Gutenberg::set_extension_unavailable( self::BLOCK_NAME, 'WordAds unavailable' ); return; } - // Make the block available. Just in case it wasn't registed before. + // Make the block available. Just in case it wasn't registered before. Jetpack_Gutenberg::set_extension_available( self::BLOCK_NAME ); } @@ -75,7 +88,7 @@ class Jetpack_WordAds_Gutenblock { public static function gutenblock_render( $attr ) { global $wordads; - /** This filter is already documented in modules/wordads/wordads.php `insert_ad()` */ + /** This filter is already documented in modules/wordads/class-wordads.php `insert_ad()` */ if ( empty( $wordads ) || empty( $wordads->params ) @@ -93,7 +106,7 @@ class Jetpack_WordAds_Gutenblock { return $wordads->get_ad( 'inline', 'house' ); } - // section_id is mostly depricated at this point, but it helps us (devs) keep track of which ads end up where + // section_id is mostly deprecated at this point, but it helps us (devs) keep track of which ads end up where // 6 is to keep track of gutenblock ads. $section_id = $wordads->params->blog_id . '6'; $align = 'center'; @@ -104,7 +117,7 @@ class Jetpack_WordAds_Gutenblock { $ad_tag_ids = $wordads->get_ad_tags(); $format = 'mrec'; - if ( isset( $attr['format'] ) && in_array( $attr['format'], array_keys( $ad_tag_ids ), true ) ) { + if ( isset( $attr['format'] ) && isset( $ad_tag_ids[ $attr['format'] ] ) ) { $format = $attr['format']; } @@ -115,12 +128,5 @@ class Jetpack_WordAds_Gutenblock { } } -add_action( - 'init', - array( 'Jetpack_WordAds_Gutenblock', 'register' ) -); - -add_action( - 'jetpack_register_gutenberg_extensions', - array( 'Jetpack_WordAds_Gutenblock', 'set_availability' ) -); +add_action( 'init', array( 'Automattic\\Jetpack\\Extensions\\WordAds', 'register' ) ); +add_action( 'jetpack_register_gutenberg_extensions', array( 'Automattic\\Jetpack\\Extensions\\WordAds', 'set_availability' ) ); diff --git a/plugins/jetpack/extensions/extended-blocks/core-audio/core-audio.php b/plugins/jetpack/extensions/extended-blocks/core-audio/core-audio.php new file mode 100644 index 00000000..34f837aa --- /dev/null +++ b/plugins/jetpack/extensions/extended-blocks/core-audio/core-audio.php @@ -0,0 +1,27 @@ +