This commit is contained in:
emmymayo
2025-02-05 23:15:46 +01:00
commit 7269c99357
16995 changed files with 3389680 additions and 0 deletions
@@ -0,0 +1,122 @@
<?php
/**
* Jetpack AI Assistant Block.
*
* @since 12.2
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\AIAssistant;
use Automattic\Jetpack\Blocks;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Status\Host;
use Jetpack_Gutenberg;
/**
* Registers our 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 (
( ( new Host() )->is_wpcom_simple()
|| ! ( new Status() )->is_offline_mode()
) && apply_filters( 'jetpack_ai_enabled', true )
) {
Blocks::jetpack_register_block(
__DIR__,
array( 'render_callback' => __NAMESPACE__ . '\load_assets' )
);
}
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* Jetpack AI Assistant block registration/dependency declaration.
*
* @param array $attr Array containing the Jetpack AI Assistant block attributes.
* @param string $content String containing the Jetpack AI Assistant block content.
*
* @return string
*/
function load_assets( $attr, $content ) {
/*
* Enqueue necessary scripts and styles.
*/
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
return sprintf(
'<div class="%1$s">%2$s</div>',
esc_attr( Blocks::classes( Blocks::get_block_feature( __DIR__ ), $attr ) ),
$content
);
}
/**
* Register extensions.
*/
add_action(
'jetpack_register_gutenberg_extensions',
function () {
if ( apply_filters( 'jetpack_ai_enabled', true ) ) {
Jetpack_Gutenberg::set_extension_available( 'ai-assistant-support' );
Jetpack_Gutenberg::set_extension_available( 'ai-assistant-form-support' );
Jetpack_Gutenberg::set_extension_available( 'ai-content-lens' );
Jetpack_Gutenberg::set_extension_available( 'ai-assistant-backend-prompts' );
Jetpack_Gutenberg::set_extension_available( 'ai-assistant-usage-panel' );
Jetpack_Gutenberg::set_extension_available( 'ai-featured-image-generator' );
Jetpack_Gutenberg::set_extension_available( 'ai-title-optimization' );
Jetpack_Gutenberg::set_extension_available( 'ai-assistant-experimental-image-generation-support' );
Jetpack_Gutenberg::set_extension_available( 'ai-general-purpose-image-generator' );
Jetpack_Gutenberg::set_extension_available( 'ai-assistant-site-logo-support' );
Jetpack_Gutenberg::set_extension_available( 'ai-title-optimization-keywords-support' );
if ( apply_filters( 'breve_enabled', true ) ) {
Jetpack_Gutenberg::set_extension_available( 'ai-proofread-breve' );
}
}
}
);
/**
* Register the `ai-list-to-table-transform` extension.
*/
add_action(
'jetpack_register_gutenberg_extensions',
function () {
if ( apply_filters( 'jetpack_ai_enabled', true ) &&
apply_filters( 'list_to_table_transform_enabled', false )
) {
\Jetpack_Gutenberg::set_extension_available( 'ai-list-to-table-transform' );
}
}
);
/**
* Register the `ai-response-feedback` extension.
*/
add_action(
'jetpack_register_gutenberg_extensions',
function () {
if ( apply_filters( 'jetpack_ai_enabled', true ) &&
apply_filters( 'ai_response_feedback_enabled', true )
) {
\Jetpack_Gutenberg::set_extension_available( 'ai-response-feedback' );
}
}
);
/**
* Register the `ai-seo-assistant` extension.
*/
add_action(
'jetpack_register_gutenberg_extensions',
function () {
if ( apply_filters( 'jetpack_ai_enabled', true ) &&
apply_filters( 'ai_seo_assistant_enabled', false )
) {
\Jetpack_Gutenberg::set_extension_available( 'ai-seo-assistant' );
}
}
);
@@ -0,0 +1,97 @@
<?php
/**
* Jetpack AI Chat.
*
* @since 12.1
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\AIChat;
use Automattic\Jetpack\Blocks;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Search\Module_Control as Search_Module_Control;
use Automattic\Jetpack\Search\Plan as Search_Plan;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Status\Host;
use Jetpack_Gutenberg;
/**
* Registers our 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 (
( new Host() )->is_wpcom_simple()
|| ( ( new Connection_Manager( 'jetpack' ) )->has_connected_owner() && ! ( new Status() )->is_offline_mode() )
) {
Blocks::jetpack_register_block(
__DIR__,
array( 'render_callback' => __NAMESPACE__ . '\load_assets' )
);
}
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* Jetpack AI Paragraph block registration/dependency declaration.
*
* @param array $attr Array containing the Jetpack AI Chat block attributes.
*
* @return string
*/
function load_assets( $attr ) {
/*
* Enqueue necessary scripts and styles.
*/
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
$ask_button_label = isset( $attr['askButtonLabel'] ) ? $attr['askButtonLabel'] : __( 'Ask', 'jetpack' );
$placeholder = isset( $attr['placeholder'] ) ? $attr['placeholder'] : __( 'Ask a question about this site.', 'jetpack' );
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$blog_id = get_current_blog_id();
$type = 'wpcom'; // WPCOM simple sites.
} else {
$blog_id = \Jetpack_Options::get_option( 'id' );
$type = 'jetpack'; // Self-hosted (includes Atomic)
}
return sprintf(
'<div class="%1$s" data-ask-button-label="%2$s" id="jetpack-ai-chat" data-blog-id="%3$d" data-blog-type="%4$s" data-placeholder="%5$s" data-show-copy="%6$d" data-show-feedback="%7$d" data-show-sources="%8$d"></div>',
esc_attr( Blocks::classes( Blocks::get_block_feature( __DIR__ ), $attr ) ),
esc_attr( $ask_button_label ),
esc_attr( $blog_id ),
esc_attr( $type ),
esc_attr( $placeholder ),
esc_attr( isset( $attr['showCopy'] ) ? ( $attr['showCopy'] ? 1 : 0 ) : 1 ),
esc_attr( isset( $attr['showFeedback'] ) ? ( $attr['showFeedback'] ? 1 : 0 ) : 1 ),
esc_attr( isset( $attr['showSources'] ) ? ( $attr['showSources'] ? 1 : 0 ) : 1 )
);
}
/**
* Add the initial state for the AI Chat block.
*/
function add_ai_chat_block_data() {
// Only relevant to the editor right now.
if ( ! is_admin() ) {
return;
}
$search = new Search_Module_Control();
$plan = new Search_Plan();
$initial_state = array(
'jetpackSettings' => array(
'instant_search_enabled' => $search->is_instant_search_enabled(),
'plan_supports_search' => $plan->supports_instant_search(),
),
);
wp_add_inline_script(
'jetpack-blocks-editor',
'var Jetpack_AIChatBlock = ' . wp_json_encode( $initial_state, JSON_HEX_TAG | JSON_HEX_AMP ) . ';',
'before'
);
}
add_action( 'enqueue_block_assets', __NAMESPACE__ . '\add_ai_chat_block_data' );
@@ -0,0 +1,131 @@
<?php
/**
* Blog Stats Block.
*
* @since 13.0
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Blog_Stats;
use Automattic\Jetpack\Blocks;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Stats\WPCOM_Stats;
use Automattic\Jetpack\Status;
use Jetpack_Gutenberg;
/**
* 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 ( ( new Connection_Manager( 'jetpack' ) )->has_connected_owner() && ! ( new Status() )->is_offline_mode() ) {
Blocks::jetpack_register_block(
__DIR__,
array( 'render_callback' => __NAMESPACE__ . '\load_assets' )
);
}
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* Blog Stats block registration/dependency declaration.
*
* @param array $attributes Array containing the Blog Stats block attributes.
*
* @return string
*/
function load_assets( $attributes ) {
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
// For outside the front-end, such as within emails or the API.
if ( ! jetpack_is_frontend() ) {
return;
}
// For when Stats has been disabled subsequent to inserting the block.
if ( ! \Jetpack::is_module_active( 'stats' ) ) {
if ( current_user_can( 'edit_theme_options' ) ) {
return sprintf(
'<p>%s</p>',
wp_kses(
sprintf(
/* translators: placeholder %s is a link to enable Jetpack Stats.. */
__( 'Please <a href="%s">enable Jetpack Stats</a> to use this block.', 'jetpack' ),
esc_url( admin_url( 'admin.php?page=jetpack_modules&module_tag=Jetpack%20Stats' ) )
),
array( 'a' => array( 'href' => array() ) )
)
);
}
return;
}
// For when there's no post ID - eg. search pages.
if ( $attributes['statsOption'] === 'post' && ! get_the_ID() ) {
if ( current_user_can( 'edit_theme_options' ) ) {
return sprintf(
'<p>%s</p>',
esc_html( __( 'There are no stats to display for this post.', 'jetpack' ) )
);
}
return;
}
$stats = 0;
$wpcom_stats = new WPCOM_Stats();
if ( $attributes['statsOption'] === 'post' ) {
// Cache in post meta to prevent wp_options blowing up when retrieving views
// for multiple posts simultaneously (eg. when inserted into template).
$cache_in_meta = true;
$data = $wpcom_stats->convert_stats_array_to_object(
$wpcom_stats->get_post_views(
get_the_ID(),
array( 'fields' => 'views' ),
$cache_in_meta
)
);
if ( isset( $data->views ) ) {
$stats = $data->views;
}
} else {
$data = $wpcom_stats->convert_stats_array_to_object(
$wpcom_stats->get_stats( array( 'fields' => 'stats' ) )
);
if ( $attributes['statsData'] === 'views' && isset( $data->stats->views ) ) {
$stats = $data->stats->views;
}
if ( $attributes['statsData'] === 'visitors' && isset( $data->stats->visitors ) ) {
$stats = $data->stats->visitors;
}
}
$fallback_label = $attributes['statsData'] === 'visitors' ? esc_html(
/* Translators: Number of visitors */
_n( 'visitor', 'visitors', $stats, 'jetpack' )
) : esc_html(
/* Translators: Number of views */
_n( 'hit', 'hits', $stats, 'jetpack' )
);
$label = empty( $attributes['label'] ) ? $fallback_label : $attributes['label'];
$wrapper_attributes = \WP_Block_Supports::get_instance()->apply_block_supports();
return sprintf(
'<div class="jetpack-blog-stats%s%s"%s><p>%s %s</p></div>',
! empty( $attributes['className'] ) ? ' ' . esc_attr( $attributes['className'] ) : '',
! empty( $wrapper_attributes['class'] ) ? ' ' . esc_attr( $wrapper_attributes['class'] ) : '',
! empty( $wrapper_attributes['style'] ) ? ' style="' . esc_attr( $wrapper_attributes['style'] ) . '"' : '',
esc_html( number_format_i18n( $stats ) ),
wp_kses_post( $label )
);
}
@@ -0,0 +1,45 @@
<?php
/**
* Blogging Prompt Block.
*
* @since 11.x
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Blogging_Prompt;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
/**
* 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(
__DIR__,
array( 'render_callback' => __NAMESPACE__ . '\load_assets' )
);
}
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* Blogging Prompt block registration/dependency declaration.
*
* @param array $attr Array containing the Blogging Prompt block attributes.
* @param string $content String containing the Blogging Prompt block content.
*
* @return string
*/
function load_assets( $attr, $content ) {
/*
* Enqueue necessary scripts and styles.
*/
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
return $content;
}
@@ -0,0 +1,163 @@
<?php
/**
* Blogroll Item Block.
*
* @since 12.6
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Blogroll_Item;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
const FEATURE_NAME = 'blogroll-item';
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',
'uses_context' => array( 'openLinksNewWindow' ),
)
);
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* Blogroll Item block registration/dependency declaration.
*
* @param array $attr Array containing the Blogroll Item block attributes.
* @param string $content String containing the block content.
* @param object $block The block.
*
* @return string
*/
function load_assets( $attr, $content, $block ) {
/*
* Enqueue necessary scripts and styles.
*/
Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME );
$kses_defaults = wp_kses_allowed_html( 'post' );
$name = wp_kses( $attr['name'], $kses_defaults );
$name_attr = esc_attr( $attr['name'] );
$id = esc_attr( $attr['id'] );
$url = esc_url( $attr['url'] );
$description = wp_kses( $attr['description'], $kses_defaults );
$icon = esc_attr( isset( $attr['icon'] ) ? $attr['icon'] : null );
$target = esc_attr( $block->context['openLinksNewWindow'] ? '_blank' : '_self' );
$email = esc_attr( get_current_user_id() ? get_userdata( get_current_user_id() )->user_email : '' );
$wp_nonce = esc_attr( wp_create_nonce( 'blogsub_subscribe_' . $id ) );
$subscribe_text = esc_html__( 'Subscribe', 'jetpack' );
$submit_text = esc_html__( 'Submit', 'jetpack' );
$cancel_text = esc_html__( 'Cancel', 'jetpack' );
$disabled_subscribe_button = '';
$subscribe_button_class = 'is-style-fill';
$is_following = ( function_exists( 'wpcom_subs_is_subscribed' ) && wpcom_subs_is_subscribed(
array(
'user_id' => get_current_user_id(),
'blog_id' => $id,
)
) ) || isset( $_GET['blogid'] ) && $id === $_GET['blogid']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- View logic.
if ( $is_following ) {
$subscribe_text = esc_html__( 'Subscribed', 'jetpack' );
$disabled_subscribe_button = 'disabled';
$subscribe_button_class = 'is-style-outline';
}
$placeholder_site_icon = '';
$site_icon_html = <<<HTML
<img class="blogroll-item-image" src="$icon" alt="$name_attr" onerror="this.parentNode.classList.add('empty-site-icon')">
HTML;
if ( empty( $icon ) ) {
$site_icon_html = '';
$placeholder_site_icon = 'empty-site-icon';
}
if ( ! jetpack_is_frontend() ) {
return <<<HTML
<div style="margin-bottom: 10px;">
<a href="$url">$name</a><div>$description</div>
</div>
HTML;
}
$form_buttons = <<<HTML
<!-- wp:buttons {"style":{"spacing":{"blockGap":"10px"}}} -->
<div class="wp-block-buttons">
<!-- wp:button {"className":"is-style-fill"} -->
<div class="wp-block-button jetpack-blogroll-item-submit-button is-style-fill">
<button type="submit" name="blog_id" value="$id" class="wp-block-button__link wp-element-button">$submit_text</button>
</div>
<!-- /wp:button -->
<!-- wp:button {"className":"is-style-outline"} -->
<div class="wp-block-button jetpack-blogroll-item-cancel-button is-style-outline">
<button type="reset" class="wp-block-button__link wp-element-button">$cancel_text</button>
</div>
</div>
<!-- /wp:buttons -->
HTML;
$subscribe_button = <<<HTML
<!-- wp:button {"className":"$subscribe_button_class"} -->
<div class="wp-block-button jetpack-blogroll-item-subscribe-button $subscribe_button_class">
<button type="button" class="wp-block-button__link wp-element-button" {$disabled_subscribe_button}>$subscribe_text</button>
</div>
<!-- /wp:button -->
HTML;
$subscribe_button_html = '';
$fieldset = '';
$has_subscription_form = defined( 'IS_WPCOM' ) && IS_WPCOM && isset( $attr['is_non_wpcom_site'] ) && ! $attr['is_non_wpcom_site'];
$classes = Blocks::classes( FEATURE_NAME, $attr );
if ( $has_subscription_form ) {
$classes .= ' has-subscription-form';
$form_buttons_html = do_blocks( $form_buttons );
$subscribe_button_html = do_blocks( $subscribe_button );
$fieldset = <<<HTML
<fieldset disabled class="jetpack-blogroll-item-submit">
<input type="hidden" name="_wpnonce" value="$wp_nonce">
<input type="email" name="email" placeholder="Email address" value="$email" class="jetpack-blogroll-item-email-input">
$form_buttons_html
</fieldset>
HTML;
}
/**
* Build the block content.
*/
$content = <<<HTML
<div class="jetpack-blogroll-item-information">
<figure class="$placeholder_site_icon">
$site_icon_html
</figure>
<div class="jetpack-blogroll-item-content">
<a class="jetpack-blogroll-item-title" href="$url" target="$target" rel="noopener noreferrer">$name</a>
<div class="jetpack-blogroll-item-description">$description</div>
</div>
$subscribe_button_html
</div>
$fieldset
HTML;
return sprintf(
'<div class="%1$s">
<hr class="wp-block-separator jetpack-blogroll-item-divider" />
<div class="jetpack-blogroll-item-slider">%2$s</div>
</div>',
esc_attr( $classes ),
$content
);
}
@@ -0,0 +1,122 @@
<?php
/**
* Blogroll Block.
*
* @since 12.1
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Blogroll;
require_once __DIR__ . '/blogroll-item/blogroll-item.php';
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
/**
* 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(
__DIR__,
array(
'render_callback' => __NAMESPACE__ . '\load_assets',
'provides_context' => array(
'openLinksNewWindow' => 'open_links_new_window',
),
)
);
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* Blogroll block registration/dependency declaration.
*
* @param array $attr Array containing the Blogroll block attributes.
* @param string $content String containing the Blogroll block content.
*
* @return string
*/
function load_assets( $attr, $content ) {
global $wp;
/*
* Enqueue necessary scripts and styles.
*/
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
$current_location = home_url( $wp->request );
$is_wpcom = ( defined( 'IS_WPCOM' ) && IS_WPCOM );
$form_content = <<<HTML
<form method="post" action="https://subscribe.wordpress.com" accept-charset="utf-8">
<input name="action" type="hidden" value="subscribe">
<input name="source" type="hidden" value="$current_location">
<input name="sub-type" type="hidden" value="jetpack_blogroll">
$content
</form>
HTML;
$blogroll_content = $is_wpcom && jetpack_is_frontend() ? $form_content : $content;
return sprintf(
'<div class="%1$s">%2$s</div>',
esc_attr( Blocks::classes( FEATURE_NAME, $attr ) ),
$blogroll_content
);
}
/**
* Register site_recommendations settings
*
* @since 12.7
*/
function site_recommendations_settings() {
register_setting(
'general',
'Blogroll Recommendations', // Visible to the user see: https://github.com/WordPress/gutenberg/issues/41637
array(
'description' => __( 'Site Recommendations', 'jetpack' ),
'type' => 'array',
'show_in_rest' => array(
'schema' => array(
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'type' => 'string',
'format' => 'text-field',
),
'name' => array(
'type' => 'string',
'format' => 'text-field',
),
'icon' => array(
'type' => 'string',
'format' => 'uri',
),
'url' => array(
'type' => 'string',
'format' => 'uri',
),
'description' => array(
'type' => 'string',
'format' => 'text-field',
),
'is_non_wpcom_site' => array(
'type' => 'boolean',
),
),
),
),
),
'auth_callback' => function () {
return current_user_can( 'edit_posts' );
},
)
);
}
add_action( 'rest_api_init', __NAMESPACE__ . '\site_recommendations_settings' );
@@ -0,0 +1,169 @@
<?php
/**
* Business Hours Block.
*
* @since 7.1.0
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Business_Hours;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
/**
* 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(
__DIR__,
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 get_default_days() {
return array(
array(
'name' => 'Sun',
'hours' => array(),
),
array(
'name' => 'Mon',
'hours' => array(
array(
'opening' => '09:00',
'closing' => '17:00',
),
),
),
array(
'name' => 'Tue',
'hours' => array(
array(
'opening' => '09:00',
'closing' => '17:00',
),
),
),
array(
'name' => 'Wed',
'hours' => array(
array(
'opening' => '09:00',
'closing' => '17:00',
),
),
),
array(
'name' => 'Thu',
'hours' => array(
array(
'opening' => '09:00',
'closing' => '17:00',
),
),
),
array(
'name' => 'Fri',
'hours' => array(
array(
'opening' => '09:00',
'closing' => '17:00',
),
),
),
array(
'name' => 'Sat',
'hours' => array(),
),
);
}
/**
* Dynamic rendering of the block.
*
* @param array $attributes Array containing the business hours block attributes.
*
* @return string
*/
function render( $attributes ) {
global $wp_locale;
if ( empty( $attributes['days'] ) || ! is_array( $attributes['days'] ) ) {
$attributes['days'] = get_default_days();
}
$wrapper_attributes = \WP_Block_Supports::get_instance()->apply_block_supports();
$start_of_week = (int) get_option( 'start_of_week', 0 );
$time_format = get_option( 'time_format' );
$content = sprintf(
'<dl class="jetpack-business-hours%s%s"%s>',
! empty( $attributes['className'] ) ? ' ' . esc_attr( $attributes['className'] ) : '',
! empty( $wrapper_attributes['class'] ) ? ' ' . esc_attr( $wrapper_attributes['class'] ) : '',
! empty( $wrapper_attributes['style'] ) ? ' style="' . esc_attr( $wrapper_attributes['style'] ) . '"' : ''
);
$days = array( 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' );
if ( $start_of_week ) {
$chunk1 = array_slice( $attributes['days'], 0, $start_of_week );
$chunk2 = array_slice( $attributes['days'], $start_of_week );
$attributes['days'] = array_merge( $chunk2, $chunk1 );
}
foreach ( $attributes['days'] as $day ) {
$content .= '<div class="jetpack-business-hours__item"><dt class="' . esc_attr( $day['name'] ) . '">' .
ucfirst( $wp_locale->get_weekday( array_search( $day['name'], $days, true ) ) ) .
'</dt>';
$content .= '<dd class="' . esc_attr( $day['name'] ) . '">';
$days_hours = '';
foreach ( $day['hours'] as $hour ) {
$opening = strtotime( $hour['opening'] );
$closing = strtotime( $hour['closing'] );
if ( ! $opening || ! $closing ) {
continue;
}
if ( $days_hours !== '' ) {
$days_hours .= ', ';
}
$days_hours .= sprintf(
'%1$s - %2$s',
gmdate( $time_format, $opening ),
gmdate( $time_format, $closing )
);
}
if ( empty( $days_hours ) ) {
$days_hours = esc_html__( 'Closed', 'jetpack' );
}
$content .= $days_hours;
$content .= '</dd></div>';
}
$content .= '</dl>';
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
/**
* Allows folks to filter the HTML content for the Business Hours block
*
* @since 7.1.0
*
* @param string $content The default HTML content set by `jetpack_business_hours_render`
* @param array $attributes Attributes generated in the block editor for the Business Hours block
*/
return apply_filters( 'jetpack_business_hours_content', $content, $attributes );
}
@@ -0,0 +1,273 @@
<?php
/**
* Button Block.
*
* @since 8.5.0
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Button;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
const FEATURE_NAME = 'button';
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',
'uses_context' => array( 'jetpack/parentBlockWidth' ),
)
);
}
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 = wp_kses_post( 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 ( ! in_array( $element, array( 'a', 'button', 'input' ), true ) ) {
$element = 'a';
}
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"', esc_attr( wp_strip_all_tags( $text, true ) ) );
}
$button = 'input' === $element
? '<' . $element . $button_attributes . ' />'
: '<' . $element . $button_attributes . '>' . $text . '</' . $element . '>';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
return '<div' . $wrapper_attributes . '>' . $button . '</div>';
}
/**
* 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 );
$has_font_size = array_key_exists( 'fontSize', $attributes );
if ( $has_font_size ) {
$classes[] = 'has-' . $attributes['fontSize'] . '-font-size';
$classes[] = 'has-custom-font-size';
}
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 Universal.Operators.StrictComparisons.LooseEqual
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 );
$has_font_family = array_key_exists( 'fontFamily', $attributes );
$has_typography_styles = array_key_exists( 'style', $attributes ) && array_key_exists( 'typography', $attributes['style'] );
$has_custom_font_size = $has_typography_styles && array_key_exists( 'fontSize', $attributes['style']['typography'] );
$has_custom_text_transform = $has_typography_styles && array_key_exists( 'textTransform', $attributes['style']['typography'] );
if ( $has_font_family ) {
$styles[] = sprintf( 'font-family: %s;', $attributes['fontFamily'] );
}
if ( $has_custom_font_size ) {
$styles[] = sprintf( 'font-size: %s;', $attributes['style']['typography']['fontSize'] );
}
if ( $has_custom_text_transform ) {
$styles[] = sprintf( 'text-transform: %s;', $attributes['style']['typography']['textTransform'] );
}
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 Universal.Operators.StrictComparisons.LooseNotEqual
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 );
}
}
@@ -0,0 +1,264 @@
<?php
/**
* Calendly Block.
*
* @since 8.2.0
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Calendly;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
/**
* 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(
__DIR__,
array(
'render_callback' => __NAMESPACE__ . '\load_assets',
'plan_check' => true,
)
);
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* Calendly block registration/dependency declaration.
*
* @param array $attr Array containing the Calendly block attributes.
* @param string $content String containing the Calendly block content.
*
* @return string
*/
function load_assets( $attr, $content ) {
if ( is_admin() ) {
return;
}
$url = Jetpack_Gutenberg::validate_block_embed_url(
get_attribute( $attr, 'url' ),
array( 'calendly.com' )
);
if ( empty( $url ) ) {
return;
}
/*
* Enqueue necessary scripts and styles.
*/
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
$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( Blocks::get_block_feature( __DIR__ ), $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 ),
'text_color' => sanitize_hex_color_no_hash( $text_color ),
'primary_color' => sanitize_hex_color_no_hash( $primary_color ),
),
$url
);
if ( 'link' === $style ) {
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 );
}
// Render deprecated version of Calendly block if needed. New markup block button class before rendering here.
if ( ! str_contains( $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 ) )
);
}
} elseif ( $is_amp_request ) { // Inline style.
$content = sprintf(
'<div class="%1$s" id="%2$s"><a href="%3$s" role="button" target="_blank">%4$s</a></div>',
esc_attr( Blocks::classes( Blocks::get_block_feature( __DIR__ ), $attr ) ),
esc_attr( $block_id ),
esc_js( $url ),
wp_kses_post( get_attribute( $attr, 'submitButtonText' ) )
);
} else {
$content = sprintf(
'<div class="%1$s" id="%2$s"></div>',
esc_attr( $classes ),
esc_attr( $block_id )
);
$script = <<<JS_END
jetpackInitCalendly( %s, %s );
JS_END;
$json_encode_flags = JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP;
if ( get_option( 'blog_charset' ) === 'UTF-8' ) {
$json_encode_flags |= JSON_UNESCAPED_UNICODE;
}
wp_add_inline_script(
'jetpack-calendly-external-js',
sprintf(
$script,
wp_json_encode( esc_url_raw( $url ), $json_encode_flags ),
wp_json_encode( $block_id, $json_encode_flags )
)
);
}
return $content;
}
/**
* Get filtered attributes.
*
* @param array $attributes Array containing the Calendly 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(
'style' => 'inline',
'submitButtonText' => esc_html__( 'Schedule time with me', 'jetpack' ),
'backgroundColor' => 'ffffff',
'textColor' => '4D5055',
'primaryColor' => '00A2FF',
'hideEventTypeDetails' => false,
);
if ( isset( $default_attributes[ $attribute_name ] ) ) {
return $default_attributes[ $attribute_name ];
}
}
/**
* Enqueues the Calendly JS library and inline function to attach event
* handlers to the button.
*
* @return void
*/
function enqueue_calendly_js() {
wp_enqueue_script(
'jetpack-calendly-external-js',
'https://assets.calendly.com/assets/external/widget.js',
null,
JETPACK__VERSION,
false
);
wp_add_inline_script(
'jetpack-calendly-external-js',
"function jetpackInitCalendly( url, elementId ) {
function initCalendlyWidget() {
if ( ! document.getElementById( elementId ) ) {
return;
}
Calendly.initInlineWidget({
url: url,
parentElement: document.getElementById( elementId ),
inlineStyles: false,
});
};
// For P2s only: wait until after o2 has
// replaced main#content to initialize widget.
if ( window.jQuery && window.o2 ) {
jQuery( 'body' ).on( 'ready_o2', function() { initCalendlyWidget() } );
// Else initialize widget without waiting.
} else {
document.addEventListener('DOMContentLoaded', function() {
initCalendlyWidget();
});
}
};
function calendly_attach_link_events( elementId ) {
var widget = document.getElementById( elementId );
if ( widget ) {
widget.addEventListener( 'click', function( event ) {
event.preventDefault();
Calendly.initPopupWidget( { url: event.target.href } );
} );
widget.addEventListener( 'keydown', function( event ) {
// Enter and space keys.
if ( event.keyCode === 13 || event.keyCode === 32 ) {
event.preventDefault();
event.target && event.target.click();
}
} );
}
}"
);
}
/**
* Renders a deprecated legacy version of the button HTML.
*
* @param array $attributes Array containing the Calendly block attributes.
* @param string $block_id The value for the ID attribute of the link.
* @param string $classes The CSS classes for the wrapper div.
* @param string $url Calendly URL for the link HREF.
*
* @return string
*/
function deprecated_render_button_v1( $attributes, $block_id, $classes, $url ) {
// This is the legacy version, so create the full link content.
$submit_button_text = get_attribute( $attributes, 'submitButtonText' );
$submit_button_classes = get_attribute( $attributes, 'submitButtonClasses' );
$submit_button_text_color = get_attribute( $attributes, 'customTextButtonColor' );
$submit_button_background_color = get_attribute( $attributes, 'customBackgroundButtonColor' );
/*
* 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{%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 );
}
return sprintf(
'<div class="wp-block-button %1$s"><a id="%2$s" class="%3$s" href="%4$s" role="button">%5$s</a></div>',
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 )
);
}
@@ -0,0 +1,12 @@
<?php
/**
* Contact Form Block.
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Contact_Form;
add_action( 'init', array( Contact_Form_Block::class, 'register_block' ), 9 );
add_action( 'enqueue_block_editor_assets', array( Contact_Form_Block::class, 'load_editor_scripts' ), 9 );
@@ -0,0 +1,161 @@
<?php
/**
* Class Jetpack_Contact_Info_Block
*
* @package automattic/jetpack
*/
use Automattic\Jetpack\Blocks;
/**
* Helper class that lets us add schema attributes dynamically because they are not something that is store with the content.
* Due to the limitations of wp_kses.
*
* @since 7.1.0
*/
class Jetpack_Contact_Info_Block {
/**
* Registers the block for use in Gutenberg
* This is done via an action so that we can disable
* registration if we need to.
*/
public static function register_block() {
Blocks::jetpack_register_block(
__DIR__,
array(
'render_callback' => __NAMESPACE__ . '\render',
)
);
Blocks::jetpack_register_block(
'jetpack/address',
array(
'parent' => array( 'jetpack/contact-info' ),
'render_callback' => __NAMESPACE__ . '\render_adress',
)
);
Blocks::jetpack_register_block(
'jetpack/email',
array(
'parent' => array( 'jetpack/contact-info' ),
'render_callback' => __NAMESPACE__ . '\render_email',
)
);
Blocks::jetpack_register_block(
'jetpack/phone',
array(
'parent' => array( 'jetpack/contact-info' ),
'render_callback' => __NAMESPACE__ . '\render_phone',
)
);
}
/**
* Adds contact info schema attributes.
*
* @param array $attr Array containing the contact info block attributes.
* @param string $content String containing the contact info block content.
*
* @return string
*/
public static function render( $attr, $content ) {
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
return str_replace(
'class="wp-block-jetpack-contact-info', // Closing " intentionally ommited to that the user can also add the className as expected.
'itemprop="location" itemscope itemtype="http://schema.org/Organization" class="wp-block-jetpack-contact-info',
$content
);
}
/**
* Adds address schema attributes.
*
* @param array $attr Array containing the address block attributes.
* @param string $content String containing the address block content.
*
* @return string
*/
public static function render_address( $attr, $content ) {
// Returns empty content if the only attribute set is linkToGoogleMaps.
if ( ! self::has_attributes( $attr, array( 'linkToGoogleMaps', 'className' ) ) ) {
return '';
}
$find = array(
'class="wp-block-jetpack-address"',
'class="jetpack-address__address',
// Closing " left out on purpose - there are multiple address fields and they all need to be updated with the same itemprop.
'class="jetpack-address__region"',
'class="jetpack-address__city"',
'class="jetpack-address__postal"',
'class="jetpack-address__country"',
);
$replace = array(
'itemprop="address" itemscope itemtype="http://schema.org/PostalAddress" class="wp-block-jetpack-address" ',
'itemprop="streetAddress" class="jetpack-address__address', // Closing " left out on purpose.
'itemprop="addressRegion" class="jetpack-address__region"',
'itemprop="addressLocality" class="jetpack-address__city"',
'itemprop="postalCode" class="jetpack-address__postal"',
'itemprop="addressCountry" class="jetpack-address__country"',
);
return str_replace( $find, $replace, $content );
}
/**
* Helper function that lets us determine if a block has any valid attributes.
*
* @param array $attr Array containing the block attributes.
* @param array $omit Array containing the block attributes that we ignore.
*
* @return bool
*/
public static function has_attributes( $attr, $omit = array() ) {
foreach ( $attr as $attribute => $value ) {
if ( ! in_array( $attribute, $omit, true ) && ! empty( $value ) ) {
return true;
}
}
return false;
}
/**
* Adds email schema attributes.
*
* @param array $attr Array containing the email block attributes.
* @param string $content String containing the email block content.
*
* @return string
*/
public static function render_email( $attr, $content ) {
$content = self::has_attributes( $attr, array( 'className' ) ) ?
str_replace( 'href="mailto:', 'itemprop="email" href="mailto:', $content ) :
'';
return $content;
}
/**
* Adds phone schema attributes. Also wraps the tel link in a span so that
* it's recognized as a telephone number in Google's Structured Data.
*
* @param array $attr Array containing the phone block attributes.
* @param string $content String containing the phone block content.
*
* @return string
*/
public static function render_phone( $attr, $content ) {
if ( self::has_attributes( $attr, array( 'className' ) ) ) {
return str_replace(
array( '<a href="tel:', '</a>' ),
array( '<span itemprop="telephone"><a href="tel:', '</a></span>' ),
$content
);
}
return '';
}
}
@@ -0,0 +1,16 @@
<?php
/**
* Contact Info block and its child blocks.
*
* @since 7.1.0
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Contact_Info;
use Jetpack_Contact_Info_Block;
require_once __DIR__ . '/class-jetpack-contact-info-block.php';
add_action( 'init', array( Jetpack_Contact_Info_Block::class, 'register_block' ) );
@@ -0,0 +1,170 @@
<?php
/**
* Cookie-consent Block.
*
* @since 12.0
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\CookieConsent;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
const COOKIE_NAME = 'eucookielaw';
/**
* Should the block be registered?
* In wp-admin, we only want to show the block in the site editor.
*
* @since 12.0
*
* @return bool
*/
function should_register_block() {
global $pagenow;
// Always register the widget if we're on the front end
if ( ! is_admin() ) {
return true;
}
if ( is_admin() && $pagenow === 'site-editor.php' ) {
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 ( ! should_register_block() ) {
return;
}
Blocks::jetpack_register_block(
__DIR__,
array(
'render_callback' => __NAMESPACE__ . '\load_assets',
'attributes' => array(
'render_from_template' => array(
'default' => false,
'type' => 'boolean',
),
),
)
);
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* Cookie-consent block registration/dependency declaration.
*
* @param array $attr Array containing the Cookie-consent block attributes.
* @param string $content String containing the Cookie-consent block content.
*
* @return string
*/
function load_assets( $attr, $content ) {
// We want to bust the cache even if the cookie isn't set.
// This is needed for when the cookie expires,
// and we should send fresh HTML with the cookie block in it.
notify_batcache_that_content_changed();
if (
/**
* Filters the display of the Cookie-consent Block e.g by GDPR CMP banner on WordAds sites.
*
* @since 13.2
*
* @param bool $disable_cookie_consent_block Whether to disable the Cookie-consent Block.
*/
apply_filters( 'jetpack_disable_cookie_consent_block', false )
) {
return '';
}
// If the user has already accepted the cookie consent, don't show the block.
if ( isset( $_COOKIE[ COOKIE_NAME ] ) ) {
return '';
}
$option = get_option( 'cookie_consent_template' );
if ( ! empty( $option ) && empty( $attr['render_from_template'] ) ) {
return '';
}
/*
* Enqueue necessary scripts and styles.
*/
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
return sprintf(
'<div class="%1$s">%2$s</div>',
esc_attr( Blocks::classes( Blocks::get_block_feature( __DIR__ ), $attr ) ),
$content
);
}
/**
* Batcache busting: since the cookie consent is part of the cached response HTML, it can still render even when the cookie is set (when it shouldn't).
* Because, by default, the cache doesn't vary around the cookie's value. This makes the cookie value part of the cache key.
*
* See: https://github.com/Automattic/batcache/blob/d4f617b335e9772a61b6d03ad3498b55c8137592/advanced-cache.php#L29
*/
function notify_batcache_that_content_changed() {
if ( function_exists( 'vary_cache_on_function' ) ) {
vary_cache_on_function( 'return isset( $_COOKIE[ "' . COOKIE_NAME . '" ] );' );
}
}
/**
* Render the cookie consent template.
*
* @since 12.4
*/
function render_cookie_consent_template() {
if ( is_admin() ) {
return;
}
// Check whether block theme functions exist.
if ( ! function_exists( 'parse_blocks' ) ) {
return;
}
$template = get_option( 'cookie_consent_template' );
if ( empty( $template ) ) {
return;
}
$parsed = parse_blocks( $template );
if ( ! empty( $parsed[0] ) ) {
$parsed[0]['attrs']['render_from_template'] = true;
echo render_block( $parsed[0] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
}
add_action( 'wp_footer', __NAMESPACE__ . '\render_cookie_consent_template' );
/**
* Register cookie_consent_template setting
*
* @since 12.4
*/
function cookie_consent_register_settings() {
register_setting(
'general',
'cookie_consent_template',
array(
'type' => 'string',
'show_in_rest' => true,
)
);
}
add_action( 'rest_api_init', __NAMESPACE__ . '\cookie_consent_register_settings' );
@@ -0,0 +1,280 @@
<?php
/**
* Donations Block.
*
* @since 8.x
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Donations;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
use WP_Post;
/**
* 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() {
require_once JETPACK__PLUGIN_DIR . '/modules/memberships/class-jetpack-memberships.php';
if ( \Jetpack_Memberships::should_enable_monetize_blocks_in_editor() ) {
Blocks::jetpack_register_block(
__DIR__,
array(
'render_callback' => __NAMESPACE__ . '\render_block',
)
);
}
}
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() ) {
$parsed = parse_blocks( $content );
if ( ! empty( $parsed[0] ) ) {
// Inject the link of the current post from the server side as the fallback link to make sure the donations block
// points to the correct post when it's inserted from the synced pattern (aka “My Pattern”).
$post_link = get_permalink();
$parsed[0]['attrs']['fallbackLinkUrl'] = $post_link;
$content = \render_block( $parsed[0] );
if ( preg_match( '/<a\s+class="jetpack-donations-fallback-link"\s+href="([^"]*)"/', $content, $matches ) ) {
$content = str_replace( $matches[1], $post_link, $content );
}
}
return $content;
}
require_once JETPACK__PLUGIN_DIR . 'modules/memberships/class-jetpack-memberships.php';
// If stripe isn't connected don't show anything to potential donors - they can't actually make a donation.
if ( ! \Jetpack_Memberships::has_connected_account() ) {
return '';
}
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
require_once JETPACK__PLUGIN_DIR . '/_inc/lib/class-jetpack-currencies.php';
$default_texts = get_default_texts();
$donations = array(
'one-time' => array_merge(
array(
'planId' => null,
'title' => __( 'One-Time', 'jetpack' ),
'class' => 'donations__one-time-item',
'heading' => $default_texts['oneTimeDonation']['heading'],
'buttonText' => $default_texts['oneTimeDonation']['buttonText'],
),
$attr['oneTimeDonation']
),
);
if ( $attr['monthlyDonation']['show'] ) {
$donations['1 month'] = array_merge(
array(
'planId' => null,
'title' => __( 'Monthly', 'jetpack' ),
'class' => 'donations__monthly-item',
'heading' => $default_texts['monthlyDonation']['heading'],
'buttonText' => $default_texts['monthlyDonation']['buttonText'],
),
$attr['monthlyDonation']
);
}
if ( $attr['annualDonation']['show'] ) {
$donations['1 year'] = array_merge(
array(
'planId' => null,
'title' => __( 'Yearly', 'jetpack' ),
'class' => 'donations__annual-item',
'heading' => $default_texts['annualDonation']['heading'],
'buttonText' => $default_texts['annualDonation']['buttonText'],
),
$attr['annualDonation']
);
}
$choose_amount_text = isset( $attr['chooseAmountText'] ) && ! empty( $attr['chooseAmountText'] ) ? $attr['chooseAmountText'] : $default_texts['chooseAmountText'];
$custom_amount_text = isset( $attr['customAmountText'] ) && ! empty( $attr['customAmountText'] ) ? $attr['customAmountText'] : $default_texts['customAmountText'];
$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 .= '<div class="donations__nav">';
}
$nav .= sprintf(
'<div role="button" tabindex="0" class="donations__nav-item" data-interval="%1$s">%2$s</div>',
esc_attr( $interval ),
esc_html( $donation['title'] )
);
}
$headings .= sprintf(
'<h4 class="%1$s">%2$s</h4>',
esc_attr( $donation['class'] ),
wp_kses_post( $donation['heading'] )
);
$amounts .= sprintf(
'<div class="donations__amounts %s">',
esc_attr( $donation['class'] )
);
foreach ( $donation['amounts'] as $amount ) {
$amounts .= sprintf(
'<div class="donations__amount" data-amount="%1$s">%2$s</div>',
esc_attr( $amount ),
esc_html( \Jetpack_Currencies::format_price( $amount, $currency ) )
);
}
$amounts .= '</div>';
$extra_text .= sprintf(
'<p class="%1$s">%2$s</p>',
esc_attr( $donation['class'] ),
wp_kses_post( $donation['extraText'] ?? $default_texts['extraText'] )
);
$buttons .= sprintf(
'<a class="wp-block-button__link donations__donate-button %1$s" href="%2$s">%3$s</a>',
esc_attr( $donation['class'] ),
esc_url( \Jetpack_Memberships::get_instance()->get_subscription_url( $plan_id ) ),
wp_kses_post( $donation['buttonText'] )
);
}
if ( $nav ) {
$nav .= '</div>';
}
$custom_amount = '';
if ( $attr['showCustomAmount'] ) {
$custom_amount .= sprintf(
'<p>%s</p>',
wp_kses_post( $custom_amount_text )
);
$default_custom_amount = ( \Jetpack_Memberships::SUPPORTED_CURRENCIES[ $currency ] ?? 1 ) * 100;
$custom_amount .= sprintf(
'<div class="donations__amount donations__custom-amount">
%1$s
<div class="donations__amount-value" data-currency="%2$s" data-empty-text="%3$s"></div>
</div>',
esc_html( \Jetpack_Currencies::CURRENCIES[ $currency ]['symbol'] ?? '¤' ),
esc_attr( $currency ),
esc_attr( \Jetpack_Currencies::format_price( $default_custom_amount, $currency, false ) )
);
}
return sprintf(
'
<div class="%1$s">
<div class="donations__container">
%2$s
<div class="donations__content">
<div class="donations__tab">
%3$s
<p>%4$s</p>
%5$s
%6$s
<hr class="donations__separator">
%7$s
%8$s
</div>
</div>
</div>
</div>
',
esc_attr( Blocks::classes( Blocks::get_block_feature( __DIR__ ), $attr ) ),
$nav,
$headings,
$choose_amount_text,
$amounts,
$custom_amount,
$extra_text,
$buttons
);
}
/**
* Get the default texts for the block.
*
* @return array
*/
function get_default_texts() {
return array(
'chooseAmountText' => __( 'Choose an amount', 'jetpack' ),
'customAmountText' => __( 'Or enter a custom amount', 'jetpack' ),
'extraText' => __( 'Your contribution is appreciated.', 'jetpack' ),
'oneTimeDonation' => array(
'heading' => __( 'Make a one-time donation', 'jetpack' ),
'buttonText' => __( 'Donate', 'jetpack' ),
),
'monthlyDonation' => array(
'heading' => __( 'Make a monthly donation', 'jetpack' ),
'buttonText' => __( 'Donate monthly', 'jetpack' ),
),
'annualDonation' => array(
'heading' => __( 'Make a yearly donation', 'jetpack' ),
'buttonText' => __( 'Donate yearly', 'jetpack' ),
),
);
}
/**
* Make default texts available to the editor.
*/
function load_editor_scripts() {
// Only relevant to the editor right now.
if ( ! is_admin() ) {
return;
}
$data = array(
'defaultTexts' => get_default_texts(),
);
wp_add_inline_script(
'jetpack-blocks-editor',
'var Jetpack_DonationsBlock = ' . wp_json_encode( $data, JSON_HEX_TAG | JSON_HEX_AMP ) . ';',
'before'
);
}
add_action( 'enqueue_block_assets', __NAMESPACE__ . '\load_editor_scripts' );
/**
* 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( Blocks::get_block_name( __DIR__ ), $post->post_content ) ) {
return true;
}
return $skip;
}
add_filter( 'amp_skip_post', __NAMESPACE__ . '\amp_skip_post', 10, 3 );
@@ -0,0 +1,259 @@
<?php
/**
* Eventbrite Block.
*
* @since 8.2.0
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Eventbrite;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
/**
* 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(
__DIR__,
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.
*
* @param array $attr Eventbrite block attributes.
* @param string $content Rendered embed element (without scripts) from the block editor.
*
* @return string Rendered block.
*/
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-' );
// 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 );
}
}
/**
* 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( Blocks::get_block_feature( __DIR__ ), $attr );
$classes .= ' wp-block-jetpack-eventbrite--embed';
$direct_link = sprintf(
'<a href="%s" rel="noopener noreferrer" target="_blank" class="eventbrite__direct-link" %s>%s</a>',
esc_url( $attr['url'] ),
$is_amp ? 'placeholder fallback' : '',
esc_html__( 'Register on Eventbrite', 'jetpack' )
);
if ( $is_amp ) {
$embed = sprintf(
'<amp-iframe src="%s" layout="responsive" resizable width="1" height="1" sandbox="allow-scripts allow-same-origin allow-forms"><button overflow>%s</button>%s</amp-iframe>',
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( __DIR__ );
wp_add_inline_script(
'eventbrite-widget',
"window.EBWidgets.createWidget( {
widgetType: 'checkout',
eventId: " . absint( $attr['eventId'] ) . ",
iframeContainerId: '" . esc_js( $widget_id ) . "',
} );"
);
}
return sprintf(
'<div id="%1$s" class="%2$s">%3$s</div>',
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( __DIR__ );
$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'
);
$lightbox = sprintf(
'<amp-lightbox id="%1$s" on="%2$s" class="eventbrite__lightbox" layout="nodisplay">%3$s</amp-lightbox>',
esc_attr( $lightbox_id ),
esc_attr( "tap:{$lightbox_id}.close" ),
sprintf(
'
<div class="eventbrite__lighbox-inside">
<div class="eventbrite__lighbox-iframe-wrapper">
<amp-iframe class="eventbrite__lighbox-iframe" src="%s" layout="fill" sandbox="allow-scripts allow-same-origin allow-forms">
<span placeholder=""></span>
</amp-iframe>
<span class="eventbrite__lighbox-close" on="%s" role="button" tabindex="0" aria-label="%s">
<svg viewBox="0 0 24 24">
<path d="M13.4 12l3.5-3.5-1.4-1.4-3.5 3.5-3.5-3.5-1.4 1.4 3.5 3.5-3.5 3.5 1.4 1.4 3.5-3.5 3.5 3.5 1.4-1.4z"></path>
</svg>
</span>
</div>
</div>
',
esc_url( $iframe_src ),
esc_attr( "tap:{$lightbox_id}.close" ),
esc_attr__( 'Close', 'jetpack' )
)
);
$content = preg_replace(
':(?=</div>\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',
"window.EBWidgets.createWidget( {
widgetType: 'checkout',
eventId: " . absint( $attr['eventId'] ) . ",
modal: true,
modalTriggerElementId: '" . esc_js( $widget_id ) . "',
} );"
);
// Modal button is saved as an `<a>` element with `role="button"` because `<button>` is not allowed
// by WordPress.com wp_kses. This javascript adds the necessary event handling for button-like behavior.
// @link https://www.w3.org/TR/wai-aria-practices/examples/button/button.html.
wp_add_inline_script(
'eventbrite-widget',
"( function() {
var widget = document.getElementById( '" . esc_js( $widget_id ) . "' );
if ( widget ) {
widget.addEventListener( 'click', function( event ) {
event.preventDefault();
} );
widget.addEventListener( 'keydown', function( event ) {
// Enter and space keys.
if ( event.keyCode === 13 || event.keyCode === 32 ) {
event.preventDefault();
event.target && event.target.click();
}
} );
}
} )();"
);
// Replace the placeholder id saved in the post_content with a unique id used by widget.js.
$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;
}
@@ -0,0 +1,78 @@
<?php
/**
* GIF Block.
*
* @since 7.0.0
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Gif;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
/**
* 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(
__DIR__,
array( 'render_callback' => __NAMESPACE__ . '\render_block' )
);
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* Gif block registration/dependency declaration.
*
* @param array $attr - Array containing the gif block attributes.
*
* @return string
*/
function render_block( $attr ) {
$padding_top = isset( $attr['paddingTop'] ) ? $attr['paddingTop'] : 0;
$style = 'padding-top:' . $padding_top;
$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;
if ( ! $giphy_url ) {
return null;
}
$classes = Blocks::classes( Blocks::get_block_feature( __DIR__ ), $attr );
$placeholder = sprintf( '<a href="%s">%s</a>', esc_url( $giphy_url ), esc_attr( $search_text ) );
ob_start();
?>
<div class="<?php echo esc_attr( $classes ); ?>">
<figure>
<?php if ( Blocks::is_amp_request() ) : ?>
<amp-iframe src="<?php echo esc_url( $giphy_url ); ?>" width="100" height="<?php echo absint( $padding_top ); ?>" sandbox="allow-scripts allow-same-origin" layout="responsive">
<div placeholder>
<?php echo wp_kses_post( $placeholder ); ?>
</div>
</amp-iframe>
<?php else : ?>
<div class="wp-block-jetpack-gif-wrapper" style="<?php echo esc_attr( $style ); ?>">
<iframe src="<?php echo esc_url( $giphy_url ); ?>" title="<?php echo esc_attr( $search_text ); ?>"></iframe>
</div>
<?php endif; ?>
<?php if ( $caption ) : ?>
<figcaption class="wp-block-jetpack-gif-caption gallery-caption"><?php echo wp_kses_post( $caption ); ?></figcaption>
<?php endif; ?>
</figure>
</div>
<?php
$html = ob_get_clean();
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
return $html;
}
@@ -0,0 +1,61 @@
<?php
/**
* Goodreads Block.
*
* @since 13.2
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Goodreads;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
/**
* 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(
__DIR__,
array( 'render_callback' => __NAMESPACE__ . '\load_assets' )
);
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* Goodreads block registration/dependency declaration.
*
* @param array $attr Array containing the Goodreads block attributes.
*
* @return string
*/
function load_assets( $attr ) {
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
if ( isset( $attr['id'] ) ) {
if ( isset( $attr['link'] ) ) {
wp_enqueue_script(
'jetpack-goodreads-' . esc_attr( $attr['id'] ),
esc_url_raw( $attr['link'] ),
array(),
JETPACK__VERSION,
true
);
}
$id = esc_attr( $attr['id'] );
} else {
$id = '';
}
$classes = esc_attr( Blocks::classes( Blocks::get_block_feature( __DIR__ ), $attr ) );
return sprintf(
'<div id="%1$s" class="%2$s"></div>',
$id,
$classes
);
}
@@ -0,0 +1,83 @@
<?php
/**
* Google Calendar Block.
*
* @since 8.3.0
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Google_Calendar;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
/**
* 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(
__DIR__,
array(
'render_callback' => __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( Blocks::get_block_feature( __DIR__ ), $attr );
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
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(
'<amp-iframe src="%1$s" frameborder="0" scrolling="no" height="%2$d" layout="fixed-height" sandbox="%3$s">%4$s%5$s</amp-iframe>',
esc_url( $url ),
absint( $height ),
esc_attr( $sandbox ),
sprintf(
'<a href="%s" placeholder>%s</a>',
esc_url( $url ),
esc_html__( 'Google Calendar', 'jetpack' )
),
sprintf(
'<noscript><iframe src="%1$s" frameborder="0" scrolling="no" sandbox="%2$s"></iframe></noscript>',
esc_url( $noscript_src ),
esc_attr( $sandbox )
)
);
} else {
$iframe = sprintf(
'<iframe src="%1$s" frameborder="0" style="border:0" scrolling="no" height="%2$d" width="100%%" sandbox="%3$s"></iframe>',
esc_url( $url ),
absint( $height ),
esc_attr( $sandbox )
);
}
return sprintf( '<div class="%s">%s</div>', esc_attr( $classes ), $iframe );
}
@@ -0,0 +1,149 @@
<?php
/**
* GSuite Block.
*
* @since 11.3
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\GoogleDocsEmbed;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
/**
* Registers the blocks for use in Gutenberg
* This is done via an action so that we can disable
* registration if we need to.
*/
function register_blocks() {
Blocks::jetpack_register_block(
__DIR__,
array(
'render_callback' => __NAMESPACE__ . '\render_callback',
)
);
}
add_action( 'init', __NAMESPACE__ . '\register_blocks' );
/**
* The block rendering callback.
*
* @param array $attributes attributes.
* @return string
*/
function render_callback( $attributes ) {
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
wp_localize_script(
'jetpack-block-' . sanitize_title_with_dashes( Blocks::get_block_feature( __DIR__ ) ),
'Jetpack_Google_Docs',
array(
'error_msg' => __( 'This document is private. To view the document, login to a Google account that the document has been shared with and then refresh this page.', 'jetpack' ),
)
);
$url = empty( $attributes['url'] ) ? '' : map_gsuite_url( $attributes['url'] );
$aspect_ratio = empty( $attributes['aspectRatio'] ) ? '' : $attributes['aspectRatio'];
switch ( $attributes['variation'] ) {
case 'google-docs':
default:
$pattern = '/^http[s]?:\/\/((?:www\.)?docs\.google\.com(?:.*)?(?:document)\/[a-z0-9\/\?=_\-\.\,&%$#\@\!\+]*)\/preview/i';
break;
case 'google-sheets':
$pattern = '/^http[s]?:\/\/((?:www\.)?docs\.google\.com(?:.*)?(?:spreadsheets)\/[a-z0-9\/\?=_\-\.\,&%$#\@\!\+]*)\/preview/i';
break;
case 'google-slides':
$pattern = '/^http[s]?:\/\/((?:www\.)?docs\.google\.com(?:.*)?(?:presentation)\/[a-z0-9\/\?=_\-\.\,&%$#\@\!\+]*)\/preview/i';
break;
}
if ( empty( $attributes['url'] ) ) {
return '';
}
if ( $pattern && ! preg_match( $pattern, $url ) ) {
return '';
}
// Add loader for Google Document/Spreadsheets/Presentation blocks.
$iframe_markup = '<iframe src="' . esc_url( $url ) . '" allowFullScreen frameborder="0" title="' . esc_html__( 'Google Document Embed', 'jetpack' ) . '" height="450"></iframe>';
$loading_markup = '';
$amp_markup = '';
if (
str_contains( $url, '/document/d/' ) ||
str_contains( $url, '/spreadsheets/d/' ) ||
str_contains( $url, '/presentation/d/' )
) {
if ( function_exists( 'amp_is_request' ) && amp_is_request() ) {
$type = str_contains( $url, '/document/d/' ) ? __( 'Google Docs', 'jetpack' ) : '';
$type = empty( $type ) && str_contains( $url, '/spreadsheets/d/' ) ? __( 'Google Sheets', 'jetpack' ) : $type;
$type = empty( $type ) && str_contains( $url, '/presentation/d/' ) ? __( 'Google Slides', 'jetpack' ) : $type;
$iframe_markup = '';
$amp_markup_message = sprintf(
/* translators: Placeholder is a google product, eg. Google Docs, Google Sheets, or Google Slides. */
__( 'Tap to open embedded document in %s.', 'jetpack' ),
esc_html( $type )
);
$amp_markup = sprintf(
'<p class="wp-block-jetpack-google-docs-embed__error-msg"><a target="_blank" rel="noopener noreferrer" href="%s">%s</a></p>',
esc_url( $url ),
$amp_markup_message
);
} else {
$loading_markup = '<div class="loader is-active"><span>' . esc_html__( 'Loading...', 'jetpack' ) . '</span></div>';
}
}
$block_classes = Blocks::classes( Blocks::get_block_feature( __DIR__ ), $attributes, array( $aspect_ratio ) );
$html =
'<figure class="' . esc_attr( $block_classes ) . '">' .
'<div class="wp-block-jetpack-google-docs-embed__wrapper">' .
$loading_markup .
$amp_markup .
$iframe_markup .
'</div>' .
'</figure>';
return $html;
}
/**
* Convert GSuite URL to a preview URL.
*
* @param string $url The URL of the published Doc/Spreadsheet/Presentation.
*
* @return string
*/
function map_gsuite_url( $url ) {
// Default regex for all the URLs.
$gsuite_regex = '/^(http|https):\/\/(docs\.google.com)\/(spreadsheets|document|presentation)\/d\/([A-Za-z0-9_-]+).*?$/i';
/**
* Check if the URL is valid.
*
* If not, return the original URL as is.
*/
preg_match( $gsuite_regex, $url, $matches );
if (
empty( $matches ) ||
empty( $matches[1] ) ||
empty( $matches[2] ) ||
empty( $matches[3] ) ||
empty( $matches[4] )
) {
return $url;
}
return "{$matches[1]}://$matches[2]/$matches[3]/d/$matches[4]/preview";
}
@@ -0,0 +1,87 @@
<?php
/**
* Image Compare Block.
*
* @since 8.6
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\ImageCompare;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
/**
* 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(
__DIR__,
array( 'render_callback' => __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( __DIR__ );
wp_localize_script(
'jetpack-block-' . sanitize_title_with_dashes( Blocks::get_block_feature( __DIR__ ) ),
'imageCompareHandle',
array(
'msg' => __( 'Slide to compare images', 'jetpack' ),
)
);
if ( Blocks::is_amp_request() ) {
$content = preg_replace(
'#<div class="juxtapose".+?</div>#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(
'<amp-image-slider layout="responsive" width="%1$s" height="%2$s"> <amp-img id="%3$d" src="%4$s" alt="%5$s" layout="fill"></amp-img> <amp-img id="%6$d" src="%7$s" alt="%8$s" layout="fill"></amp-img></amp-image-slider>',
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'] )
);
}
@@ -0,0 +1,149 @@
<?php
/**
* Instagram Gallery Block.
*
* @since 8.5.0
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Instagram_Gallery;
use Automattic\Jetpack\Blocks;
use Jetpack;
use Jetpack_Gutenberg;
use Jetpack_Instagram_Gallery_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() {
if ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) || Jetpack::is_connection_ready() ) {
Blocks::jetpack_register_block(
__DIR__,
array( 'render_callback' => __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(
Blocks::get_block_feature( __DIR__ ),
$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' ) ) {
require_once JETPACK__PLUGIN_DIR . '/_inc/lib/class-jetpack-instagram-gallery-helper.php';
}
$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
. '<br />'
. esc_html__( '(Only administrators and the post author will see this message.)', 'jetpack' );
return Jetpack_Gutenberg::notice( $message, 'error', Blocks::classes( Blocks::get_block_feature( __DIR__ ), $attributes ) );
}
if ( empty( $gallery->images ) ) {
return '';
}
$images = array_slice( $gallery->images, 0, $count );
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
ob_start();
?>
<?php if ( Blocks::is_amp_request() ) : ?>
<style>
.wp-block-jetpack-instagram-gallery__grid .wp-block-jetpack-instagram-gallery__grid-post amp-img img {
object-fit: cover;
}
</style>
<?php endif; ?>
<div class="<?php echo esc_attr( $grid_classes ); ?>" style="<?php echo esc_attr( $grid_style ); ?>">
<?php foreach ( $images as $image ) : ?>
<a
class="wp-block-jetpack-instagram-gallery__grid-post"
href="<?php echo esc_url( $image->link ); ?>"
rel="noopener noreferrer"
target="_blank"
>
<img
alt="<?php echo esc_attr( $image->title ? $image->title : $image->link ); ?>"
src="<?php echo esc_url( $image->url ); ?>"
loading="lazy"
/>
</a>
<?php endforeach; ?>
</div>
<?php
return ob_get_clean();
}
/**
* Get Instagram Gallery block attribute.
*
* @param string $attribute String containing the attribute name to get.
* @param array $attributes Array containing the Instagram Gallery block attributes.
*
* @return mixed
*/
function get_instagram_gallery_attribute( $attribute, $attributes ) {
if ( array_key_exists( $attribute, $attributes ) ) {
return $attributes[ $attribute ];
}
$default_attributes = array(
'columns' => 3,
'count' => 9,
'isStackedOnMobile' => true,
'spacing' => 10,
);
if ( array_key_exists( $attribute, $default_attributes ) ) {
return $default_attributes[ $attribute ];
}
}
@@ -0,0 +1,160 @@
<?php
/**
* Like Block.
*
* @since 12.9
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Like;
use Automattic\Jetpack\Assets;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
/**
* 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() {
$is_wpcom = defined( 'IS_WPCOM' ) && IS_WPCOM;
Blocks::jetpack_register_block(
__DIR__,
array(
'api_version' => 3,
'render_callback' => __NAMESPACE__ . '\render_block',
'description' => $is_wpcom ? __( 'Give your readers the ability to show appreciation for your posts and easily share them with others.', 'jetpack' ) : __( 'Give your readers the ability to show appreciation for your posts.', 'jetpack' ),
)
);
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* Like block render function.
*
* @param array $attr Array containing the Like block attributes.
* @param string $content String containing the Like block content.
* @param object $block Object containing the Like block data.
*
* @return string
*/
function render_block( $attr, $content, $block ) {
// Do not render the Like block in other context than front-end (i.e. feed, emails, API, etc.).
if ( ! jetpack_is_frontend() ) {
return;
}
/*
* Enqueue necessary scripts and styles.
*/
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
$html = '';
$uniqid = uniqid();
$post_id = $block->context['postId'] ?? null;
$title = esc_html__( 'Like or Reblog', 'jetpack' );
if ( ! $post_id ) {
return;
}
/**
* Enable an alternate Likes layout.
*
* @since 12.9
*
* @module likes
*
* @param bool $new_layout Enable the new Likes layout. False by default.
*/
$new_layout = apply_filters( 'likes_new_layout', true ) ? '&amp;n=1' : '';
add_action( 'wp_footer', __NAMESPACE__ . '\render_iframe', 25 );
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$style_url = content_url( 'mu-plugins/likes/jetpack-likes.css' );
$script_url = content_url( 'mu-plugins/likes/queuehandler.js' );
} else {
require_once JETPACK__PLUGIN_DIR . 'modules/likes.php';
$style_url = plugins_url( 'modules/likes/style.css', dirname( __DIR__, 2 ) );
$script_url = Assets::get_file_url_for_environment(
'_inc/build/likes/queuehandler.min.js',
'modules/likes/queuehandler.js'
);
}
wp_enqueue_script( 'jetpack_likes_queuehandler', $script_url, array(), JETPACK__VERSION, true );
wp_enqueue_style( 'jetpack_likes', $style_url, array(), JETPACK__VERSION );
$show_reblog_button = $attr['showReblogButton'] ?? false;
$show_avatars = $attr['showAvatars'] ?? true;
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$blog_id = get_current_blog_id();
$bloginfo = get_blog_details( (int) $blog_id );
$domain = $bloginfo->domain;
$reblog_param = $show_reblog_button ? '&amp;reblog=1' : '';
$show_avatars_param = $show_avatars ? '' : '&amp;slim=1';
$src = sprintf( '//widgets.wp.com/likes/index.html?ver=%1$s#blog_id=%2$d&amp;post_id=%3$d&amp;origin=%4$s&amp;obj_id=%2$d-%3$d-%5$s%6$s&amp;block=1%7$s%8$s', rawurlencode( JETPACK__VERSION ), $blog_id, $post_id, $domain, $uniqid, $new_layout, $reblog_param, $show_avatars_param );
// provide the mapped domain when needed
if ( isset( $_SERVER['HTTP_HOST'] ) && strpos( sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ), '.wordpress.com' ) === false ) {
$sanitized_host = filter_var( wp_unslash( $_SERVER['HTTP_HOST'] ), FILTER_SANITIZE_URL );
$src .= '&amp;domain=' . rawurlencode( $sanitized_host );
}
} else {
$blog_id = \Jetpack_Options::get_option( 'id' );
$url = home_url();
$url_parts = wp_parse_url( $url );
$domain = $url_parts['host'];
$show_avatars_param = $show_avatars ? '' : '&amp;slim=1';
$src = sprintf( 'https://widgets.wp.com/likes/?ver=%1$s#blog_id=%2$d&amp;post_id=%3$d&amp;origin=%4$s&amp;obj_id=%2$d-%3$d-%5$s%6$s&amp;block=1%7$s', rawurlencode( JETPACK__VERSION ), $blog_id, $post_id, $domain, $uniqid, $new_layout, $show_avatars_param );
}
$name = sprintf( 'like-post-frame-%1$d-%2$d-%3$s', $blog_id, $post_id, $uniqid );
$wrapper = sprintf( 'like-post-wrapper-%1$d-%2$d-%3$s', $blog_id, $post_id, $uniqid );
$html = "<div class='sharedaddy sd-block sd-like jetpack-likes-widget-wrapper jetpack-likes-widget-unloaded' id='" . esc_attr( $wrapper ) . "' data-src='" . esc_attr( $src ) . "' data-name='" . esc_attr( $name ) . "' data-title='" . esc_attr( $title ) . "'>"
. "<div class='likes-widget-placeholder post-likes-widget-placeholder' style='height: 55px;'><span class='button'><span>" . esc_html__( 'Like', 'jetpack' ) . "</span></span> <span class='loading'>" . esc_html__( 'Loading...', 'jetpack' ) . '</span></div>'
. "<span class='sd-text-color'></span><a class='sd-link-color'></a>"
. '</div>';
return sprintf(
'<div class="%1$s">%2$s</div>',
esc_attr( Blocks::classes( Blocks::get_block_feature( __DIR__ ), $attr ) ),
$html
);
}
/**
* Helper function to determine whether the Like module has been disabled
*/
function is_legacy_likes_disabled() {
$settings = new \Jetpack_Likes_Settings();
$is_wpcom = defined( 'IS_WPCOM' ) && IS_WPCOM;
$is_likes_module_inactive = ! \Jetpack::is_module_active( 'likes' );
$is_disabled_on_wpcom = $is_wpcom && get_option( 'disabled_likes' ) && get_option( 'disabled_reblogs' );
$is_disabled_on_non_wpcom = ! $is_wpcom && get_option( 'disabled_likes' );
return $is_likes_module_inactive || $is_disabled_on_wpcom || $is_disabled_on_non_wpcom || ! $settings->is_likes_module_enabled();
}
/**
* Renders the iframe and enqueues the necessary scripts.
*/
function render_iframe() {
static $main_iframe_added = false;
if ( ! $main_iframe_added ) {
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
// @phan-suppress-next-line PhanUndeclaredStaticMethod -- Can't do a stub for this one since Jetpack has its own class with the same name.
\Jetpack_Likes::likes_master();
} else {
require_once JETPACK__PLUGIN_DIR . 'modules/likes.php';
jetpack_likes_master_iframe();
}
$main_iframe_added = true;
}
}
@@ -0,0 +1,262 @@
<?php
/**
* Mailchimp Block.
*
* @since 7.1.0
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Mailchimp;
use Automattic\Jetpack\Blocks;
use Jetpack;
use Jetpack_Gutenberg;
use Jetpack_Options;
/**
* 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(
__DIR__,
array(
'render_callback' => __NAMESPACE__ . '\load_assets',
)
);
}
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* Mailchimp block registration/dependency declaration.
*
* @param array $attr - Array containing the Mailchimp block attributes.
* @param string $content - Mailchimp block content.
*
* @return string
*/
function load_assets( $attr, $content ) {
if ( ! verify_connection() ) {
return null;
}
$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( __DIR__ );
$wrapper_attributes = \WP_Block_Supports::get_instance()->apply_block_supports();
$classes = ! empty( $wrapper_attributes['class'] ) ? $wrapper_attributes['class'] : '';
$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();
?>
<div class="<?php echo esc_attr( $classes ); ?>"<?php echo ! empty( $wrapper_attributes['style'] ) ? ' style="' . esc_attr( $wrapper_attributes['style'] ) . '"' : ''; ?> data-blog-id="<?php echo esc_attr( $blog_id ); ?>">
<form
aria-describedby="wp-block-jetpack-mailchimp_consent-text"
<?php if ( $is_amp_request ) : ?>
action-xhr="<?php echo esc_url( $amp_form_action ); ?>"
method="post"
id="mailchimp_form"
target="_top"
on="submit-success:AMP.setState( { mailing_list_status: 'subscribed', mailing_list_email: event.response.email } )"
<?php endif; ?>
>
<p>
<input
aria-label="<?php echo esc_attr( $values['emailPlaceholder'] ); ?>"
placeholder="<?php echo esc_attr( $values['emailPlaceholder'] ); ?>"
required
title="<?php echo esc_attr( $values['emailPlaceholder'] ); ?>"
type="email"
name="email"
/>
</p>
<?php foreach ( is_array( $values['interests'] ) ? $values['interests'] : array() as $interest ) : ?>
<input
name="interests[<?php echo esc_attr( $interest ); ?>]"
type="hidden"
class="mc-submit-param"
value="1"
/>
<?php endforeach; ?>
<?php
if (
! empty( $values['signupFieldTag'] )
&& ! empty( $values['signupFieldValue'] )
) :
?>
<input
name="merge_fields[<?php echo esc_attr( $values['signupFieldTag'] ); ?>]"
type="hidden"
class="mc-submit-param"
value="<?php echo esc_attr( $values['signupFieldValue'] ); ?>"
/>
<?php endif; ?>
<?php echo render_button( $attr, $content ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<p id="wp-block-jetpack-mailchimp_consent-text">
<?php echo wp_kses_post( $values['consentText'] ); ?>
</p>
<?php if ( $is_amp_request ) : ?>
<div submit-success>
<template type="amp-mustache">
<div class="wp-block-jetpack-mailchimp_notification wp-block-jetpack-mailchimp_success wp-block-jetpack-mailchimp__is-amp">
<?php echo esc_html( $values['successLabel'] ); ?>
</div>
</template>
</div>
<div submit-error>
<template type="amp-mustache">
<div class="wp-block-jetpack-mailchimp_notification wp-block-jetpack-mailchimp_error wp-block-jetpack-mailchimp__is-amp">
<?php echo esc_html( $values['errorLabel'] ); ?>
</div>
</template>
</div>
<div submitting>
<template type="amp-mustache">
<div class="wp-block-jetpack-mailchimp_notification wp-block-jetpack-mailchimp_processing wp-block-jetpack-mailchimp__is-amp" role="status">
<?php echo esc_html( $values['processingLabel'] ); ?>
</div>
</template>
</div>
<?php endif; ?>
</form>
<?php if ( ! $is_amp_request ) : ?>
<div class="wp-block-jetpack-mailchimp_notification wp-block-jetpack-mailchimp_processing" role="status">
<?php echo esc_html( $values['processingLabel'] ); ?>
</div>
<div class="wp-block-jetpack-mailchimp_notification wp-block-jetpack-mailchimp_success" role="status">
<?php echo esc_html( $values['successLabel'] ); ?>
</div>
<div class="wp-block-jetpack-mailchimp_notification wp-block-jetpack-mailchimp_error" role="alert">
<?php echo esc_html( $values['errorLabel'] ); ?>
</div>
<?php endif; ?>
</div>
<?php
$html = ob_get_clean();
return $html;
}
/**
* Mailchimp connection/list selection verification.
*
* @return boolean
*/
function verify_connection() {
$option = get_option( 'jetpack_mailchimp' );
if ( ! $option ) {
return false;
}
$data = json_decode( $option, true );
if ( ! $data ) {
return false;
}
return isset( $data['follower_list_id'] ) && isset( $data['keyring_id'] );
}
/**
* Builds complete set of attributes using default values where needed.
*
* @param array $attr Saved set of attributes for the Mailchimp block.
* @return array
*/
function get_attributes_with_defaults( $attr ) {
$values = array();
$defaults = array(
'emailPlaceholder' => esc_html__( 'Enter your email', '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' ),
'errorLabel' => esc_html__( 'Whoops! There was an error and we couldn\'t process your subscription. Please reload the page and try again.', 'jetpack' ),
'interests' => array(),
'signupFieldTag' => '',
'signupFieldValue' => '',
);
foreach ( $defaults as $id => $default ) {
$values[ $id ] = isset( $attr[ $id ] ) ? $attr[ $id ] : $default;
}
return $values;
}
/**
* 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,
sprintf(
'background-color: %s',
sanitize_hex_color( $attr['customBackgroundButtonColor'] )
)
);
}
if ( ! empty( $attr['customTextButtonColor'] ) ) {
array_push(
$button_styles,
sprintf(
'color: %s',
sanitize_hex_color( $attr['customTextButtonColor'] )
)
);
}
$button_styles = implode( ';', $button_styles );
$button_classes = 'components-button is-button is-primary ';
if ( ! empty( $attr['submitButtonClasses'] ) ) {
$button_classes .= $attr['submitButtonClasses'];
}
return sprintf(
'<p><button type="submit" class="%s" style="%s">%s</button></p>',
esc_attr( $button_classes ),
esc_attr( $button_styles ),
wp_kses_post( $text )
);
}
@@ -0,0 +1,239 @@
<?php
/**
* Map block.
*
* @since 6.8.0
*
* @package automattic/jetpack
*/
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;
if ( ! class_exists( 'Jetpack_Mapbox_Helper' ) ) {
require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-mapbox-helper.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(
__DIR__,
array(
'render_callback' => __NAMESPACE__ . '\load_assets',
)
);
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* Record a Tracks event every time the Map block is loaded on WordPress.com and Atomic.
*
* @param string $access_token_source The Mapbox API access token source.
*/
function wpcom_load_event( $access_token_source ) {
if ( 'wpcom' !== $access_token_source ) {
return;
}
$event_name = 'map_block_mapbox_wpcom_key_load';
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
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 );
}
}
/**
* Function to determine which map provider to choose
*
* @param array $html The block's HTML - needed for the class name.
*
* @return string The name of the map provider.
*/
function get_map_provider( $html ) {
$mapbox_styles = array( 'is-style-terrain' );
// return mapbox if html contains one of the mapbox styles
foreach ( $mapbox_styles as $style ) {
if ( str_contains( $html, $style ) ) {
return 'mapbox';
}
}
// you can override the map provider with a cookie
if ( isset( $_COOKIE['map_provider'] ) ) {
return sanitize_text_field( wp_unslash( $_COOKIE['map_provider'] ) );
}
// if we don't apply the filters & default to mapbox
return apply_filters( 'wpcom_map_block_map_provider', 'mapbox' );
}
/**
* Map block registration/dependency declaration.
*
* @param array $attr Array containing the map block attributes.
* @param string $content String containing the map block content.
*
* @return string
*/
function load_assets( $attr, $content ) {
$access_token = Jetpack_Mapbox_Helper::get_access_token();
wpcom_load_event( $access_token['source'] );
if ( Blocks::is_amp_request() ) {
static $map_block_counter = array();
$id = get_the_ID();
if ( ! isset( $map_block_counter[ $id ] ) ) {
$map_block_counter[ $id ] = 0;
}
++$map_block_counter[ $id ];
$iframe_url = add_query_arg(
array(
'map-block-counter' => absint( $map_block_counter[ $id ] ),
'map-block-post-id' => $id,
),
get_permalink()
);
$placeholder = preg_replace( '/(?<=<div\s)/', 'placeholder ', $content );
return sprintf(
'<amp-iframe src="%s" width="%d" height="%d" layout="responsive" allowfullscreen sandbox="allow-scripts">%s</amp-iframe>',
esc_url( $iframe_url ),
4,
3,
$placeholder
);
}
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
$map_provider = get_map_provider( $content );
if ( $map_provider === 'mapkit' ) {
return preg_replace( '/<div /', '<div data-map-provider="mapkit" data-api-key="' . esc_attr( $access_token['key'] ) . '" data-blog-id="' . \Jetpack_Options::get_option( 'id' ) . '" ', $content, 1 );
}
return preg_replace( '/<div /', '<div data-map-provider="mapbox" data-api-key="' . esc_attr( $access_token['key'] ) . '" ', $content, 1 );
}
/**
* Render a page containing only a single Map block.
*/
function render_single_block_page() {
// phpcs:ignore WordPress.Security.NonceVerification
$map_block_counter = isset( $_GET['map-block-counter'] ) ? absint( $_GET['map-block-counter'] ) : null;
// phpcs:ignore WordPress.Security.NonceVerification
$map_block_post_id = isset( $_GET['map-block-post-id'] ) ? absint( $_GET['map-block-post-id'] ) : null;
if ( ! $map_block_counter || ! $map_block_post_id ) {
return;
}
/* Create an array of all root-level DIVs that are Map Blocks */
$post = get_post( $map_block_post_id );
if ( ! class_exists( 'DOMDocument' ) ) {
return;
}
$post_html = new \DOMDocument();
/** This filter is already documented in core/wp-includes/post-template.php */
$content = apply_filters( 'the_content', $post->post_content );
/* Suppress warnings */
libxml_use_internal_errors( true );
@$post_html->loadHTML( $content ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
libxml_use_internal_errors( false );
$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 */
if ( ! $container ) {
return;
}
/* Compile scripts and styles */
ob_start();
add_filter( 'jetpack_is_amp_request', '__return_false' );
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
wp_scripts()->do_items();
wp_styles()->do_items();
add_filter( 'jetpack_is_amp_request', '__return_true' );
$head_content = ob_get_clean();
/* 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 );
$access_token = Jetpack_Mapbox_Helper::get_access_token();
$page_html = sprintf(
'<!DOCTYPE html><head><style>html, body { margin: 0; padding: 0; }</style>%s</head><body>%s</body>',
$head_content,
preg_replace( '/(?<=<div\s)/', 'data-api-key="' . esc_attr( $access_token['key'] ) . '" ', $block_markup, 1 )
);
echo $page_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
exit( 0 );
}
add_action( 'wp', __NAMESPACE__ . '\render_single_block_page' );
/**
* Helper function to generate the markup of the block in PHP.
*
* @param Array $points - Array containing geo location points.
*
* @return string Markup for the jetpack/map block.
*/
function map_block_from_geo_points( $points ) {
$map_block_data = array(
'points' => $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( '<li><a href="%s">%s</a></li>', esc_url( $link ), $point['title'] );
},
$points
);
$map_block = '<!-- wp:jetpack/map ' . wp_json_encode( $map_block_data ) . ' -->' . PHP_EOL;
$map_block .= sprintf(
'<div class="wp-block-jetpack-map" data-map-style="default" data-map-details="true" data-points="%1$s" data-zoom="%2$d" data-map-center="%3$s" data-marker-color="red" data-show-fullscreen-button="true">',
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 .= '<ul>' . implode( "\n", $list_items ) . '</ul>';
$map_block .= '</div>' . PHP_EOL;
$map_block .= '<!-- /wp:jetpack/map -->';
return $map_block;
}
@@ -0,0 +1,22 @@
<?php
/**
* Markdown Block.
*
* @since 6.8.0
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Markdown;
use Automattic\Jetpack\Blocks;
/**
* 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( __DIR__ );
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
@@ -0,0 +1,67 @@
<?php
/**
* Nextdoor Block.
*
* @since 12.8
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Nextdoor;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
/**
* 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(
__DIR__,
array( 'render_callback' => __NAMESPACE__ . '\load_assets' )
);
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* Nextdoor block registration/dependency declaration.
*
* @param array $attr Array containing the Nextdoor block attributes.
* @return string
*/
function load_assets( $attr ) {
if ( ! isset( $attr['url'] ) ) {
return;
}
$url = Jetpack_Gutenberg::validate_block_embed_url(
$attr['url'],
array( '/^http[s]?:\/\/((?:www\.)?nextdoor(?:.*)?\/(?:embed)\/\S*)/i' ),
true
);
if ( empty( $url ) ) {
return;
}
$url = preg_replace( '#/embed/#', '/p/', $url );
/*
* Enqueue necessary scripts and styles.
*/
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
$block_id = wp_unique_id( 'nextdoor-block-' );
$link_markup = '<a href="' . esc_url( $url ) . '" title="' . esc_html__( 'Nextdoor embed', 'jetpack' ) . '">' . esc_html( $url ) . '</a>';
$block_classes = Blocks::classes( Blocks::get_block_feature( __DIR__ ), $attr );
$html =
'<figure id="' . esc_attr( $block_id ) . '" class="' . esc_attr( $block_classes ) . '">' .
$link_markup .
'</figure>';
return $html;
}
@@ -0,0 +1,224 @@
<?php
/**
* OpenTable Block.
*
* @since 8.2
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\OpenTable;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
/**
* 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(
__DIR__,
array(
'render_callback' => __NAMESPACE__ . '\load_assets',
'plan_check' => true,
)
);
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* OpenTable block registration/dependency declaration.
*
* @param array $attributes Array containing the OpenTable block attributes.
*
* @return string
*/
function load_assets( $attributes ) {
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
$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 || ! str_contains( $class_name, 'is-style-' ) ) {
$classes[] = sprintf( 'is-style-%s', $style );
}
if ( array_key_exists( 'rid', $attributes ) && is_array( $attributes['rid'] ) && count( $attributes['rid'] ) > 1 ) {
$classes[] = 'is-multi';
}
if ( array_key_exists( 'negativeMargin', $attributes ) && $attributes['negativeMargin'] ) {
$classes[] = 'has-no-margin';
}
$classes = Blocks::classes( Blocks::get_block_feature( __DIR__ ), $attributes, $classes );
$content = '<div class="' . esc_attr( $classes ) . '">';
$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(
'<amp-iframe src="%s" layout="fill" sandbox="allow-scripts allow-forms allow-same-origin allow-popups">%s</amp-iframe>',
esc_url( $src ),
sprintf(
'<a placeholder href="%s">%s</a>',
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 .= '<script src="' . esc_url( $script_url ) . '"></script>';
}
$content .= '</div>';
return $content;
}
/**
* Get the a block attribute
*
* @param array $attributes Array of block attributes.
* @param string $attribute_name The attribute to get.
*
* @return string The filtered attribute
*/
function get_attribute( $attributes, $attribute_name ) {
if ( isset( $attributes[ $attribute_name ] ) ) {
if ( in_array( $attribute_name, array( 'iframe', 'newtab' ), true ) ) {
return $attributes[ $attribute_name ] ? 'true' : 'false';
}
return $attributes[ $attribute_name ];
}
$default_attributes = array(
'style' => 'standard',
'iframe' => 'true',
'domain' => 'com',
'lang' => 'en-US',
'newtab' => 'false',
);
return isset( $default_attributes[ $attribute_name ] ) ? $default_attributes[ $attribute_name ] : null;
}
/**
* Get the block type attribute
*
* @param array $attributes Array of block attributes.
*
* @return string The filtered attribute
*/
function get_type_attribute( $attributes ) {
if ( ! empty( $attributes['rid'] ) && is_countable( $attributes['rid'] ) && count( $attributes['rid'] ) > 1 ) {
return 'multi';
}
if ( empty( $attributes['style'] ) || 'button' !== $attributes['style'] ) {
return 'standard';
}
return 'button';
}
/**
* Get the block theme attribute
*
* OpenTable has a confusing mix of themes and types for the widget. A type
* can have a theme, but the button style can not have a theme. The other two
* types (multi and standard) can have one of the three themes.
*
* We have combined these into a `style` attribute as really there are 4 styles
* standard, wide, tall, and button. Multi can be determined by the number of
* restaurant IDs we have.
*
* This function along with `jetpack_opentable_block_get_type_attribute`, translates
* the style attribute to a type and theme.
*
* Type Theme Style
* ==========|==========|==========
* Multi | |
* Standard | Standard | Standard
* | Wide | Wide
* | Tall | Tall
* Button | Standard | Button
*
* @param array $attributes Array of block attributes.
*
* @return string The filtered attribute
*/
function get_theme_attribute( $attributes ) {
$valid_themes = array( 'standard', 'wide', 'tall' );
if ( empty( $attributes['style'] )
|| ! in_array( $attributes['style'], $valid_themes, true )
|| 'button' === $attributes['style'] ) {
return 'standard';
}
return $attributes['style'];
}
/**
* Build an embed URL from an array of block attributes.
*
* @param array $attributes Array of block attributess.
*
* @return string Embed URL
*/
function build_embed_url( $attributes ) {
$url = add_query_arg(
array(
'type' => get_type_attribute( $attributes ),
'theme' => get_theme_attribute( $attributes ),
'iframe' => get_attribute( $attributes, 'iframe' ),
'domain' => get_attribute( $attributes, 'domain' ),
'lang' => get_attribute( $attributes, 'lang' ),
'newtab' => get_attribute( $attributes, 'newtab' ),
),
'//www.opentable.com/widget/reservation/loader'
);
if ( ! empty( $attributes['rid'] ) ) {
foreach ( $attributes['rid'] as $rid ) {
$url .= '&rid=' . $rid;
}
}
/**
* Filter the OpenTable URL used to embed a widget.
*
* @since 8.2.0
*
* @param string $url OpenTable embed URL.
* @param array $attributes Array of block attributes.
*/
return apply_filters( 'jetpack_opentable_block_url', $url, $attributes );
}
@@ -0,0 +1,63 @@
<?php
/**
* Payment Buttons Block.
*
* @since 11.3
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\PaymentButtons;
use Automattic\Jetpack\Blocks;
/**
* 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() {
require_once JETPACK__PLUGIN_DIR . '/modules/memberships/class-jetpack-memberships.php';
if ( \Jetpack_Memberships::should_enable_monetize_blocks_in_editor() ) {
Blocks::jetpack_register_block(
__DIR__,
array(
'render_callback' => __NAMESPACE__ . '\render_block',
'supports' => array(
'layout' => array(
'allowSwitching' => false,
'allowInheriting' => false,
'default' => array(
'type' => 'flex',
),
),
),
)
);
} else {
$required_plan = ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ? 'personal-bundle' : 'jetpack_personal';
\Jetpack_Gutenberg::set_extension_unavailable(
'payment-buttons',
'missing_plan',
array(
'required_feature' => 'memberships',
'required_plan' => $required_plan,
)
);
}
}
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_assets_as_required( __DIR__ );
return $content;
}
@@ -0,0 +1,24 @@
<?php
/**
* Payments Intro Block.
*
* Acts as a menu for select payments blocks
*
* @since 10.x
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\PaymentsIntro;
use Automattic\Jetpack\Blocks;
/**
* 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( __DIR__ );
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
@@ -0,0 +1,66 @@
<?php
/**
* Paywall Block.
*
* @since 12.5
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Paywall;
use Automattic\Jetpack\Blocks;
const FEATURE_NAME = 'paywall';
const BLOCK_NAME = 'jetpack/' . FEATURE_NAME;
const BLOCK_HTML = '<!-- wp:' . BLOCK_NAME . ' /-->';
const THE_EXCERPT_BLOCK = '[[[[[' . BLOCK_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 ( ! \Jetpack::is_module_active( 'subscriptions' ) ) {
return;
}
if ( ! class_exists( '\Jetpack_Memberships' ) ) {
return;
}
Blocks::jetpack_register_block(
__DIR__,
array(
'render_callback' => __NAMESPACE__ . '\render_block',
)
);
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* Paywall block render callback.
*
* @return string
*/
function render_block() {
if ( doing_filter( 'get_the_excerpt' ) ) {
if ( \Jetpack_Memberships::user_can_view_post() ) {
return '';
}
return THE_EXCERPT_BLOCK;
}
return '';
}
/**
* Adds the Paywall block to excerpt allowed blocks.
*
* @param array $allowed_blocks The allowed blocks.
*
* @return array The allowed blocks.
*/
function excerpt_allowed_blocks( $allowed_blocks ) {
return array_merge( $allowed_blocks, array( Blocks::get_block_name( __DIR__ ) ) );
}
add_filter( 'excerpt_allowed_blocks', __NAMESPACE__ . '\excerpt_allowed_blocks' );
@@ -0,0 +1,248 @@
<?php
/**
* Pinterest Block.
*
* Note: this block is no longer available to be added to new posts.
* It is only kept available for existing posts with Pinterest blocks.
* You can still embed Pinterest content using the embed method provided by WordPress itself.
*
* @since 8.0.0
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Pinterest;
use Automattic\Jetpack\Blocks;
use WP_Error;
const URL_PATTERN = '#^https?://(?:www\.)?(?:[a-z]{2}\.)?pinterest\.[a-z.]+/pin/(?P<pin_id>[^/]+)/?#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.
* @return 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 ( str_starts_with( $path, '/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(
__DIR__,
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 <https://github.com/ampproject/amphtml/blob/b5dea36e0b8bd012585d50839766a084f99a3685/extensions/amp-pinterest/0.1/pin-widget.js#L106>.
$error = new WP_Error( 'missing_required_image', '', compact( 'pin_id' ) );
}
if ( $error ) {
set_transient( $transient_id, $error, HOUR_IN_SECONDS );
return $error;
} else {
$data = $body['data'][0];
set_transient( $transient_id, $data, MONTH_IN_SECONDS );
return $data;
}
}
/**
* Render a Pin using the amp-pinterest component.
*
* This does not render boards or user profiles.
*
* Since AMP components need to be statically sized to be valid (so as to avoid layout shifting), there are quite a few
* hard-coded numbers as taken from the CSS for the AMP component.
*
* @param array $attr Block attributes.
* @return string Markup for <amp-pinterest>.
*/
function render_amp_pin( $attr ) {
$info = null;
if ( preg_match( URL_PATTERN, $attr['url'], $matches ) ) {
$info = fetch_pin_info( $matches['pin_id'] );
}
if ( is_array( $info ) ) {
$image = $info['images']['237x'];
$title = isset( $info['rich_metadata']['title'] ) ? $info['rich_metadata']['title'] : null;
$description = isset( $info['rich_metadata']['description'] ) ? $info['rich_metadata']['description'] : null;
// This placeholder will appear while waiting for the amp-pinterest component to initialize (or if it fails to initialize due to JS being disabled).
$placeholder = sprintf(
// The AMP_Img_Sanitizer will convert his to <amp-img> while also supplying `noscript > img` as fallback when JS is disabled.
'<a href="%s" placeholder><img src="%s" alt="%s" layout="fill" object-fit="contain" object-position="top left"></a>',
esc_url( $attr['url'] ),
esc_url( $image['url'] ),
esc_attr( $title )
);
$amp_padding = 5; // See <https://github.com/ampproject/amphtml/blob/b5dea36e0b8bd012585d50839766a084f99a3685/extensions/amp-pinterest/0.1/amp-pinterest.css#L269>.
$amp_fixed_width = 237; // See <https://github.com/ampproject/amphtml/blob/b5dea36e0b8bd012585d50839766a084f99a3685/extensions/amp-pinterest/0.1/amp-pinterest.css#L270>.
$pin_info_height = 60; // Minimum Obtained by measuring the height of the .-amp-pinterest-embed-pin-text element.
// Add height based on how much description there is. There are roughly 30 characters on a line of description text.
$has_description = false;
if ( ! empty( $info['description'] ) ) {
$desc_padding_top = 5; // See <https://github.com/ampproject/amphtml/blob/b5dea36e0b8bd012585d50839766a084f99a3685/extensions/amp-pinterest/0.1/amp-pinterest.css#L342>.
$pin_info_height += $desc_padding_top;
// Trim whitespace on description if there is any left, use to calculate the likely rows of text.
$description = trim( $info['description'] );
if ( strlen( $description ) > 0 ) {
$has_description = true;
$desc_line_height = 17; // See <https://github.com/ampproject/amphtml/blob/b5dea36e0b8bd012585d50839766a084f99a3685/extensions/amp-pinterest/0.1/amp-pinterest.css#L341>.
$pin_info_height += ceil( strlen( $description ) / 30 ) * $desc_line_height;
}
}
if ( ! empty( $info['repin_count'] ) ) {
$pin_stats_height = 16; // See <https://github.com/ampproject/amphtml/blob/b5dea36e0b8bd012585d50839766a084f99a3685/extensions/amp-pinterest/0.1/amp-pinterest.css#L322>.
$pin_info_height += $pin_stats_height;
}
// When Pin description is empty, make sure title and description from rich metadata are supplied for accessibility and discoverability.
$title = $has_description ? '' : implode( "\n", array_filter( array( $title, $description ) ) );
$amp_pinterest = sprintf(
'<amp-pinterest style="%1$s" data-do="embedPin" data-url="%2$s" width="%3$d" height="%4$d" title="%5$s">%6$s</amp-pinterest>',
esc_attr( 'line-height:1.5; font-size:21px' ), // Override styles from theme due to precise height calculations above.
esc_url( $attr['url'] ),
$amp_fixed_width + ( $amp_padding * 2 ),
$image['height'] + $pin_info_height + ( $amp_padding * 2 ),
esc_attr( $title ),
$placeholder
);
} else {
// Fallback embed when info is not available.
$amp_pinterest = sprintf(
'<amp-pinterest data-do="embedPin" data-url="%1$s" width="%2$d" height="%3$d">%4$s</amp-pinterest>',
esc_url( $attr['url'] ),
450, // Fallback width.
750, // Fallback height.
sprintf(
'<a placeholder href="%s">%s</a>',
esc_url( $attr['url'] ),
esc_html( $attr['url'] )
)
);
}
return sprintf(
'<div class="wp-block-jetpack-pinterest">%s</div>',
$amp_pinterest
);
}
/**
* Pinterest block registration/dependency declaration.
*
* @param array $attr Array containing the Pinterest block attributes.
* @param string $content String containing the Pinterest block content.
*
* @return string
*/
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(
'
<div class="%1$s">
<a data-pin-do="%2$s" href="%3$s"></a>
</div>
',
esc_attr( Blocks::classes( Blocks::get_block_feature( __DIR__ ), $attr ) ),
esc_attr( $type ),
esc_url( $url )
);
}
}
@@ -0,0 +1,294 @@
<?php
/**
* Podcast Player Block.
*
* @since 8.4.0
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Podcast_Player;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
use Jetpack_Podcast_Helper;
if ( ! class_exists( 'Jetpack_Podcast_Helper' ) ) {
require_once JETPACK__PLUGIN_DIR . '/_inc/lib/class-jetpack-podcast-helper.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(
__DIR__,
array(
'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 '<p>' . esc_html( $message ) . '</p>';
}
/**
* 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 ( ! empty( $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' ) );
}
if ( is_wp_error( $player_data['tracks'] ) ) {
return render_error( $player_data['tracks']->get_error_message() );
}
// 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 );
$wrapper_attributes = \WP_Block_Supports::get_instance()->apply_block_supports();
$block_classname = Blocks::classes( Blocks::get_block_feature( __DIR__ ), $attributes, array( 'is-default' ) );
$is_amp = Blocks::is_amp_request();
ob_start();
?>
<div class="<?php echo esc_attr( $block_classname ); ?>"<?php echo ! empty( $wrapper_attributes['style'] ) ? ' style="' . esc_attr( $wrapper_attributes['style'] ) . '"' : ''; ?> id="<?php echo esc_attr( $instance_id ); ?>">
<section
class="jetpack-podcast-player <?php echo esc_attr( $player_classes_name ); ?>"
style="<?php echo esc_attr( $player_inline_style ); ?>"
>
<?php
render(
'podcast-header',
array_merge(
$player_props,
array(
'primary_colors' => $primary_colors,
'player_id' => $player_data['playerId'],
)
)
);
?>
<?php if ( count( $player_data['tracks'] ) > 1 ) : ?>
<ol class="jetpack-podcast-player__tracks">
<?php foreach ( $player_data['tracks'] as $track_index => $attachment ) : ?>
<?php
render(
'playlist-track',
array(
'is_active' => 0 === $track_index,
'attachment' => $attachment,
'primary_colors' => $primary_colors,
'secondary_colors' => $secondary_colors,
)
);
?>
<?php endforeach; ?>
</ol>
<?php endif; ?>
</section>
<?php if ( ! $is_amp ) : ?>
<script type="application/json"><?php echo wp_json_encode( $player_props ); ?></script>
<?php endif; ?>
</div>
<?php
/**
* Enqueue necessary scripts and styles.
*/
if ( ! $is_amp ) {
wp_enqueue_style( 'wp-mediaelement' );
}
Jetpack_Gutenberg::load_assets_as_required( __DIR__, array( 'mediaelement' ) );
return ob_get_clean();
}
/**
* Given the color name, block attributes and the CSS property,
* the function will return an array with the `class` and `style`
* HTML attributes to be used straight in the markup.
*
* @example
* $color = get_colors( 'secondary', $attributes, 'border-color'
* => 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.
*
* @html-template-var array $template_props
*
* @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 string|null HTML markup or null.
*/
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;
}
}
@@ -0,0 +1,46 @@
<?php
/**
* Podcast Title template.
*
* @html-template Automattic\Jetpack\Extensions\Podcast_Player\render
* @package automattic/jetpack
*/
// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable -- HTML template, let Phan handle it.
namespace Automattic\Jetpack\Extensions\Podcast_Player;
/**
* Template variables.
*
* @var array $template_props
*/
$track_title = $template_props['attachment']['title'];
$track_link = empty( $template_props['attachment']['link'] ) ? $template_props['attachment']['src'] : $template_props['attachment']['link'];
$track_duration = ! empty( $template_props['attachment']['duration'] ) ? $template_props['attachment']['duration'] : '';
$class = 'jetpack-podcast-player__track ' . $template_props['secondary_colors']['class'];
$style = $template_props['secondary_colors']['style'];
if ( $template_props['is_active'] ) {
$class = 'jetpack-podcast-player__track is-active ' . $template_props['primary_colors']['class'];
$style = $template_props['primary_colors']['style'];
}
?>
<li
class="<?php echo esc_attr( trim( $class ) ); ?>"
style="<?php echo esc_attr( $style ); ?>"
>
<a
class="jetpack-podcast-player__track-link jetpack-podcast-player__link"
href="<?php echo esc_url( $track_link ); ?>"
role="button"
<?php echo $template_props['is_active'] ? 'aria-current="track"' : ''; ?>
>
<span class="jetpack-podcast-player__track-status-icon"></span>
<span class="jetpack-podcast-player__track-title"><?php echo esc_html( $track_title ); ?></span>
<time class="jetpack-podcast-player__track-duration"><?php echo esc_html( $track_duration ); ?></time>
</a>
</li>
@@ -0,0 +1,66 @@
<?php
/**
* Podcast Header Title template.
*
* @html-template Automattic\Jetpack\Extensions\Podcast_Player\render
* @package automattic/jetpack
*/
// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable -- HTML template, let Phan handle it.
namespace Automattic\Jetpack\Extensions\Podcast_Player;
/**
* Template variables.
*
* @var string $template_props
*/
if ( ! isset( $template_props['title'] ) && empty( $template_props['track']['title'] ) ) {
return;
}
$track_link = empty( $template_props['track']['link'] ) ? $template_props['track']['src'] : $template_props['track']['link'];
?>
<h2 id="<?php echo esc_attr( $template_props['player_id'] ); ?>__title" class="jetpack-podcast-player__title">
<span
class="jetpack-podcast-player__current-track-title <?php echo esc_attr( $template_props['primary_colors']['class'] ); ?>"
<?php echo isset( $template_props['primary_colors']['style'] ) ? 'style="' . esc_attr( $template_props['primary_colors']['style'] ) . '"' : ''; ?>
>
<?php
echo esc_html( $template_props['track']['title'] );
if ( ! empty( $track_link ) ) :
// Prevent whitespace between title and link to cause a jump when JS kicks in.
// phpcs:disable Squiz.PHP.EmbeddedPhp.ContentAfterEnd
?><a
class="jetpack-podcast-player__track-title-link"
href="<?php echo esc_url( $track_link ); ?>"
target="_blank"
rel="noopener noreferrer nofollow"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M15.6 7.2H14v1.5h1.6c2 0 3.7 1.7 3.7 3.7s-1.7 3.7-3.7 3.7H14v1.5h1.6c2.8 0 5.2-2.3 5.2-5.2 0-2.9-2.3-5.2-5.2-5.2zM4.7 12.4c0-2 1.7-3.7 3.7-3.7H10V7.2H8.4c-2.9 0-5.2 2.3-5.2 5.2 0 2.9 2.3 5.2 5.2 5.2H10v-1.5H8.4c-2 0-3.7-1.7-3.7-3.7zm4.6.9h5.3v-1.5H9.3v1.5z" />
</svg>
</a>
<?php endif; // phpcs:enable ?>
</span>
<?php
// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable -- This file expects $template_props set outside the file.
if ( ! empty( $template_props['title'] ) ) :
?>
<span class="jetpack-podcast-player--visually-hidden"> - </span>
<?php
render(
'podcast-title',
array(
'title' => $template_props['title'],
'link' => $template_props['link'],
)
);
?>
<?php endif; ?>
</h2>
@@ -0,0 +1,70 @@
<?php
/**
* Podcast Header template.
*
* @html-template Automattic\Jetpack\Extensions\Podcast_Player\render
* @package automattic/jetpack
*/
// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable -- HTML template, let Phan handle it.
namespace Automattic\Jetpack\Extensions\Podcast_Player;
/**
* Template variables.
*
* @var array $template_props
*/
/**
* Block attributes.
*/
$attributes = (array) $template_props['attributes']; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
$show_cover_art = (bool) $attributes['showCoverArt'];
$show_episode_title = (bool) $attributes['showEpisodeTitle'];
$show_episode_description = (bool) $attributes['showEpisodeDescription'];
// Current track.
$tracks = $template_props['tracks'];
$track = ( is_array( $tracks ) && ! empty( $tracks ) ) ? $tracks[0] : array();
?>
<div class="jetpack-podcast-player__header">
<div class="jetpack-podcast-player__current-track-info">
<?php if ( $show_cover_art && isset( $template_props['cover'] ) ) : ?>
<div class="jetpack-podcast-player__cover">
<img class="jetpack-podcast-player__cover-image" src="<?php echo esc_url( $template_props['cover'] ); ?>" alt="" />
</div>
<?php endif; ?>
<?php
if ( $show_episode_title ) {
render(
'podcast-header-title',
array(
'player_id' => $template_props['player_id'],
'title' => $template_props['title'],
'link' => $template_props['link'],
'track' => $track,
'primary_colors' => $template_props['primary_colors'],
)
);
}
?>
</div>
<?php
if ( $show_episode_description && ! empty( $track ) && isset( $track['description'] ) ) :
?>
<div
id="<?php echo esc_attr( $template_props['player_id'] ); ?>__track-description"
class="jetpack-podcast-player__track-description"
>
<?php echo esc_html( $track['description'] ); ?>
</div>
<?php endif; ?>
<div class="jetpack-podcast-player__audio-player">
<div class="jetpack-podcast-player--audio-player-loading"></div>
</div>
</div>
@@ -0,0 +1,39 @@
<?php
/**
* Podcast Title template.
*
* @html-template Automattic\Jetpack\Extensions\Podcast_Player\render
* @package automattic/jetpack
*/
// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable -- HTML template, let Phan handle it.
namespace Automattic\Jetpack\Extensions\Podcast_Player;
/**
* Template variables.
*
* @var string $template_props
*/
if ( empty( $template_props['title'] ) ) {
return;
}
?>
<span class="jetpack-podcast-player__podcast-title">
<?php if ( ! empty( $template_props['link'] ) ) : ?>
<a
class="jetpack-podcast-player__link"
href="<?php echo esc_url( $template_props['link'] ); ?>"
target="_blank"
rel="noopener noreferrer nofollow"
>
<?php echo esc_html( $template_props['title'] ); ?>
</a>
<?php
else :
echo esc_html( $template_props['title'] );
endif;
?>
</span>
@@ -0,0 +1,161 @@
<?php
/**
* Determine access to premium content.
*
* @package Automattic\Jetpack\Extensions\Premium_Content
*/
namespace Automattic\Jetpack\Extensions\Premium_Content;
use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Abstract_Token_Subscription_Service;
use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\WPCOM_Online_Subscription_Service;
use Automattic\Jetpack\Status\Host;
require_once __DIR__ . '/subscription-service/include.php';
/**
* Determines if the memberships module is set up.
*
* @return bool Whether the memberships module is set up.
*/
function membership_checks() {
// If Jetpack is not yet configured, don't show anything ...
if ( ! class_exists( '\Jetpack_Memberships' ) ) {
return false;
}
// if stripe not connected don't show anything...
if ( ! \Jetpack_Memberships::has_connected_account() ) {
return false;
}
return true;
}
/**
* Determines if the site has a plan that supports the
* Premium Content block.
*
* @return bool
*/
function required_plan_checks() {
$availability = \Jetpack_Gutenberg::get_cached_availability();
$slug = 'premium-content/container';
return ( isset( $availability[ $slug ] ) && $availability[ $slug ]['available'] );
}
/**
* Determines if the block should be rendered. Returns true
* if the block passes all required checks, or if the user is
* an editor.
*
* @return bool Whether the block should be rendered.
*/
function pre_render_checks() {
return ( current_user_can_edit() || membership_checks() );
}
/**
* Determines whether the current user can edit.
*
* @return bool Whether the user can edit.
*/
function current_user_can_edit() {
$user = wp_get_current_user();
return 0 !== $user->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_ids = array();
if ( isset( $attributes['selectedPlanIds'] ) ) {
$selected_plan_ids = $attributes['selectedPlanIds'];
} elseif ( isset( $attributes['selectedPlanId'] ) ) {
$selected_plan_ids = array( $attributes['selectedPlanId'] );
}
if ( isset( $block ) && ! empty( $block->context['premium-content/planId'] ) ) {
$selected_plan_ids = array( $block->context['premium-content/planId'] );
} elseif ( isset( $block ) && ! empty( $block->context['premium-content/planIds'] ) ) {
$selected_plan_ids = $block->context['premium-content/planIds'];
}
if ( empty( $selected_plan_ids ) ) {
return false;
}
$can_view = false;
$paywall = subscription_service();
$access_level = Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS; // Only paid subscribers should be granted access to the premium content
$tier_ids = \Jetpack_Memberships::get_all_newsletter_plan_ids();
$tier_ids = array_intersect( $tier_ids, $selected_plan_ids );
if ( ! empty( $tier_ids ) ) {
// If the selected plan is a tier, we want to check directly if user has a higher "tier".
// This is to prevent situation where the user upgrades and lose access to premium-gated content
$subscriptions = array();
if ( ( new Host() )->is_wpcom_simple() && is_user_logged_in() ) {
$user_id = wp_get_current_user()->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(), $user_id, get_current_blog_id() );
// format the subscriptions so that they can be validated.
$subscriptions = WPCOM_Online_Subscription_Service::abbreviate_subscriptions( $subscriptions );
} else {
$token = $paywall->get_and_set_token_from_request();
$payload = $paywall->decode_token( $token );
$is_valid_token = ! empty( $payload );
if ( $is_valid_token ) {
$subscriptions = (array) $payload['subscriptions'];
}
}
foreach ( $tier_ids as $tier_id ) {
$can_view = ! $paywall->maybe_gate_access_for_user_if_tier( $tier_id, $subscriptions );
if ( $can_view ) {
break;
}
}
}
$non_tier_ids = array_diff( $selected_plan_ids, $tier_ids );
if ( ! $can_view ) {
// For selected plans that are not tiers, we want to check if the user has any of the selected plans.
$can_view = $paywall->visitor_can_view_content( $non_tier_ids, $access_level );
}
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;
}
@@ -0,0 +1,62 @@
<?php
/**
* Create legacy buttons markup.
*
* @package Automattic\Jetpack\Extensions\Premium_Content
*/
namespace Automattic\Jetpack\Extensions\Premium_Content;
/**
* Creates a subscribe/login buttons markup for legacy blocks.
*
* @param array $attributes Block attributes.
* @param string $content String containing the block content.
* @param object $block Legacy block.
*
* @return string Subscribe/login buttons markup.
*/
function create_legacy_buttons_markup( $attributes, $content, $block ) {
$button_styles = array();
if ( ! empty( $attributes['customBackgroundButtonColor'] ) ) {
array_push(
$button_styles,
sprintf(
'background-color: %s',
isset( $attributes['customBackgroundButtonColor'] ) ? sanitize_hex_color( $attributes['customBackgroundButtonColor'] ) : 'transparent'
)
);
}
if ( ! empty( $attributes['customTextButtonColor'] ) ) {
array_push(
$button_styles,
sprintf(
'color: %s',
isset( $attributes['customTextButtonColor'] ) ? sanitize_hex_color( $attributes['customTextButtonColor'] ) : 'inherit'
)
);
}
$button_styles = implode( ';', $button_styles );
$login_button = sprintf(
'<div class="wp-block-button"><a role="button" href="%1$s" class="%2$s" style="%3$s">%4$s</a></div>',
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 "<div class='wp-block-premium-content-logged-out-view__buttons'>{$subscribe_button}{$login_button}</div>";
}
@@ -0,0 +1,672 @@
<?php
/**
* A paywall that exchanges JWT tokens from WordPress.com to allow
* a current visitor to view content that has been deemed "Premium content".
*
* @package Automattic\Jetpack\Extensions\Premium_Content
*/
namespace Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service;
use Automattic\Jetpack\Extensions\Premium_Content\JWT;
use WP_Error;
use WP_Post;
use const Automattic\Jetpack\Extensions\Subscriptions\META_NAME_FOR_POST_TIER_ID_SETTINGS;
/**
* Class Abstract_Token_Subscription_Service
*
* @package Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service
*/
abstract class Abstract_Token_Subscription_Service implements Subscription_Service {
const JWT_AUTH_TOKEN_COOKIE_NAME = 'wp-jp-premium-content-session'; // wp prefix helps with skipping batcache
const DECODE_EXCEPTION_FEATURE = 'memberships';
const DECODE_EXCEPTION_MESSAGE = 'Problem decoding provided token';
const REST_URL_ORIGIN = 'https://subscribe.wordpress.com/';
const BLOG_SUB_ACTIVE = 'active';
const BLOG_SUB_PENDING = 'pending';
const POST_ACCESS_LEVEL_EVERYBODY = 'everybody';
const POST_ACCESS_LEVEL_SUBSCRIBERS = 'subscribers';
const POST_ACCESS_LEVEL_PAID_SUBSCRIBERS = 'paid_subscribers';
const POST_ACCESS_LEVEL_PAID_SUBSCRIBERS_ALL_TIERS = 'paid_subscribers_all_tiers';
/**
* An optional user_id to query against (omitting this will use either the token or current user id)
*
* @var int|null
*/
protected $user_id = null;
/**
* Constructor
*
* @param int|null $user_id An optional user_id to query subscriptions against. Uses token from request/cookie or logged-in user information if omitted.
*/
public function __construct( $user_id = null ) {
$this->user_id = $user_id;
}
/**
* Initialize the token subscription service.
*
* @inheritDoc
*/
public function initialize() {
$this->get_and_set_token_from_request();
}
/**
* Set the token from the Request to the cookie and retrieve the token.
*
* @return string|null
*/
public function get_and_set_token_from_request() {
// URL token always has a precedence, so it can overwrite the cookie when new data available.
$token = $this->token_from_request();
if ( null !== $token ) {
$this->set_token_cookie( $token );
return $token;
}
return $this->token_from_cookie();
}
/**
* Get the token payload .
*
* @return array
*/
public function get_token_payload() {
$token = $this->get_and_set_token_from_request();
if ( empty( $token ) ) {
return array();
}
$token_payload = $this->decode_token( $token );
if ( ! is_array( $token_payload ) ) {
return array();
}
return $token_payload;
}
/**
* Get a token property, otherwise return false.
*
* @param string $key the property name.
*
* @return mixed|false
*/
public function get_token_property( $key ) {
$token_payload = $this->get_token_payload();
if ( ! isset( $token_payload[ $key ] ) ) {
return false;
}
return $token_payload[ $key ];
}
/**
* 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.
* @param array $access_level Access level for content.
*
* @return bool Whether the user can view the content
*/
public function visitor_can_view_content( $valid_plan_ids, $access_level ) {
global $current_user;
$old_user = $current_user; // backup the current user so we can set the current user to the token user for paywall purposes
$payload = $this->get_token_payload();
$is_valid_token = ! empty( $payload );
if ( $is_valid_token && isset( $payload['user_id'] ) ) {
// set the current user to the payload's user id
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$current_user = get_user_by( 'id', $payload['user_id'] );
}
$is_blog_subscriber = false;
$is_paid_subscriber = false;
$subscriptions = array();
if ( $is_valid_token ) {
/**
* Allow access to the content if:
*
* Active: user has a valid subscription
*/
$is_blog_subscriber = in_array(
$payload['blog_sub'],
array(
self::BLOG_SUB_ACTIVE,
),
true
);
$subscriptions = (array) $payload['subscriptions'];
$is_paid_subscriber = static::validate_subscriptions( $valid_plan_ids, $subscriptions );
}
$has_access = $this->user_has_access( $access_level, $is_blog_subscriber, $is_paid_subscriber, get_the_ID(), $subscriptions );
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$current_user = $old_user;
return $has_access;
}
/**
* Retrieves the email of the currently authenticated subscriber.
*
* @return string The email address of the current user.
*/
public function get_subscriber_email() {
$email = $this->get_token_property( 'blog_subscriber' );
if ( empty( $email ) ) {
return '';
}
return $email;
}
/**
* Returns true if the current authenticated user is subscribed to the current site.
*
* @return boolean
*/
public function is_current_user_subscribed() {
return $this->get_token_property( 'blog_sub' ) === 'active';
}
/**
* Returns true if the current authenticated user has a pending subscription to the current site.
*
* @return bool
*/
abstract public function is_current_user_pending_subscriber(): bool;
/**
* Return if the user has access to the content depending on the access level and the user rights
*
* @param string $access_level Post or blog access level.
* @param bool $is_blog_subscriber Is user a subscriber of the blog.
* @param bool $is_paid_subscriber Is user a paid subscriber of the blog.
* @param int $post_id Post ID.
* @param array $user_abbreviated_subscriptions User subscription abbreviated.
*
* @return bool Whether the user has access to the content.
*/
protected function user_has_access( $access_level, $is_blog_subscriber, $is_paid_subscriber, $post_id, $user_abbreviated_subscriptions ) {
if ( is_user_logged_in() && current_user_can( 'edit_post', $post_id ) ) {
// Admin has access
$has_access = true;
} else {
switch ( $access_level ) {
case self::POST_ACCESS_LEVEL_EVERYBODY:
default:
$has_access = true;
break;
case self::POST_ACCESS_LEVEL_SUBSCRIBERS:
$has_access = $is_blog_subscriber || $is_paid_subscriber;
break;
case self::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS_ALL_TIERS:
$has_access = $is_paid_subscriber;
break;
case self::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS:
$has_access = $is_paid_subscriber &&
! $this->maybe_gate_access_for_user_if_post_tier( $post_id, $user_abbreviated_subscriptions );
break;
}
}
do_action( 'earn_user_has_access', $access_level, $has_access, $is_blog_subscriber, $is_paid_subscriber, $post_id );
return $has_access;
}
/**
* Check post access for tiers.
*
* @param int $post_id Current post id.
* @param array $user_abbreviated_subscriptions User subscription abbreviated.
*
* @return bool
*/
private function maybe_gate_access_for_user_if_post_tier( $post_id, $user_abbreviated_subscriptions ) {
$tier_id = intval(
get_post_meta( $post_id, META_NAME_FOR_POST_TIER_ID_SETTINGS, true )
);
if ( ! $tier_id ) {
return false;
}
return $this->maybe_gate_access_for_user_if_tier( $tier_id, $user_abbreviated_subscriptions );
}
/**
* Get all plans id that make access valid for a post with this tier id.
*
* @param int $tier_id Newsletter tier post ID.
*
* @return array|WP_Error
*/
public static function get_valid_plan_ids_for_tier( int $tier_id ) {
// Valid plans are:
// - monthly plan with ID $tier_id
// - yearly plan related to this $tier_id (in meta jetpack_memberships_tier)
// - monthly tiers with same currency and price same or higher than original tier
// - yearly plans that are more expensive than the yearly plan linked to the original tier
$valid_plan_ids = array();
$all_plans = \Jetpack_Memberships::get_all_plans();
// Let's get the current tier
$tier = null;
foreach ( $all_plans as $post ) {
if ( $post->ID === $tier_id ) {
$tier = $post;
break;
}
}
if ( $tier === null ) {
// We have an error
return new WP_Error( 'related-plan-not-found', 'The plan related to the tier cannot be found' );
}
$tier_price = self::find_metadata( $tier, 'jetpack_memberships_price' );
$tier_currency = self::find_metadata( $tier, 'jetpack_memberships_currency' );
$tier_product_id = self::find_metadata( $tier, 'jetpack_memberships_product_id' );
if ( $tier_price === null || $tier_currency === null || $tier_product_id === null ) {
// There is an issue with the meta
return new WP_Error( 'wrong-data-plan-not-found', 'The plan related to the tier is missing data' );
}
$valid_plan_ids[] = $tier_id;
$tier_price = floatval( $tier_price );
// At this point we know the post is
$annual_tier = null;
foreach ( $all_plans as $plan ) {
if ( intval( self::find_metadata( $plan, 'jetpack_memberships_tier' ) ) === $tier_id ) {
$annual_tier = $plan;
break;
}
}
$annual_tier_price = null;
if ( ! empty( $annual_tier ) ) {
$annual_tier_price = floatval( self::find_metadata( $annual_tier, 'jetpack_memberships_price' ) );
$valid_plan_ids[] = $annual_tier->ID;
}
foreach ( $all_plans as $post ) {
if ( in_array( $post->ID, $valid_plan_ids, true ) ) {
continue;
}
$plan_price = self::find_metadata( $post, 'jetpack_memberships_price' );
$plan_currency = self::find_metadata( $post, 'jetpack_memberships_currency' );
$plan_interval = self::find_metadata( $post, 'jetpack_memberships_interval' );
if ( $plan_price === null || $plan_currency === null || $plan_interval === null ) {
// There is an issue with the meta
continue;
}
$plan_price = floatval( $plan_price );
if ( $tier_currency !== $plan_currency ) {
// For now, we don't count if there are different currency (not sure how to convert price in a pure JP env)
continue;
}
if ( ( $plan_interval === '1 month' && $plan_price >= $tier_price ) ||
( $annual_tier_price !== null && $plan_interval === '1 year' && $plan_price >= $annual_tier_price )
) {
$valid_plan_ids [] = $post->ID;
}
}
return $valid_plan_ids;
}
/**
* Find metadata in post
*
* @param WP_Post|object $post Post.
* @param string $meta_key Meta to retrieve.
*
* @return mixed|null
*/
private static function find_metadata( $post, $meta_key ) {
if ( $post instanceof WP_Post ) {
return $post->{$meta_key};
}
foreach ( $post->metadata as $meta ) {
if ( $meta->key === $meta_key ) {
return $meta->value;
}
}
return null;
}
/**
* Check access for tier.
*
* @param int $tier_id Tier id.
* @param array $user_abbreviated_subscriptions User subscription abbreviated.
*
* @return bool
*/
public function maybe_gate_access_for_user_if_tier( $tier_id, $user_abbreviated_subscriptions ) {
$plan_ids = \Jetpack_Memberships::get_all_newsletter_plan_ids();
if ( ! in_array( $tier_id, $plan_ids, true ) ) {
// If the tier is not in the plans, we bail
return false;
}
// We now need the tier price and currency, and the same for the annual price (if available)
$all_plans = \Jetpack_Memberships::get_all_plans();
$tier = null;
foreach ( $all_plans as $post ) {
if ( $post->ID === $tier_id ) {
$tier = $post;
break;
}
}
if ( $tier === null ) {
return false;
}
$tier_price = self::find_metadata( $tier, 'jetpack_memberships_price' );
$tier_currency = self::find_metadata( $tier, 'jetpack_memberships_currency' );
$tier_product_id = self::find_metadata( $tier, 'jetpack_memberships_product_id' );
$annual_tier_price = $tier_price * 12;
if ( $tier_price === null || $tier_currency === null || $tier_product_id === null ) {
// There is an issue with the meta
return false;
}
$tier_price = floatval( $tier_price );
// At this point we know the post is
$annual_tier_id = null;
$annual_tier = null;
foreach ( $all_plans as $plan ) {
if ( intval( self::find_metadata( $plan, 'jetpack_memberships_tier' ) ) === $tier_id ) {
$annual_tier = $plan;
break;
}
}
$annual_tier_price = null;
if ( ! empty( $annual_tier ) ) {
$annual_tier_id = $annual_tier->ID;
$annual_tier_price = floatval( self::find_metadata( $annual_tier, 'jetpack_memberships_price' ) );
}
foreach ( $user_abbreviated_subscriptions as $subscription_plan_id => $details ) {
$details = (array) $details;
$end = is_int( $details['end_date'] ) ? $details['end_date'] : strtotime( $details['end_date'] );
if ( $end < time() ) {
// subscription not active anymore
continue;
}
$subscription_post = null;
foreach ( $all_plans as $plan ) {
if ( intval( self::find_metadata( $plan, 'jetpack_memberships_product_id' ) ) === intval( $subscription_plan_id ) ) {
$subscription_post = $plan;
break;
}
}
if ( empty( $subscription_post ) ) {
// No post linked to this plan
continue;
}
$subscription_post_id = $subscription_post->ID;
if ( $subscription_post_id === $tier_id || $subscription_post_id === $annual_tier_id ) {
// User is subscribed to the right tier
return false;
}
$subscription_price = self::find_metadata( $subscription_post, 'jetpack_memberships_price' );
$subscription_currency = self::find_metadata( $subscription_post, 'jetpack_memberships_currency' );
$subscription_interval = self::find_metadata( $subscription_post, 'jetpack_memberships_interval' );
if ( $subscription_price === null || $subscription_currency === null || $subscription_interval === null ) {
// There is an issue with the meta
continue;
}
$subscription_price = floatval( $subscription_price );
if ( $tier_currency !== $subscription_currency ) {
// For now, we don't count if there are different currency (not sure how to convert price in a pure JP env)
continue;
}
if ( ( $subscription_interval === '1 month' && $subscription_price >= $tier_price ) ||
( $annual_tier_price !== null && $subscription_interval === '1 year' && $subscription_price >= $annual_tier_price )
) {
// One subscription is more expensive than the minimum set by the post' selected tier
return false;
}
}
return true; // No user subscription is more expensive than the post's tier price...
}
/**
* Decode the given token.
*
* @param string $token Token to decode.
*
* @return array|false
*/
public function decode_token( $token ) {
if ( empty( $token ) ) {
return false;
}
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();
// 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', $permalink = null ) {
global $wp;
if ( empty( $permalink ) ) {
$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 ];
}
}
/**
* Check whether the JWT_TOKEN cookie is set
*
* @return bool
*/
public static function has_token_from_cookie() {
return isset( $_COOKIE[ self::JWT_AUTH_TOKEN_COOKIE_NAME ] ) && ! empty( $_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 ( defined( 'TESTING_IN_JETPACK' ) && TESTING_IN_JETPACK ) {
return;
}
if ( ! empty( $token ) && ! headers_sent() ) {
// phpcs:ignore Jetpack.Functions.SetCookie.FoundNonHTTPOnlyFalse
setcookie( self::JWT_AUTH_TOKEN_COOKIE_NAME, $token, strtotime( '+1 month' ), '/', '', is_ssl(), false );
}
}
/**
* Clear the auth cookie.
*/
public static function clear_token_cookie() {
if ( defined( 'TESTING_IN_JETPACK' ) && TESTING_IN_JETPACK ) {
return;
}
if ( ! self::has_token_from_cookie() ) {
return;
}
unset( $_COOKIE[ self::JWT_AUTH_TOKEN_COOKIE_NAME ] );
if ( ! headers_sent() ) {
// phpcs:ignore Jetpack.Functions.SetCookie.FoundNonHTTPOnlyFalse
setcookie( self::JWT_AUTH_TOKEN_COOKIE_NAME, '', 1, '/', '', is_ssl(), false );
}
}
/**
* 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'] ) && is_string( $_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 object[] $token_subscriptions : ID must exist in the provided <code>$valid_subscriptions</code> parameter.
* The provided end date needs to be greater than <code>now()</code>.
*
* @return bool
*/
public static function validate_subscriptions( array $valid_plan_ids, array $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( intval( $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 ) {
// The redirect url might have a part URL encoded but not the whole URL.
$redirect_url = rawurldecode( $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<int, array>
*/
public static function abbreviate_subscriptions( $subscriptions_from_bd ) {
if ( empty( $subscriptions_from_bd ) ) {
return array();
}
$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;
}
}
@@ -0,0 +1,65 @@
<?php
/**
* A paywall that exchanges JWT tokens from WordPress.com to allow
* a current visitor to view content that has been deemed "Premium content".
*
* @package Automattic\Jetpack\Extensions\Premium_Content
*/
namespace Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service;
use Automattic\Jetpack\Connection\Tokens;
use Automattic\Jetpack\Status\Host;
/**
* Class Jetpack_Token_Subscription_Service
*
* @package Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service
*/
class Jetpack_Token_Subscription_Service extends Abstract_Token_Subscription_Service {
/**
* Is the Jetpack_Options class available?
*
* @return bool Whether Jetpack_Options class exists.
*/
public static function available() {
return ( new Host() )->is_wpcom_simple() || class_exists( '\Jetpack_Options' );
}
/**
* Get the site ID.
*
* @return int The site ID.
*/
public function get_site_id() {
return \Jetpack_Options::get_option( 'id' );
}
/**
* Get the key.
*
* @return string The key.
*/
public function get_key() {
if ( ( new Host() )->is_wpcom_simple() ) {
// phpcs:ignore ImportDetection.Imports.RequireImports.Symbol
return defined( 'EARN_JWT_SIGNING_KEY' ) ? EARN_JWT_SIGNING_KEY : false;
}
$token = ( new Tokens() )->get_access_token();
if ( ! isset( $token->secret ) ) {
return false;
}
return $token->secret;
}
/**
* Returns true if the current authenticated user has a pending subscription to the current site.
*
* @return bool
*/
public function is_current_user_pending_subscriber(): bool {
return self::BLOG_SUB_PENDING === $this->get_token_property( 'blog_sub' );
}
}
@@ -0,0 +1,426 @@
<?php
/**
* JSON Web Token implementation, based on this spec:
* https://tools.ietf.org/html/rfc7519
*
* @package Automattic\Jetpack\Extensions\Premium_Content
*/
namespace Automattic\Jetpack\Extensions\Premium_Content;
use DateTime;
use DomainException;
use InvalidArgumentException;
use UnexpectedValueException;
/**
* JSON Web Token implementation, based on this spec:
* https://tools.ietf.org/html/rfc7519
*
* PHP version 5
*
* @category Authentication
* @package Authentication_JWT
* @author Neuman Vong <neuman@twilio.com>
* @author Anant Narayanan <anant@php.net>
* @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 = static::$timestamp === null ? 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_encode( static::json_encode( $header ) );
$segments[] = static::urlsafe_b64_encode( static::json_encode( $payload ) );
$signing_input = implode( '.', $segments );
$signature = static::sign( $signing_input, $key, $alg );
$segments[] = static::urlsafe_b64_encode( $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 ) {
$obj = json_decode( $input, false, 512, JSON_BIGINT_AS_STRING );
$errno = json_last_error();
if ( $errno ) {
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 ) {
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 never
*/
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
@@ -0,0 +1,69 @@
<?php
/**
* The environment does not have a subscription service available.
* This represents this scenario.
*
* @package Automattic\Jetpack\Extensions\Premium_Content
*/
namespace Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service;
use function site_url;
// phpcs:disable
/**
* Class Unconfigured_Subscription_Service
*
* @package Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service
*/
class Unconfigured_Subscription_Service implements Subscription_Service {
/**
* Is always available because it is the fallback.
*
* @inheritDoc
*/
public static function available() {
return true;
}
/**
* Function: initialize()
*
* @inheritDoc
*/
public function initialize() {
// noop.
}
/**
* No subscription service available, no users can see this content.
*
* @param array $valid_plan_ids .
* @param string $access_level .
*/
public function visitor_can_view_content( $valid_plan_ids, $access_level ) {
return false;
}
/**
* is the current user a pending subscriber for the current site?
*
* @return bool
*/
public function is_current_user_pending_subscriber(): bool
{
return false;
}
/**
* The current visitor would like to obtain access. Where do they go?
*
* @param string $mode .
*/
public function access_url( $mode = 'subscribe' ) {
return site_url();
}
}
// phpcs:enable
@@ -0,0 +1,66 @@
<?php
/**
* This subscription service is used when a subscriber is offline and a token is not available.
*
* @package Automattic\Jetpack\Extensions\Premium_Content
*/
namespace Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service;
use const Automattic\Jetpack\Extensions\Subscriptions\META_NAME_FOR_POST_LEVEL_ACCESS_SETTINGS;
/**
* Class WPCOM_Offline_Subscription_Service
* This subscription service is used when a subscriber is offline and a token is not available.
* This subscription service will be used when sending emails to subscribers
*
* @package Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service
*/
class WPCOM_Offline_Subscription_Service extends WPCOM_Online_Subscription_Service {
/**
* Is available()
*
* @return bool
*/
public static function available() {
// Return available if the user is logged in and we are on WPCOM.
return false;
}
/**
* Check if the subscriber can receive the newsletter.
* This is the only method where is user does not need to be logged in.
*
* @param int $user_id User id.
* @param int $post_id Post id.
*
* @return bool
* @throws \Exception Throws an exception when used outside of WPCOM.
*/
public function subscriber_can_receive_post_by_mail( $user_id, $post_id ) {
if ( 0 === $user_id || empty( $user_id ) ) {
// Email cannot be sent to non-users
return false;
}
$previous_user = wp_get_current_user();
wp_set_current_user( $user_id );
$access_level = get_post_meta( $post_id, META_NAME_FOR_POST_LEVEL_ACCESS_SETTINGS, true );
if ( ! $access_level || self::POST_ACCESS_LEVEL_EVERYBODY === $access_level ) {
// The post is not gated, we return early
return true;
}
$valid_plan_ids = \Jetpack_Memberships::get_all_newsletter_plan_ids();
$is_blog_subscriber = true; // it is a subscriber as this is used in async when lopping through subscribers...
$allowed = $this->user_can_view_content( $valid_plan_ids, $access_level, $is_blog_subscriber, $post_id );
wp_set_current_user( $previous_user->ID );
return $allowed;
}
}
@@ -0,0 +1,167 @@
<?php
/**
* This subscription service is used when a subscriber is online
*
* @package Automattic\Jetpack\Extensions\Premium_Content
*/
namespace Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service;
/**
* Class WPCOM_Offline_Subscription_Service
*
* @package Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service
*/
class WPCOM_Online_Subscription_Service extends Jetpack_Token_Subscription_Service {
/**
* Is available()
*
* @return bool
*/
public static function available() {
// Return available if the user is logged in and we are on WPCOM.
return defined( 'IS_WPCOM' ) && IS_WPCOM && is_user_logged_in();
}
/**
* Lookup users subscriptions for a site and determine if the user has a valid subscription to match the plan ID
*
* @param array $valid_plan_ids .
* @param string $access_level .
*
* @return bool
*/
public function visitor_can_view_content( $valid_plan_ids, $access_level ) {
include_once WP_CONTENT_DIR . '/mu-plugins/email-subscriptions/subscriptions.php';
$email = wp_get_current_user()->user_email;
$subscriber_object = \Blog_Subscriber::get( $email );
$is_blog_subscriber = false;
if ( $subscriber_object ) {
$blog_id = $this->get_site_id();
$subscription_status = \Blog_Subscription::get_subscription_status_for_blog( $subscriber_object, $blog_id );
$is_blog_subscriber = 'active' === $subscription_status;
}
return $this->user_can_view_content( $valid_plan_ids, $access_level, $is_blog_subscriber, get_the_ID() );
}
/**
* Retrieves the email of the currently authenticated subscriber.
*
* This function checks if the current user has an active subscription. If the user is subscribed,
* their email is returned. Otherwise, it returns an empty string to indicate no active subscription.
*
* @return string The email address of the subscribed user or an empty string if not subscribed.
*/
public function get_subscriber_email() {
if ( ! is_user_logged_in() ) {
return '';
}
return wp_get_current_user()->user_email;
}
/**
* Returns true if the current authenticated user is subscribed to the current site.
*
* @return bool
*/
public function is_current_user_subscribed(): bool {
include_once WP_CONTENT_DIR . '/mu-plugins/email-subscriptions/subscriptions.php';
$email = wp_get_current_user()->user_email;
$subscriber_object = \Blog_Subscriber::get( $email );
if ( empty( $subscriber_object ) ) {
return false;
}
$blog_id = $this->get_site_id();
$subscription_status = \Blog_Subscription::get_subscription_status_for_blog( $subscriber_object, $blog_id );
if ( 'active' !== $subscription_status ) {
return false;
}
return true;
}
/**
* Returns true if the current authenticated user has a pending subscription to the current site.
*
* @return bool
*/
public function is_current_user_pending_subscriber(): bool {
include_once WP_CONTENT_DIR . '/mu-plugins/email-subscriptions/subscriptions.php';
$email = wp_get_current_user()->user_email;
$subscriber_object = \Blog_Subscriber::get( $email );
if ( empty( $subscriber_object ) ) {
return false;
}
$blog_id = $this->get_site_id();
$subscription_status = \Blog_Subscription::get_subscription_status_for_blog( $subscriber_object, $blog_id );
if ( self::BLOG_SUB_PENDING !== $subscription_status ) {
return false;
}
return true;
}
/**
* Lookup users subscriptions for a site and determine if the user has a valid subscription to match the plan ID
*
* @param array $valid_plan_ids .
* @param string $access_level .
* @param bool $is_blog_subscriber .
* @param int $post_id .
*
* @return bool
*/
protected function user_can_view_content( $valid_plan_ids, $access_level, $is_blog_subscriber, $post_id ) {
$user_id = is_user_logged_in() ? wp_get_current_user()->ID : $this->user_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(), $user_id, $this->get_site_id() );
// format the subscriptions so that they can be validated.
$subscriptions = self::abbreviate_subscriptions( $subscriptions );
$is_paid_subscriber = static::validate_subscriptions( $valid_plan_ids, $subscriptions );
return $this->user_has_access( $access_level, $is_blog_subscriber, $is_paid_subscriber, $post_id, $subscriptions );
}
/**
* Report the subscriptions as an ID => [ 'end_date' => ]. mapping
*
* @param array $subscriptions_from_bd .
*
* @return array<int, object>
*/
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'] ) ? gmdate( 'Y-m-d H:i:s', ( time() + 365 * 24 * 3600 ) ) : $subscription['end_date'];
}
}
return $subscriptions;
}
/**
* Get the site ID.
*
* @return int The site ID.
*/
public function get_site_id() {
return get_current_blog_id();
}
}
@@ -0,0 +1,85 @@
<?php
/**
* Subcription service includes to build out the service.
*
* @package Automattic\Jetpack\Extensions\Premium_Content
*/
namespace Automattic\Jetpack\Extensions\Premium_Content;
require_once __DIR__ . '/class-jwt.php';
require_once __DIR__ . '/interface-subscription-service.php';
require_once __DIR__ . '/class-abstract-token-subscription-service.php';
require_once __DIR__ . '/class-jetpack-token-subscription-service.php';
require_once __DIR__ . '/class-wpcom-online-subscription-service.php';
require_once __DIR__ . '/class-wpcom-offline-subscription-service.php';
require_once __DIR__ . '/class-unconfigured-subscription-service.php';
use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Jetpack_Token_Subscription_Service;
use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Subscription_Service;
use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Unconfigured_Subscription_Service;
use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\WPCOM_Online_Subscription_Service;
const PAYWALL_FILTER = 'earn_premium_content_subscription_service';
/**
* Initializes the premium content subscription service.
*/
function paywall_initialize() {
$paywall = subscription_service();
if ( $paywall ) {
$paywall->initialize();
}
}
add_action( 'init', 'Automattic\Jetpack\Extensions\Premium_Content\paywall_initialize', 9 );
/**
* Gets the service handling the premium content subscriptions.
*
* @param int|null $user_id An optional user_id to query subscriptions against. Uses token from request/cookie or logged-in user information if omitted.
* @return Subscription_Service Service that will handle the premium content subscriptions.
*/
function subscription_service( $user_id = null ) {
/**
* 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, $user_id );
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.
* @param int|null $user_id An optional user_id to query subscriptions against. Uses token from request/cookie or logged-in user information if omitted.
* @return Subscription_Service Service that will handle the premium content.
*/
function default_service( $service, $user_id = null ) {
if ( null !== $service ) {
return $service;
}
// Prefer to use the WPCOM_Online_Subscription_Service if this code is executing on WPCOM.
$wpcom_available_but_user_is_not_logged_in = defined( 'IS_WPCOM' ) && IS_WPCOM && ! is_user_logged_in() && ! empty( $user_id );
if ( WPCOM_Online_Subscription_Service::available() || $wpcom_available_but_user_is_not_logged_in ) {
// Return the WPCOM Online subscription service when we are on WPCOM.
return new WPCOM_Online_Subscription_Service( $user_id );
}
// Fallback on using the Jetpack_Token_Subscription_Service if this is not executing on WPCOM but is executing on a Jetpack site.
if ( Jetpack_Token_Subscription_Service::available() ) {
// Return the Jetpack Token Subscription Service when it is available.
return new Jetpack_Token_Subscription_Service();
}
// Return an Unconfigured Subscription Service if this is not a WPCOM or Jetpack site or if both of those services are not available.
return new Unconfigured_Subscription_Service();
}
add_filter( PAYWALL_FILTER, 'Automattic\Jetpack\Extensions\Premium_Content\default_service', 10, 2 );
@@ -0,0 +1,65 @@
<?php
/**
* The Subscription Service represents the entity responsible for making sure a visitor
* can see blocks that are considered premium content.
*
* If a visitor is not allowed to see they need to be given a way gain access.
*
* It is assumed that it will be a monetary exchange but that is up to the host
* that brokers the content exchange.
*
* @package Automattic\Jetpack\Extensions\Premium_Content;
*/
namespace Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service;
interface Subscription_Service {
/**
* The subscription service can be used.
*
* @return bool
*/
public static function available();
/**
* Allows a Subscription Service to setup anything it needs to provide its features.
*
* This is called during an `init` action hook callback.
*
* Examples of things a Service may want to do here:
* - Determine a visitor is arriving with a new token to unlock content and
* store the token for future browsing (e.g. in a cookie)
* - Set up WP-API endpoints necessary for the function to work
* - Token refreshes
*
* @return void
*/
public function initialize();
/**
* Given a token (this could be from a cookie, a querystring, or some other means)
* can the visitor see the premium content?
*
* @param array $valid_plan_ids .
* @param string $access_level .
*
* @return bool
*/
public function visitor_can_view_content( $valid_plan_ids, $access_level );
/**
* Is the current user a pending subscriber for the current site?
*
* @return bool
*/
public function is_current_user_pending_subscriber(): bool;
/**
* The current visitor would like to obtain access. Where do they go?
*
* @param string $mode .
* @return string
*/
public function access_url( $mode = 'subscribe' );
}
@@ -0,0 +1,42 @@
<?php
/**
* Premium Content Buttons Child Block.
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Premium_Content;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
const BUTTONS_NAME = 'premium-content/buttons';
/**
* 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_buttons_block() {
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;
}
@@ -0,0 +1,65 @@
<?php
/**
* Premium Content Logged Out View Child Block.
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Premium_Content;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
const LOGGEDOUT_VIEW_NAME = 'premium-content/logged-out-view';
require_once dirname( __DIR__ ) . '/_inc/access-check.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_loggedout_view_block() {
Blocks::jetpack_register_block(
LOGGEDOUT_VIEW_NAME,
array(
'render_callback' => __NAMESPACE__ . '\render_loggedout_view_block',
'uses_context' => array( 'premium-content/planId', 'premium-content/planIds' ),
)
);
}
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;
}
@@ -0,0 +1,109 @@
<?php
/**
* Premium Content Login Button Child Block.
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Premium_Content;
use Automattic\Jetpack\Blocks;
use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Abstract_Token_Subscription_Service;
use Automattic\Jetpack\Status\Host;
use Jetpack_Gutenberg;
use Jetpack_Options;
require_once dirname( __DIR__ ) . '/_inc/subscription-service/include.php';
const LOGIN_BUTTON_NAME = 'premium-content/login-button';
/**
* 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_login_button_block() {
Blocks::jetpack_register_block(
LOGIN_BUTTON_NAME,
array(
'render_callback' => __NAMESPACE__ . '\render_login_button_block',
)
);
}
add_action( 'init', __NAMESPACE__ . '\register_login_button_block' );
/**
* Returns current URL.
*
* @return string
*/
function get_current_url() {
if ( ! isset( $_SERVER['HTTP_HOST'] ) || ! isset( $_SERVER['REQUEST_URI'] ) ) {
return '';
}
return ( is_ssl() ? 'https://' : 'http://' ) . wp_unslash( $_SERVER['HTTP_HOST'] ) . wp_unslash( $_SERVER['REQUEST_URI'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
}
/**
* Returns subscriber log in URL.
*
* @param string $redirect Path to redirect to on login.
*
* @return string
*/
function get_subscriber_login_url( $redirect ) {
$redirect = ! empty( $redirect ) ? $redirect : get_site_url();
if ( ( new Host() )->is_wpcom_simple() ) {
// On WPCOM we will redirect immediately
return wpcom_logmein_redirect_url( $redirect, false, null, 'link', get_current_blog_id() );
}
// On self-hosted we will save and hide the token
$redirect_url = get_site_url() . '/wp-json/jetpack/v4/subscribers/auth';
$redirect_url = add_query_arg( 'redirect_url', $redirect, $redirect_url );
return add_query_arg(
array(
'site_id' => intval( Jetpack_Options::get_option( 'id' ) ),
'redirect_url' => rawurlencode( $redirect_url ),
),
'https://subscribe.wordpress.com/memberships/jwt/'
);
}
/**
* Determines whether the current visitor is a logged in user or a subscriber.
*
* @return bool
*/
function is_subscriber_logged_in() {
return is_user_logged_in() || Abstract_Token_Subscription_Service::has_token_from_cookie();
}
/**
* 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 '';
}
// The viewer is logged it, so they shouldn't see the login button.
if ( is_subscriber_logged_in() ) {
return '';
}
Jetpack_Gutenberg::load_styles_as_required( LOGIN_BUTTON_NAME );
$redirect_url = get_current_url();
$url = get_subscriber_login_url( $redirect_url );
return preg_replace( '/(<a\b[^><]*)>/i', '$1 href="' . esc_url( $url ) . '">', $content );
}
@@ -0,0 +1,172 @@
<?php
/**
* Premium Content Block.
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Premium_Content;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
use WP_Post;
use const Automattic\Jetpack\Extensions\Subscriptions\META_NAME_CONTAINS_PAID_CONTENT;
require_once __DIR__ . '/_inc/access-check.php';
require_once __DIR__ . '/logged-out-view/logged-out-view.php';
require_once __DIR__ . '/subscriber-view/subscriber-view.php';
require_once __DIR__ . '/buttons/buttons.php';
require_once __DIR__ . '/login-button/login-button.php';
require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/subscriptions/constants.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() {
require_once JETPACK__PLUGIN_DIR . '/modules/memberships/class-jetpack-memberships.php';
if ( \Jetpack_Memberships::should_enable_monetize_blocks_in_editor() ) {
Blocks::jetpack_register_block(
__DIR__,
array(
'render_callback' => __NAMESPACE__ . '\render_block',
'provides_context' => array(
'premium-content/planId' => 'selectedPlanId', // Deprecated.
'premium-content/planIds' => 'selectedPlanIds',
'isPremiumContentChild' => 'isPremiumContentChild',
),
)
);
}
register_post_meta(
'post',
META_NAME_CONTAINS_PAID_CONTENT,
array(
'show_in_rest' => true,
'single' => true,
'type' => 'boolean',
'auth_callback' => function () {
return wp_get_current_user()->has_cap( 'edit_posts' );
},
)
);
// This ensures Jetpack will sync this post meta to WPCOM.
add_filter(
'jetpack_sync_post_meta_whitelist',
function ( $allowed_meta ) {
return array_merge(
$allowed_meta,
array(
META_NAME_CONTAINS_PAID_CONTENT,
)
);
}
);
add_action( 'wp_after_insert_post', __NAMESPACE__ . '\add_paid_content_post_meta', 99, 2 );
}
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 '';
}
// Render the Stripe nudge when Stripe is unconnected
if ( ! membership_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_assets_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 ) {
\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' )
);
} else {
// 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' )
);
}
}
/**
* 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 ) {
require_once JETPACK__PLUGIN_DIR . '_inc/lib/components.php';
return \Jetpack_Components::render_frontend_nudge(
array(
'checkoutUrl' => $checkout_url,
'description' => $description,
'buttonText' => $button_text,
)
);
}
/**
* Add a meta to prevent publication on firehose, ES AI or Reader
*
* @param int $post_id Post id.
* @param WP_Post $post Post being saved.
* @return void
*/
function add_paid_content_post_meta( int $post_id, WP_Post $post ) {
if ( $post->post_type !== 'post' && $post->post_type !== 'page' ) {
return;
}
$contains_paid_content = has_block( 'premium-content/container', $post );
if ( $contains_paid_content ) {
update_post_meta(
$post_id,
META_NAME_CONTAINS_PAID_CONTENT,
$contains_paid_content
);
}
if ( ! $contains_paid_content ) {
delete_post_meta(
$post_id,
META_NAME_CONTAINS_PAID_CONTENT
);
}
}
@@ -0,0 +1,57 @@
<?php
/**
* Premium Content Subscriber View Child Block.
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Premium_Content;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
const SUBSCRIBER_VIEW_NAME = 'premium-content/subscriber-view';
require_once dirname( __DIR__ ) . '/_inc/access-check.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_subscriber_view_block() {
Blocks::jetpack_register_block(
SUBSCRIBER_VIEW_NAME,
array(
'render_callback' => __NAMESPACE__ . '\render_subscriber_view_block',
'uses_context' => array( 'premium-content/planId', 'premium-content/planIds' ),
)
);
}
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 '';
}
@@ -0,0 +1,130 @@
<?php
/**
* Utilities for the rating block.
*
* @since 8.0.0
*
* @package automattic/jetpack
*/
if ( ! function_exists( 'jetpack_rating_meta_get_symbol_low_fidelity' ) ) {
/**
* Returns the low fidelity symbol for the block.
*
* @return string
*/
function jetpack_rating_meta_get_symbol_low_fidelity() {
return '<span aria-hidden="true">⭐</span>';
}
}
if ( ! function_exists( 'jetpack_rating_star_get_symbol_high_fidelity' ) ) {
/**
* Return the high fidelity symbol for the block.
*
* @param string $classname_whole Name of the whole symbol class.
* @param string $classname_half Name of the half symbol class.
* @param string $color Color of the block.
*
* @return string
*/
function jetpack_rating_star_get_symbol_high_fidelity( $classname_whole, $classname_half, $color ) {
return <<<ELO
<span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path class="{$classname_whole}" fill="{$color}" stroke="{$color}" d="M12,17.3l6.2,3.7l-1.6-7L22,9.2l-7.2-0.6L12,2L9.2,8.6L2,9.2L7.5,14l-1.6,7L12,17.3z" />
</svg>
</span>
<span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path class="{$classname_half}" fill="{$color}" stroke="{$color}" d="M12,17.3l6.2,3.7l-1.6-7L22,9.2l-7.2-0.6L12,2L9.2,8.6L2,9.2L7.5,14l-1.6,7L12,17.3z" />
</svg>
</span>
ELO;
}
}
if ( ! function_exists( 'jetpack_rating_meta_get_symbol_high_fidelity' ) ) {
/**
* Returns the high fidelity symbol for the block.
*
* @param array $attributes Array containing the block attributes.
* @param integer $pos Value to render whole and half symbols.
* @return string
*/
function jetpack_rating_meta_get_symbol_high_fidelity( $attributes, $pos ) {
$classname_whole = ( $attributes['rating'] >= ( $pos - 0.5 ) ) ? '' : 'is-rating-unfilled';
$classname_half = ( $attributes['rating'] >= $pos ) ? '' : 'is-rating-unfilled';
$color = empty( $attributes['color'] ) ? 'currentColor' : esc_attr( $attributes['color'] );
return jetpack_rating_star_get_symbol_high_fidelity( $classname_whole, $classname_half, $color );
}
}
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.
*
* @param array $attributes Array containing the block attributes.
*
* @return string
*/
function jetpack_rating_meta_get_symbols( $attributes ) {
// Output SVGs for high fidelity contexts, then color them according to rating.
// These are hidden by default, then unhid when CSS loads.
$symbols_hifi = array();
for ( $pos = 1; $pos <= $attributes['maxRating']; $pos++ ) {
$symbols_hifi[] = '<span style="display: none;" ' . jetpack_rating_get_schema_for_symbol( $pos, $attributes['maxRating'] ) . '>' . jetpack_rating_meta_get_symbol_high_fidelity( $attributes, $pos ) . '</span>';
}
// Output fallback symbols for low fidelity contexts, like AMP,
// where CSS is not loaded so the high-fidelity symbols won't be rendered.
$symbols_lofi = '';
for ( $i = 0; $i < $attributes['rating']; $i++ ) {
$symbols_lofi .= jetpack_rating_meta_get_symbol_low_fidelity();
}
return '<p>' . $symbols_lofi . '</p>' . implode( $symbols_hifi );
}
}
if ( ! function_exists( 'jetpack_rating_meta_render_block' ) ) {
/**
* Dynamic rendering of the block.
*
* @param array $attributes Array containing the block attributes.
*
* @return string
*/
function jetpack_rating_meta_render_block( $attributes ) {
$classname = empty( $attributes['className'] ) ? '' : ' ' . $attributes['className'];
return sprintf(
'<div class="%1$s" style="text-align:%4$s" itemprop="reviewRating" itemscope itemtype="http://schema.org/Rating">%2$s%3$s</div>',
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.
'<span itemprop="ratingValue" class="screen-reader-text" content="' . esc_attr( $attributes['rating'] ) . '">' . sprintf( __( 'Rating: %1$s out of %2$s.', 'jetpack' ), esc_attr( $attributes['rating'] ), esc_attr( $attributes['maxRating'] ) ) . '</span>',
( isset( $attributes['align'] ) ) ? esc_attr( $attributes['align'] ) : ''
);
}
}
@@ -0,0 +1,58 @@
<?php
/**
* Star Rating Block.
*
* @since 8.0.0
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Rating_Star;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
// 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(
__DIR__,
array(
'render_callback' => __NAMESPACE__ . '\render_block',
)
);
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* Dynamic rendering of the block.
*
* @param array $attributes Array containing the block attributes.
*
* @return string
*/
function render_block( $attributes ) {
// Tell Jetpack to load the assets registered via jetpack_register_block.
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
return jetpack_rating_meta_render_block( $attributes );
}
/**
* 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 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', __NAMESPACE__ . '\amp_add_inline_css', 11 );
@@ -0,0 +1,87 @@
<?php
/**
* Jetpack Recipe Block
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Recipe;
use Jetpack_Gutenberg;
/**
* Jetpack Recipe Block class.
*
* Helper class that lets us add schema attributes dynamically because they are not something that is store with the content.
* Due to the limitations of wp_kses.
*
* @since 11.1
*/
class Jetpack_Recipe_Block {
/**
* Adds recipe schema attributes.
*
* @param array $attr Array containing the recipe block attributes.
* @param string $content String containing the recipe block content.
*
* @return string
*/
public static function render( $attr, $content ) {
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
$find = array(
'/(class="wp-block-jetpack-recipe(\s|"))/',
'/(class="wp-block-jetpack-recipe-title(\s|"))/',
'/(class="wp-block-jetpack-recipe-description(\s|"))/',
);
$replace = array(
'itemscope itemtype="https://schema.org/Recipe" ${1}',
'itemprop="name" ${1}',
'itemprop="description" ${1}',
);
return preg_replace( $find, $replace, $content );
}
/**
* Adds recipe hero schema attributes.
*
* @param array $attr Array containing the recipe-hero block attributes.
* @param string $content String containing the recipe-hero block content.
*
* @return string
*/
public static function render_hero( $attr, $content ) {
$find = array(
'<img',
);
$replace = array(
'<img itemprop="image" ',
);
return str_replace( $find, $replace, $content );
}
/**
* Adds recipe step schema attributes.
*
* @param array $attr Array containing the recipe-step block attributes.
* @param string $content String containing the recipe-step block content.
*
* @return string
*/
public static function render_step( $attr, $content ) {
$find = array(
'class="wp-block-jetpack-recipe-step-name"',
'class="wp-block-jetpack-recipe-step-desc"',
'class="wp-image',
);
$replace = array(
'itemprop="name" class="wp-block-jetpack-recipe-step-name"',
'itemprop="text" class="wp-block-jetpack-recipe-step-desc"',
'itemprop="image" class="wp-image',
);
return str_replace( $find, $replace, $content );
}
}
@@ -0,0 +1,68 @@
<?php
/**
* Recipe Block.
*
* @since 11.1
*
* @package automattic/jetpack
*/
use Automattic\Jetpack\Blocks;
add_action(
'init',
function () {
Blocks::jetpack_register_block(
__DIR__,
array(
'render_callback' => array( 'Automattic\\Jetpack\\Extensions\\Recipe\\Jetpack_Recipe_Block', 'render' ),
)
);
Blocks::jetpack_register_block(
'jetpack/recipe-details',
array(
'parent' => array( 'jetpack/recipe' ),
)
);
Blocks::jetpack_register_block(
'jetpack/recipe-hero',
array(
'parent' => array( 'jetpack/recipe' ),
'render_callback' => array( 'Automattic\\Jetpack\\Extensions\\Recipe\\Jetpack_Recipe_Block', 'render_hero' ),
)
);
Blocks::jetpack_register_block(
'jetpack/recipe-ingredients-list',
array(
'parent' => array( 'jetpack/recipe' ),
)
);
Blocks::jetpack_register_block(
'jetpack/recipe-ingredient-item',
array(
'parent' => array( 'jetpack/recipe' ),
)
);
Blocks::jetpack_register_block(
'jetpack/recipe-steps',
array(
'parent' => array( 'jetpack/recipe' ),
)
);
Blocks::jetpack_register_block(
'jetpack/recipe-step',
array(
'parent' => array( 'jetpack/recipe' ),
'render_callback' => array( 'Automattic\\Jetpack\\Extensions\\Recipe\\Jetpack_Recipe_Block', 'render_step' ),
)
);
}
);
require_once __DIR__ . '/class-jetpack-recipe-block.php';
@@ -0,0 +1,13 @@
<?php // phpcs:disable Squiz.Commenting.FileComment.Missing
/**
* Memberships block.
*
* @since 7.3.0
*
* @package automattic/jetpack
*/
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();
}
@@ -0,0 +1,65 @@
<?php
/**
* Related Posts Block.
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\RelatedPosts;
use Automattic\Jetpack\Blocks;
use Automattic\Jetpack\Modules;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Status\Host;
use WP_Block;
/**
* 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 (
( new Host() )->is_wpcom_simple()
|| ( \Jetpack::is_connection_ready() && ! ( new Status() )->is_offline_mode() )
) {
Blocks::jetpack_register_block(
__DIR__,
array(
'render_callback' => __NAMESPACE__ . '\render_block',
)
);
}
}
add_action( 'init', __NAMESPACE__ . '\register_block', 9 );
/**
* Related Posts block render callback.
*
* @param array $attributes Array containing the Button block attributes.
* @param string $content The block content.
* @param WP_Block $block The block object.
*
* @return string
*/
function render_block( $attributes, $content, $block ) {
// If the Related Posts module is not active, don't render the block.
if (
! ( new Host() )->is_wpcom_simple()
&& ! ( new Modules() )->is_active( 'related-posts' )
) {
return '';
}
// If the Related Posts option is turned off, don't render the block.
$options = \Jetpack_Options::get_option( 'relatedposts', array() );
if ( empty( $options['enabled'] ) || ! $options['enabled'] ) {
return '';
}
if ( ! class_exists( 'Jetpack_RelatedPosts' ) ) {
require_once JETPACK__PLUGIN_DIR . 'modules/related-posts/jetpack-related-posts.php';
}
return \Jetpack_RelatedPosts::init()->render_block( $attributes, $content, $block );
}
@@ -0,0 +1,54 @@
<?php
/**
* Repeat Visitor Block
*
* @since 7.2.0
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Repeat_Visitor;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
/**
* 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(
__DIR__,
array( 'render_callback' => __NAMESPACE__ . '\render_block' )
);
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* Repeat Visitor block dependency declaration.
*
* @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_assets_as_required( __DIR__ );
$classes = Blocks::classes( Blocks::get_block_feature( __DIR__ ), $attributes );
$count = isset( $_COOKIE['jp-visit-counter'] ) ? (int) $_COOKIE['jp-visit-counter'] : 0;
$criteria = isset( $attributes['criteria'] ) ? $attributes['criteria'] : 'after-visits';
$threshold = isset( $attributes['threshold'] ) ? (int) $attributes['threshold'] : 3;
if (
( 'after-visits' === $criteria && $count >= $threshold ) ||
( 'before-visits' === $criteria && $count < $threshold )
) {
return $content;
}
// return an empty div so that view script increments the visit counter in the cookie.
return '<div class="' . esc_attr( $classes ) . '"></div>';
}
@@ -0,0 +1,43 @@
<?php
/**
* Send a Message Block.
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Send_A_Message;
require_once __DIR__ . '/whatsapp-button/whatsapp-button.php';
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
/**
* 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(
__DIR__,
array(
'render_callback' => __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( Blocks::get_block_feature( __DIR__ ) );
return $content;
}
@@ -0,0 +1,45 @@
<?php
/**
* WhatsApp Button Block.
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\WhatsApp_Button;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
const PARENT_NAME = 'send-a-message';
const FEATURE_NAME = 'whatsapp-button';
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',
'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;
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,195 @@
<?php
/**
* SVG icons related functions and filters
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Sharing_Button_Block;
/**
* Returns an svg icon for a given social network.
*
* @param string $social_logo logo name for social icon.
*
* @return string
*/
function get_social_logo( $social_logo ) {
$svg = '';
switch ( $social_logo ) {
case 'amazon':
$svg = '<svg class="social-logo social-logo-amazon" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M13.582 8.182c-1.648.185-3.802.308-5.344.984-1.781.769-3.03 2.337-3.03 4.644 0 2.953 1.86 4.429 4.253 4.429 2.02 0 3.125-.477 4.685-2.065.516.747.685 1.109 1.629 1.894a.589.589 0 00.672-.066l.006.006c.567-.505 1.599-1.401 2.18-1.888.231-.188.19-.496.009-.754-.52-.718-1.072-1.303-1.072-2.634V8.305c0-1.876.133-3.599-1.249-4.891C15.23 2.369 13.422 2 12.04 2 9.336 2 6.318 3.01 5.686 6.351c-.068.355.191.542.423.594l2.754.298c.258-.013.445-.266.494-.523.236-1.151 1.2-1.706 2.284-1.706.584 0 1.249.215 1.595.738.398.584.346 1.384.346 2.061v.369zm-.533 5.906c-.451.8-1.169 1.291-1.967 1.291-1.09 0-1.728-.83-1.728-2.061 0-2.42 2.171-2.86 4.227-2.86v.615c.001 1.108.027 2.031-.532 3.015zm7.634 5.251C18.329 21.076 14.917 22 11.979 22c-4.118 0-7.826-1.522-10.632-4.057-.22-.199-.024-.471.241-.317 3.027 1.762 6.771 2.823 10.639 2.823 2.608 0 5.476-.541 8.115-1.66.397-.169.73.262.341.55zm.653 1.704c-.194.163-.379.076-.293-.139.284-.71.92-2.298.619-2.684-.301-.386-1.99-.183-2.749-.092-.23.027-.266-.173-.059-.319 1.348-.946 3.555-.673 3.811-.356.26.32-.066 2.533-1.329 3.59z"/></g></svg>';
break;
case 'behance':
$svg = '<svg class="social-logo social-logo-behance" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M7.799 5.698c.589 0 1.12.051 1.606.156.482.102.894.273 1.241.507.344.235.612.546.804.938.188.387.281.871.281 1.443 0 .619-.141 1.137-.421 1.551-.284.413-.7.751-1.255 1.014.756.218 1.317.601 1.689 1.146.374.549.557 1.205.557 1.975 0 .623-.12 1.161-.359 1.612a3.144 3.144 0 01-.973 1.114c-.408.288-.876.5-1.399.637A6.144 6.144 0 017.963 18H2V5.698h5.799m-.35 4.97c.481 0 .878-.114 1.192-.345.311-.228.463-.603.463-1.119 0-.286-.051-.523-.152-.707a1.123 1.123 0 00-.416-.427 1.733 1.733 0 00-.596-.216 3.616 3.616 0 00-.697-.06H4.709v2.874h2.74zm.151 5.237c.267 0 .521-.024.759-.077.243-.053.457-.137.637-.261.182-.12.332-.283.441-.491.11-.206.163-.474.163-.798 0-.633-.18-1.084-.533-1.357-.356-.27-.83-.404-1.413-.404H4.709v3.388H7.6zm8.562-.041c.367.358.897.538 1.583.538.493 0 .92-.125 1.277-.374.354-.248.571-.514.654-.79h2.155c-.347 1.072-.872 1.838-1.589 2.299-.708.463-1.572.693-2.58.693-.701 0-1.332-.113-1.899-.337a4.041 4.041 0 01-1.439-.958 4.364 4.364 0 01-.904-1.484 5.433 5.433 0 01-.32-1.899c0-.666.11-1.288.329-1.863a4.36 4.36 0 01.933-1.492c.406-.42.885-.751 1.444-.994a4.63 4.63 0 011.857-.363c.754 0 1.414.145 1.98.44a3.941 3.941 0 011.389 1.181 4.82 4.82 0 01.783 1.69c.16.632.217 1.292.171 1.983h-6.428c-.001.706.237 1.372.604 1.73m2.811-4.68c-.291-.321-.783-.496-1.384-.496-.39 0-.714.066-.973.2a1.972 1.972 0 00-.621.491 1.772 1.772 0 00-.328.628 2.695 2.695 0 00-.111.587h3.98c-.058-.625-.271-1.085-.563-1.41zm-3.916-3.446h4.985V6.524h-4.985v1.214z"/></g></svg>';
break;
case 'blogger-alt':
$svg = '<svg class="social-logo social-logo-blogger-alt" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M19.779 9.904h-.981l-.021.001a1.163 1.163 0 01-1.16-1.079l-.001-.013A5.813 5.813 0 0011.803 3H8.871a5.813 5.813 0 00-5.813 5.813v6.375a5.813 5.813 0 005.813 5.813h6.257a5.814 5.814 0 005.813-5.813l.002-4.121a1.164 1.164 0 00-1.164-1.163zM8.726 7.713h3.291a1.117 1.117 0 110 2.234H8.726a1.117 1.117 0 110-2.234zm6.601 8.657H8.72a1.057 1.057 0 110-2.114h6.607a1.057 1.057 0 110 2.114z"/></g></svg>';
break;
case 'blogger':
$svg = '<svg class="social-logo social-logo-blogger" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M14.722 14.019a.654.654 0 01-.654.654H9.977a.654.654 0 010-1.308h4.091c.361 0 .654.293.654.654zm-4.741-3.321h2.038a.692.692 0 000-1.384H9.981a.692.692 0 000 1.384zM21 5v14a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h14a2 2 0 012 2zm-3.456 6.39a.72.72 0 00-.72-.72h-.607l-.013.001a.72.72 0 01-.718-.668l-.001-.008a3.599 3.599 0 00-3.599-3.599H10.07a3.599 3.599 0 00-3.599 3.599v3.947a3.6 3.6 0 003.599 3.599h3.874a3.599 3.599 0 003.599-3.599l.001-2.552z"/></g></svg>';
break;
case 'bluesky':
$svg = '<svg class="social-logo social-logo-bluesky" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21.2 3.3C20.7 3.1 19.8 2.8 17.6 4.3C15.4 6 12.9 9.2 12 11C11.1 9.2 8.6 6 6.3 4.3C4.1 2.7 3.3 3 2.7 3.3C2.1 3.6 2 4.6 2 5.1C2 5.6 2.3 9.8 2.5 10.5C3.2 12.8 5.6 13.6 7.8 13.3C4.5 13.8 1.6 15 5.4 19.2C9.6 23.5 11.1 18.3 11.9 15.6C12.7 18.3 13.6 23.3 18.3 19.2C21.9 15.6 19.3 13.8 16 13.3C18.2 13.5 20.6 12.8 21.3 10.5C21.7 9.8 22 5.7 22 5.1C22 4.6 21.9 3.6 21.2 3.3Z" /></svg>';
break;
case 'codepen':
$svg = '<svg class="social-logo social-logo-codepen" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M22.016 8.84l-.007-.037c-.005-.025-.008-.048-.015-.072-.003-.015-.01-.028-.013-.042l-.023-.062-.02-.042a.391.391 0 00-.03-.057.357.357 0 00-.025-.038l-.035-.052-.03-.037c-.015-.017-.028-.032-.043-.045-.01-.012-.022-.023-.035-.035a.442.442 0 00-.048-.04l-.037-.03-.015-.012-9.161-6.096a.864.864 0 00-.955 0L2.359 8.237l-.015.012-.038.028-.048.04a.638.638 0 00-.078.082c-.012.013-.022.023-.03.037-.011.017-.025.035-.035.052a.498.498 0 01-.025.038c-.011.022-.021.039-.03.059a.39.39 0 01-.02.041 1.184 1.184 0 00-.034.106c-.007.023-.011.046-.016.071-.001.014-.005.025-.006.037a.73.73 0 00-.009.114v6.093c0 .037.003.075.008.112l.007.038c.005.023.008.047.015.072a.209.209 0 00.013.04c.007.022.013.042.022.063l.02.04a.4.4 0 00.055.096l.035.052.03.037a.603.603 0 00.042.045l.035.035c.015.013.032.028.048.04l.038.03.013.01 9.163 6.095a.858.858 0 00.959.004l9.163-6.095.015-.01c.013-.01.027-.02.037-.03a.534.534 0 00.048-.04c.013-.012.025-.023.035-.035.017-.015.03-.032.043-.045l.03-.037a.678.678 0 00.035-.052l.025-.038a.4.4 0 00.03-.058l.02-.04.023-.063c.003-.013.01-.027.013-.04.007-.025.01-.048.015-.072l.007-.037c.003-.042.007-.079.007-.117V8.954a.625.625 0 00-.008-.114zm-9.154-4.376l6.751 4.49-3.016 2.013-3.735-2.492V4.464zm-1.724 0v4.009l-3.735 2.494-3.014-2.013 6.749-4.49zm-7.439 6.098L5.853 12l-2.155 1.438v-2.876zm7.439 8.974l-6.749-4.491 3.015-2.011 3.735 2.492v4.01zM12 14.035L8.953 12 12 9.966 15.047 12 12 14.035zm.862 5.501v-4.009l3.735-2.492 3.016 2.011-6.751 4.49zm7.441-6.098L18.147 12l2.156-1.438v2.876z"/></g></svg>';
break;
case 'dribbble':
$svg = '<svg class="social-logo social-logo-dribbble" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12 22C6.486 22 2 17.514 2 12S6.486 2 12 2s10 4.486 10 10-4.486 10-10 10zm8.434-8.631c-.292-.092-2.644-.794-5.32-.365 1.117 3.07 1.572 5.57 1.659 6.09a8.56 8.56 0 003.661-5.725zm-5.098 6.507c-.127-.749-.623-3.361-1.822-6.477l-.056.019c-4.818 1.679-6.547 5.02-6.701 5.334A8.5 8.5 0 0012 20.555a8.488 8.488 0 003.336-.679zm-9.682-2.152c.193-.331 2.538-4.213 6.943-5.637.111-.036.224-.07.337-.102a29.017 29.017 0 00-.692-1.45c-4.266 1.277-8.405 1.223-8.778 1.216a8.497 8.497 0 002.19 5.973zm-2.015-7.46c.382.005 3.901.02 7.897-1.041a54.477 54.477 0 00-3.167-4.94 8.572 8.572 0 00-4.73 5.981zm6.359-6.555a45.7 45.7 0 013.187 5c3.037-1.138 4.323-2.867 4.477-3.085a8.508 8.508 0 00-7.664-1.915zm8.614 2.903c-.18.243-1.612 2.078-4.77 3.367a27.028 27.028 0 01.751 1.678c2.842-.357 5.666.215 5.948.275a8.503 8.503 0 00-1.929-5.32z"/></g></svg>';
break;
case 'dropbox':
$svg = '<svg class="social-logo social-logo-dropbox" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12 6.134L6.069 9.797 2 6.54l5.883-3.843L12 6.134zm-10 6.92l5.883 3.843L12 13.459 6.069 9.797 2 13.054zm10 .405l4.116 3.439L22 13.054l-4.069-3.257L12 13.459zM22 6.54l-5.884-3.843L12 6.134l5.931 3.663L22 6.54zm-9.989 7.66l-4.129 3.426-1.767-1.153v1.291l5.896 3.539 5.897-3.539v-1.291l-1.769 1.153-4.128-3.426z"/></g></svg>';
break;
case 'eventbrite':
$svg = '<svg class="social-logo social-logo-eventbrite" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M18.041 3.931L5.959 3A2.96 2.96 0 003 5.959v12.083A2.96 2.96 0 005.959 21l12.083-.931C19.699 19.983 21 18.744 21 17.11V6.89c0-1.634-1.259-2.863-2.959-2.959zM16.933 8.17c-.082.215-.192.432-.378.551-.188.122-.489.132-.799.132-1.521 0-3.062-.048-4.607-.048-.152.708-.304 1.416-.451 2.128.932-.004 1.873.005 2.81.005.726 0 1.462-.069 1.586.525.04.189-.001.426-.052.615-.105.38-.258.676-.625.783-.185.054-.408.058-.646.058-1.145 0-2.345.017-3.493.02-.169.772-.328 1.553-.489 2.333 1.57-.005 3.067-.041 4.633-.058.627-.007 1.085.194 1.009.85a2.17 2.17 0 01-.211.725c-.102.208-.248.376-.488.452-.237.075-.541.064-.862.078-.304.014-.614.008-.924.016-.309.009-.619.022-.919.022-1.253 0-2.429.08-3.683.073-.603-.004-1.014-.249-1.124-.757-.059-.273-.018-.58.036-.841a3542.51 3542.51 0 011.629-7.763c.056-.265.114-.511.225-.714a1.24 1.24 0 01.79-.62c.368-.099.883-.047 1.344-.047.305 0 .612.008.914.016.925.026 1.817.03 2.747.053.304.007.615.016.915.016.621 0 1.17.073 1.245.614.039.288-.051.567-.132.783z" fill-rule="evenodd" clip-rule="evenodd"/></g></svg>';
break;
case 'facebook':
$svg = '<svg class="social-logo social-logo-facebook" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12 2C6.5 2 2 6.5 2 12c0 5 3.7 9.1 8.4 9.9v-7H7.9V12h2.5V9.8c0-2.5 1.5-3.9 3.8-3.9 1.1 0 2.2.2 2.2.2v2.5h-1.3c-1.2 0-1.6.8-1.6 1.6V12h2.8l-.4 2.9h-2.3v7C18.3 21.1 22 17 22 12c0-5.5-4.5-10-10-10z"/></g></svg>';
break;
case 'fediverse':
$svg = '<svg class="social-logo social-logo-fediverse" height="24" width="24" viewBox="0 0 743 743" xmlns="http://www.w3.org/2000/svg"><g><path d="M181.131 275.137a68.894 68.894 0 01-29.465 29.328l161.758 162.389 38.998-19.764-171.291-171.953zm213.363 214.187l-38.998 19.764 81.963 82.283a68.895 68.895 0 0129.471-29.332l-72.436-72.715zM581.646 339.391l-91.576 46.41 6.752 43.189 103.616-52.513a68.896 68.896 0 01-18.792-37.086zm-144.738 73.351L220.383 522.477a68.895 68.895 0 0118.795 37.089L443.66 455.934l-6.752-43.192zM367.275 142.438l-104.48 203.97 30.848 30.967 110.623-215.957a68.899 68.899 0 01-36.991-18.98zM235.621 399.459l-52.922 103.314a68.896 68.896 0 0136.987 18.979l46.781-91.328-30.846-30.965zM150.768 304.918a68.888 68.888 0 01-34.416 7.195 68.979 68.979 0 01-6.651-.695l30.903 197.662a68.883 68.883 0 0134.416-7.195 68.91 68.91 0 016.646.695l-30.898-197.662zM239.342 560.545c.707 4.589.949 9.239.72 13.877a68.902 68.902 0 01-7.267 27.18l197.629 31.712c-.708-4.59-.95-9.24-.723-13.878a68.892 68.892 0 017.27-27.178l-197.629-31.713zM601.133 377.199l-91.219 178.082a68.895 68.895 0 0136.994 18.983l91.217-178.08a68.895 68.895 0 01-36.992-18.985zM476.723 125.33a68.895 68.895 0 01-29.471 29.332l141.266 141.811a68.889 68.889 0 0129.468-29.332L476.723 125.33zM347.787 104.631l-178.576 90.498a68.897 68.897 0 0118.793 37.086l178.574-90.502a68.896 68.896 0 01-18.791-37.082zM446.926 154.826a68.902 68.902 0 01-34.983 7.483 69.274 69.274 0 01-6.029-.633l15.818 101.291 43.163 6.926-17.969-115.067zm-16 167.028l37.4 239.482a68.895 68.895 0 0133.914-6.943c2.415.137 4.82.401 7.207.791L474.09 328.777l-43.164-6.923zM188.131 232.975c.734 4.66.988 9.383.758 14.095a68.91 68.91 0 01-7.16 26.983l101.369 16.281 19.923-38.908-114.89-18.451zm173.736 27.9l-19.926 38.912 239.514 38.467a68.897 68.897 0 01-.695-13.719 68.893 68.893 0 017.349-27.324l-226.242-36.336z"/><path d="M412.284 156.054c34.538 1.882 64.061-24.592 65.943-59.13 1.881-34.538-24.592-64.062-59.131-65.943-34.538-1.882-64.061 24.592-65.943 59.13-1.881 34.538 24.593 64.062 59.131 65.943zM646.144 390.82c34.538 1.881 64.062-24.593 65.943-59.131 1.882-34.538-24.592-64.061-59.13-65.943-34.538-1.881-64.062 24.593-65.943 59.131-1.882 34.538 24.592 64.061 59.13 65.943zM495.086 685.719c34.538 1.881 64.062-24.592 65.943-59.13 1.881-34.538-24.592-64.062-59.13-65.943-34.538-1.882-64.062 24.592-65.943 59.13-1.882 34.538 24.592 64.062 59.13 65.943zM167.866 633.211c34.538 1.882 64.062-24.592 65.943-59.13 1.882-34.538-24.592-64.062-59.13-65.943-34.538-1.881-64.062 24.592-65.943 59.13-1.881 34.538 24.592 64.062 59.13 65.943zM116.692 305.86c34.538 1.882 64.062-24.592 65.943-59.13 1.881-34.538-24.592-64.062-59.131-65.943-34.538-1.881-64.061 24.592-65.943 59.13-1.881 34.538 24.593 64.062 59.131 65.943z" fill-opacity=".996"/></g></svg>';
break;
case 'feed':
$svg = '<svg class="social-logo social-logo-feed" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M2 8.667V12c5.515 0 10 4.485 10 10h3.333c0-7.363-5.97-13.333-13.333-13.333zM2 2v3.333c9.19 0 16.667 7.477 16.667 16.667H22C22 10.955 13.045 2 2 2zm2.5 15a2.5 2.5 0 100 5 2.5 2.5 0 000-5z"/></g></svg>';
break;
case 'flickr':
$svg = '<svg class="social-logo social-logo-flickr" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M6.5 7c-2.75 0-5 2.25-5 5s2.25 5 5 5 5-2.25 5-5-2.25-5-5-5zm11 0c-2.75 0-5 2.25-5 5s2.25 5 5 5 5-2.25 5-5-2.25-5-5-5z"/></g></svg>';
break;
case 'foursquare':
$svg = '<svg class="social-logo social-logo-foursquare" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M17.573 2H6.905C5.434 2 5 3.107 5 3.805v16.948c0 .785.422 1.077.66 1.172.238.097.892.177 1.285-.275 0 0 5.035-5.843 5.122-5.93.132-.132.132-.132.262-.132h3.26c1.368 0 1.588-.977 1.732-1.552.078-.318.692-3.428 1.225-6.122l.675-3.368C19.56 2.893 19.14 2 17.573 2zm-1.078 5.22c-.053.252-.372.518-.665.518h-4.157c-.467 0-.802.318-.802.787v.508c0 .467.337.798.805.798h3.528c.331 0 .655.362.583.715-.072.353-.407 2.102-.448 2.295-.04.193-.262.523-.655.523h-2.88c-.523 0-.683.068-1.033.503-.35.437-3.505 4.223-3.505 4.223-.032.035-.063.027-.063-.015V4.852c0-.298.26-.648.648-.648h8.562c.315 0 .61.297.528.683l-.446 2.333z"/></g></svg>';
break;
case 'ghost':
$svg = '<svg class="social-logo social-logo-ghost" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M10.203 20.997H3.005v-3.599h7.198v3.599zm10.792-3.599h-7.193v3.599h7.193v-3.599zm.003-7.198H3v3.599h17.998V10.2zm-7.195-7.197H3.005v3.599h10.798V3.003zm7.197 0h-3.599v3.599H21V3.003z"/></g></svg>';
break;
case 'github':
$svg = '<svg class="social-logo social-logo-github" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12 2C6.477 2 2 6.477 2 12c0 4.419 2.865 8.166 6.839 9.489.5.09.682-.218.682-.484 0-.236-.009-.866-.014-1.699-2.782.602-3.369-1.34-3.369-1.34-.455-1.157-1.11-1.465-1.11-1.465-.909-.62.069-.608.069-.608 1.004.071 1.532 1.03 1.532 1.03.891 1.529 2.341 1.089 2.91.833.091-.647.349-1.086.635-1.337-2.22-.251-4.555-1.111-4.555-4.943 0-1.091.39-1.984 1.03-2.682-.103-.254-.447-1.27.097-2.646 0 0 .84-.269 2.75 1.025A9.548 9.548 0 0112 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.748-1.025 2.748-1.025.546 1.376.202 2.394.1 2.646.64.699 1.026 1.591 1.026 2.682 0 3.841-2.337 4.687-4.565 4.935.359.307.679.917.679 1.852 0 1.335-.012 2.415-.012 2.741 0 .269.18.579.688.481A9.997 9.997 0 0022 12c0-5.523-4.477-10-10-10z"/></g></svg>';
break;
case 'google-alt':
$svg = '<svg class="social-logo social-logo-google-alt" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm-.05 16c-3.312 0-6-2.688-6-6s2.688-6 6-6c1.62 0 2.976.594 4.014 1.566L14.26 9.222c-.432-.408-1.188-.888-2.31-.888-1.986 0-3.606 1.65-3.606 3.672 0 2.022 1.62 3.672 3.606 3.672 2.298 0 3.144-1.59 3.3-2.532h-3.306v-2.238h5.616c.084.378.15.732.15 1.23 0 3.426-2.298 5.862-5.76 5.862z"/></g></svg>';
break;
case 'google-plus-alt':
$svg = '<svg class="social-logo social-logo-google-plus-alt" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M8 11h6.61c.06.35.11.7.11 1.16 0 4-2.68 6.84-6.72 6.84-3.87 0-7-3.13-7-7s3.13-7 7-7c1.89 0 3.47.69 4.69 1.83l-1.9 1.83c-.52-.5-1.43-1.08-2.79-1.08-2.39 0-4.34 1.98-4.34 4.42S5.61 16.42 8 16.42c2.77 0 3.81-1.99 3.97-3.02H8V11zm15 0h-2V9h-2v2h-2v2h2v2h2v-2h2"/></g></svg>';
break;
case 'google-plus':
$svg = '<svg class="social-logo social-logo-google-plus" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm-1.919 14.05a4.051 4.051 0 010-8.1c1.094 0 2.009.401 2.709 1.057l-1.15 1.118a2.229 2.229 0 00-1.559-.599c-1.341 0-2.434 1.114-2.434 2.479s1.094 2.479 2.434 2.479c1.551 0 2.122-1.073 2.227-1.709h-2.232v-1.511h3.791c.057.255.101.494.101.83.001 2.312-1.55 3.956-3.887 3.956zM19 12.75h-1.25V14h-1.5v-1.25H15v-1.5h1.25V10h1.5v1.25H19v1.5z"/></g></svg>';
break;
case 'google':
$svg = '<svg class="social-logo social-logo-google" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12.02 10.18v3.73h5.51c-.26 1.57-1.67 4.22-5.5 4.22-3.31 0-6.01-2.75-6.01-6.12s2.7-6.12 6.01-6.12c1.87 0 3.13.8 3.85 1.48l2.84-2.76C16.99 2.99 14.73 2 12.03 2c-5.52 0-10 4.48-10 10s4.48 10 10 10c5.77 0 9.6-4.06 9.6-9.77 0-.83-.11-1.42-.25-2.05h-9.36z"/></g></svg>';
break;
case 'instagram':
$svg = '<svg class="social-logo social-logo-instagram" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12 4.622c2.403 0 2.688.009 3.637.052.877.04 1.354.187 1.671.31.42.163.72.358 1.035.673.315.315.51.615.673 1.035.123.317.27.794.31 1.671.043.949.052 1.234.052 3.637s-.009 2.688-.052 3.637c-.04.877-.187 1.354-.31 1.671-.163.42-.358.72-.673 1.035-.315.315-.615.51-1.035.673-.317.123-.794.27-1.671.31-.949.043-1.233.052-3.637.052s-2.688-.009-3.637-.052c-.877-.04-1.354-.187-1.671-.31a2.786 2.786 0 01-1.035-.673 2.786 2.786 0 01-.673-1.035c-.123-.317-.27-.794-.31-1.671-.043-.949-.052-1.234-.052-3.637s.009-2.688.052-3.637c.04-.877.187-1.354.31-1.671.163-.42.358-.72.673-1.035.315-.315.615-.51 1.035-.673.317-.123.794-.27 1.671-.31.949-.043 1.234-.052 3.637-.052M12 3c-2.444 0-2.751.01-3.711.054-.958.044-1.612.196-2.184.418a4.401 4.401 0 00-1.594 1.039c-.5.5-.808 1.002-1.038 1.594-.223.572-.375 1.226-.419 2.184C3.01 9.249 3 9.556 3 12s.01 2.751.054 3.711c.044.958.196 1.612.418 2.185.23.592.538 1.094 1.038 1.594s1.002.808 1.594 1.038c.572.222 1.227.375 2.185.418.96.044 1.267.054 3.711.054s2.751-.01 3.711-.054c.958-.044 1.612-.196 2.185-.418a4.411 4.411 0 001.594-1.038c.5-.5.808-1.002 1.038-1.594.222-.572.375-1.227.418-2.185.044-.96.054-1.267.054-3.711s-.01-2.751-.054-3.711c-.044-.958-.196-1.612-.418-2.185A4.411 4.411 0 0019.49 4.51c-.5-.5-1.002-.808-1.594-1.038-.572-.222-1.227-.375-2.185-.418C14.751 3.01 14.444 3 12 3zm0 4.378a4.622 4.622 0 100 9.244 4.622 4.622 0 000-9.244zM12 15a3 3 0 110-6 3 3 0 010 6zm4.804-8.884a1.08 1.08 0 10.001 2.161 1.08 1.08 0 00-.001-2.161z"/></g></svg>';
break;
case 'link':
$svg = '<svg class="social-logo social-logo-link" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M17 13H7v-2h10v2zm1-6h-1c-1.631 0-3.065.792-3.977 2H18c1.103 0 2 .897 2 2v2c0 1.103-.897 2-2 2h-4.977c.913 1.208 2.347 2 3.977 2h1a4 4 0 004-4v-2a4 4 0 00-4-4zM2 11v2a4 4 0 004 4h1c1.63 0 3.065-.792 3.977-2H6c-1.103 0-2-.897-2-2v-2c0-1.103.897-2 2-2h4.977C10.065 7.792 8.631 7 7 7H6a4 4 0 00-4 4z"/></g></svg>';
break;
case 'linkedin':
$svg = '<svg class="social-logo social-logo-linkedin" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M19.7 3H4.3A1.3 1.3 0 003 4.3v15.4A1.3 1.3 0 004.3 21h15.4a1.3 1.3 0 001.3-1.3V4.3A1.3 1.3 0 0019.7 3zM8.339 18.338H5.667v-8.59h2.672v8.59zM7.004 8.574a1.548 1.548 0 11-.002-3.096 1.548 1.548 0 01.002 3.096zm11.335 9.764H15.67v-4.177c0-.996-.017-2.278-1.387-2.278-1.389 0-1.601 1.086-1.601 2.206v4.249h-2.667v-8.59h2.559v1.174h.037c.356-.675 1.227-1.387 2.526-1.387 2.703 0 3.203 1.779 3.203 4.092v4.711z"/></g></svg>';
break;
case 'mail':
$svg = '<svg class="social-logo social-logo-mail" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M20 4H4a2 2 0 00-2 2v12a2 2 0 002 2h16a2 2 0 002-2V6a2 2 0 00-2-2zm0 4.236l-8 4.882-8-4.882V6h16v2.236z"/></g></svg>';
break;
case 'mastodon':
$svg = '<svg class="social-logo social-logo-mastodon" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M11.973 2.352c-2.468.02-4.842.286-6.225.921 0 0-2.742 1.229-2.742 5.415 0 .958-.018 2.105.012 3.32.1 4.094.75 8.128 4.535 9.129 1.745.462 3.244.56 4.45.494 2.19-.122 3.417-.781 3.417-.781l-.072-1.588s-1.565.491-3.32.431c-1.74-.06-3.576-.188-3.858-2.324a4.359 4.359 0 01-.04-.598s1.709.416 3.874.516c1.324.06 2.563-.076 3.824-.226 2.418-.29 4.524-1.78 4.79-3.141.416-2.144.38-5.232.38-5.232 0-4.186-2.74-5.415-2.74-5.415-1.383-.635-3.76-.9-6.227-.921h-.058zM9.18 5.622c1.028 0 1.804.395 2.318 1.185l.502.84.5-.84c.514-.79 1.292-1.186 2.32-1.186.888 0 1.605.313 2.15.922.53.609.794 1.434.794 2.469v5.068h-2.008V9.16c0-1.037-.438-1.562-1.31-1.562-.966 0-1.448.622-1.448 1.857v2.693h-1.996V9.455c0-1.235-.484-1.857-1.45-1.857-.872 0-1.308.525-1.308 1.562v4.92H6.236V9.012c0-1.035.263-1.86.793-2.469.547-.609 1.263-.922 2.15-.922z"/></g></svg>';
break;
case 'medium-alt':
$svg = '<svg class="social-logo social-logo-medium-alt" height="24" width="24" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M7.423 6c3.27 0 5.922 2.686 5.922 6s-2.651 6-5.922 6S1.5 15.313 1.5 12s2.652-6 5.923-6zm9.458.351c1.635 0 2.961 2.53 2.961 5.65 0 3.118-1.325 5.648-2.96 5.648-1.636 0-2.962-2.53-2.962-5.649s1.325-5.649 2.96-5.649zm4.577.589c.576 0 1.042 2.265 1.042 5.06 0 2.794-.466 5.06-1.042 5.06-.575 0-1.04-2.265-1.04-5.06 0-2.794.465-5.06 1.04-5.06z" fill-rule="nonzero"/></g></svg>';
break;
case 'medium':
$svg = '<svg class="social-logo social-logo-medium" height="24" width="24" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M3 3v18h18V3H3zm15 4.26l-1 .93a.28.28 0 00-.11.27v6.8a.27.27 0 00.11.27l.94.93v.2h-4.75v-.2l1-1c.09-.1.09-.12.09-.27V9.74l-2.71 6.9h-.37L8 9.74v4.62a.67.67 0 00.17.54l1.27 1.54v.2H5.86v-.2l1.27-1.54a.64.64 0 00.17-.54V9a.5.5 0 00-.16-.4L6 7.26v-.2h3.52L12.23 13l2.38-5.94H18v.2z"/></g></svg>';
break;
case 'nextdoor':
$svg = '<svg class="social-logo social-logo-nextdoor" height="24" width="24" stroke-miterlimit="10" viewBox="0 0 130 130" xmlns="http://www.w3.org/2000/svg" clip-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"><g><path d="M64.25 3.531c-31.144.337-57.596 24.22-60.469 55.907-3.064 33.799 21.857 63.685 55.657 66.75 33.799 3.064 63.685-21.857 66.75-55.657 3.064-33.8-21.857-63.686-55.657-66.75a62.075 62.075 0 00-6.281-.25zm3.938 34.907C82.468 38.438 93.5 48.58 93.5 61.5v27c0 .685-.565 1.25-1.25 1.25H80.906a1.267 1.267 0 01-1.25-1.25V63.375c0-5.58-4.309-11.938-11.469-11.938-7.47 0-11.468 6.358-11.468 11.938V88.5c0 .685-.565 1.25-1.25 1.25H44.125c-.68 0-1.219-.57-1.219-1.25V64.156c0-.74-.529-1.364-1.25-1.531-13.13-2.93-15.115-10.285-15.375-21.125-.005-.332.142-.67.375-.906.233-.237.543-.375.875-.375l11.688.062c.66.01 1.187.529 1.218 1.188.13 4.44.438 9.406 4.438 9.406.83 0 1.443-1.179 1.813-1.719 4.41-6.48 12.28-10.718 21.5-10.718z"/></g></svg>';
break;
case 'patreon':
$svg = '<svg class="social-logo social-logo-patreon" height="24" width="24" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path fill-rule="evenodd" clip-rule="evenodd" d="M13.975 5a5.05 5.05 0 00-5.041 5.046c0 2.774 2.261 5.03 5.04 5.03A5.034 5.034 0 0019 10.047C19 7.264 16.746 5 13.975 5zM5 18.44h2.461V5H5v13.44z"/></g></svg>';
break;
case 'pinterest-alt':
$svg = '<svg class="social-logo social-logo-pinterest-alt" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12.289 2C6.617 2 3.606 5.648 3.606 9.622c0 1.846 1.025 4.146 2.666 4.878.25.111.381.063.439-.169.044-.175.267-1.029.365-1.428a.365.365 0 00-.091-.362c-.54-.63-.975-1.791-.975-2.873 0-2.777 2.194-5.464 5.933-5.464 3.23 0 5.49 2.108 5.49 5.122 0 3.407-1.794 5.768-4.13 5.768-1.291 0-2.257-1.021-1.948-2.277.372-1.495 1.089-3.112 1.089-4.191 0-.967-.542-1.775-1.663-1.775-1.319 0-2.379 1.309-2.379 3.059 0 1.115.394 1.869.394 1.869s-1.302 5.279-1.54 6.261c-.405 1.666.053 4.368.094 4.604.021.126.167.169.25.063.129-.165 1.699-2.419 2.142-4.051.158-.59.817-2.995.817-2.995.43.784 1.681 1.446 3.013 1.446 3.963 0 6.822-3.494 6.822-7.833C20.394 5.112 16.849 2 12.289 2"/></g></svg>';
break;
case 'pinterest':
$svg = '<svg class="social-logo social-logo-pinterest" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12 2C6.477 2 2 6.477 2 12c0 4.236 2.636 7.855 6.356 9.312-.087-.791-.166-2.005.035-2.869.182-.78 1.173-4.971 1.173-4.971s-.299-.599-.299-1.484c0-1.39.806-2.429 1.809-2.429.853 0 1.265.641 1.265 1.409 0 .858-.546 2.141-.828 3.329-.236.996.499 1.807 1.481 1.807 1.777 0 3.144-1.874 3.144-4.579 0-2.394-1.72-4.068-4.177-4.068-2.845 0-4.515 2.134-4.515 4.34 0 .859.331 1.781.744 2.282a.297.297 0 01.069.287c-.077.316-.246.995-.279 1.134-.044.183-.145.222-.334.134-1.249-.581-2.03-2.407-2.03-3.874 0-3.154 2.292-6.051 6.607-6.051 3.469 0 6.165 2.472 6.165 5.775 0 3.446-2.173 6.22-5.189 6.22-1.013 0-1.966-.526-2.292-1.148l-.623 2.377c-.226.869-.835 1.957-1.243 2.622.936.289 1.93.445 2.961.445 5.523 0 10-4.477 10-10S17.523 2 12 2z"/></g></svg>';
break;
case 'pocket':
$svg = '<svg class="social-logo social-logo-pocket" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M21.927 4.194A1.82 1.82 0 0020.222 3H3.839a1.823 1.823 0 00-1.813 1.814v6.035l.069 1.2c.29 2.73 1.707 5.115 3.899 6.778l.119.089.025.018a9.897 9.897 0 003.91 1.727 10.06 10.06 0 004.049-.014.261.261 0 00.064-.023 9.906 9.906 0 003.753-1.691l.025-.018c.04-.029.08-.058.119-.089 2.192-1.664 3.609-4.049 3.898-6.778l.069-1.2V4.814a1.792 1.792 0 00-.098-.62zm-4.235 6.287l-4.704 4.512a1.372 1.372 0 01-1.898 0l-4.705-4.512a1.371 1.371 0 111.898-1.979l3.756 3.601 3.755-3.601a1.372 1.372 0 011.898 1.979z"/></g></svg>';
break;
case 'polldaddy':
$svg = '<svg class="social-logo social-logo-polldaddy" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12 2C6.487 2 2 6.487 2 12c0 5.514 4.487 10 10 10 5.514 0 10-4.486 10-10 0-5.513-4.486-10-10-10zm.991 1.68c2.361.084 4.657 1.251 6.197 3.136.283.334.541.693.774 1.067a7.775 7.775 0 00-6.094-2.94 7.764 7.764 0 00-5.896 2.703c-.006.003-.01.01-.016.014l-.152.159-.031.032a6.122 6.122 0 00-1.633 4.165 6.15 6.15 0 006.143 6.143c.57 0 1.123-.081 1.649-.227-1.849.839-4.131.747-5.926-.324-1.841-1.089-3.171-3.111-3.433-5.313A7.386 7.386 0 016.69 6.137C8.294 4.5 10.634 3.563 12.991 3.68zm3.373 8.519c-.049-2.024-1.587-3.889-3.544-4.174-1.927-.343-3.917.857-4.451 2.661a3.673 3.673 0 00.2 2.653c.39.8 1.067 1.451 1.894 1.759 1.664.654 3.63-.27 4.173-1.863.593-1.58-.396-3.423-1.94-3.776-1.52-.407-3.161.757-3.204 2.243a2.362 2.362 0 00.753 1.879c.501.476 1.23.667 1.871.529a2.067 2.067 0 001.469-1.134 1.912 1.912 0 00-.087-1.767c-.297-.513-.859-.863-1.429-.881a1.698 1.698 0 00-1.437.679 1.525 1.525 0 00-.18 1.489c.004.011.01.021.016.03.193.634.774 1.1 1.467 1.117a1.618 1.618 0 01-.97-.183c-.466-.244-.809-.747-.893-1.29a1.8 1.8 0 01.499-1.539 2.016 2.016 0 011.58-.606c.593.04 1.159.35 1.517.859.364.496.51 1.156.383 1.773-.116.62-.529 1.174-1.093 1.514a2.515 2.515 0 01-1.914.286c-.65-.161-1.226-.606-1.584-1.206a2.825 2.825 0 01-.341-2.031c.143-.7.573-1.321 1.176-1.753 1.193-.883 3.056-.751 4.106.411 1.106 1.1 1.327 3.027.406 4.371-.877 1.376-2.74 2.086-4.374 1.594-1.639-.449-2.913-2.079-3.031-3.853-.07-.884.13-1.797.583-2.577.445-.777 1.155-1.432 1.972-1.862 1.64-.88 3.816-.743 5.349.424 1.251.924 2.083 2.42 2.236 4.009l.001.03c0 2.9-2.359 5.26-5.26 5.26a5.216 5.216 0 01-1.947-.376 5.01 5.01 0 002.613-.079 4.955 4.955 0 002.514-1.751c.618-.828.95-1.861.901-2.869zM12 21.113c-5.024 0-9.111-4.087-9.111-9.113 0-4.789 3.713-8.723 8.411-9.081a6.548 6.548 0 00-.397.06c-2.644.453-5.017 2.106-6.32 4.409-1.309 2.301-1.391 5.19-.3 7.527 1.056 2.34 3.253 4.156 5.776 4.553 2.497.44 5.133-.483 6.787-2.301 1.719-1.797 2.269-4.529 1.486-6.796-.583-1.81-1.976-3.331-3.7-4.046 3.417.594 6.174 3.221 6.174 6.781 0 1.004-.241 2.02-.657 2.966-1.498 2.984-4.586 5.041-8.149 5.041z"/></g></svg>';
break;
case 'print':
$svg = '<svg class="social-logo social-logo-print" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M9 16h6v2H9v-2zm13 1h-3v3a2 2 0 01-2 2H7a2 2 0 01-2-2v-3H2V9a2 2 0 012-2h1V5a2 2 0 012-2h10a2 2 0 012 2v2h1a2 2 0 012 2v8zM7 7h10V5H7v2zm10 7H7v6h10v-6zm3-3.5a1.5 1.5 0 10-3.001.001A1.5 1.5 0 0020 10.5z"/></g></svg>';
break;
case 'reddit':
$svg = '<svg class="social-logo social-logo-reddit" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M22 11.816a2.279 2.279 0 00-2.277-2.277c-.593 0-1.122.24-1.526.614-1.481-.965-3.455-1.594-5.647-1.69l1.171-3.702 3.18.748a1.878 1.878 0 001.876 1.862 1.88 1.88 0 001.877-1.878 1.88 1.88 0 00-1.877-1.877c-.769 0-1.431.466-1.72 1.13l-3.508-.826a.386.386 0 00-.46.261l-1.35 4.268c-2.316.038-4.411.67-5.97 1.671a2.24 2.24 0 00-1.492-.581A2.279 2.279 0 002 11.816c0 .814.433 1.523 1.078 1.925a4.056 4.056 0 00-.061.672c0 3.292 4.011 5.97 8.941 5.97s8.941-2.678 8.941-5.97c0-.214-.02-.424-.053-.632A2.259 2.259 0 0022 11.816zm-3.224-7.422a1.1 1.1 0 11-.001 2.199 1.1 1.1 0 01.001-2.199zM2.777 11.816c0-.827.672-1.5 1.499-1.5.313 0 .598.103.838.269-.851.676-1.477 1.479-1.812 2.36a1.482 1.482 0 01-.525-1.129zm9.182 7.79c-4.501 0-8.164-2.329-8.164-5.193S7.457 9.22 11.959 9.22s8.164 2.329 8.164 5.193-3.663 5.193-8.164 5.193zm8.677-6.605c-.326-.89-.948-1.701-1.797-2.384.248-.186.55-.301.883-.301.827 0 1.5.673 1.5 1.5.001.483-.23.911-.586 1.185zm-11.64 1.703c-.76 0-1.397-.616-1.397-1.376 0-.76.637-1.397 1.397-1.397s1.376.637 1.376 1.397-.616 1.376-1.376 1.376zm7.405-1.376c0 .76-.616 1.376-1.376 1.376-.76 0-1.399-.616-1.399-1.376 0-.76.639-1.397 1.399-1.397s1.376.637 1.376 1.397zm-1.172 3.38a.389.389 0 010 .55c-.674.674-1.727 1.002-3.219 1.002l-.011-.002-.011.002c-1.492 0-2.544-.328-3.218-1.002a.389.389 0 11.55-.55c.521.521 1.394.775 2.669.775l.011.002.011-.002c1.275 0 2.148-.253 2.669-.775a.387.387 0 01.549 0z"/></g></svg>';
break;
case 'share':
$svg = '<svg class="social-logo social-logo-share" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M18 16c-.788 0-1.499.31-2.034.807L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.048 4.118A2.981 2.981 0 0015 19a3 3 0 103-3z"/></g></svg>';
break;
case 'skype':
$svg = '<svg class="social-logo social-logo-skype" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M10.113 2.699l.1-.02c.033.017.066.033.098.051l-.198-.031zM2.72 10.223l-.017.103c.018.032.033.064.051.095l-.034-.198zm18.555 3.548c.007-.035.011-.071.018-.106-.018-.031-.033-.064-.052-.095l.034.201zm-7.712 7.428c.032.019.065.035.096.053l.105-.017-.201-.036zM22 16.386a5.55 5.55 0 01-1.637 3.953 5.548 5.548 0 01-3.953 1.637 5.575 5.575 0 01-2.75-.725l.105-.017-.202-.035c.032.019.065.035.096.053a9.524 9.524 0 01-1.654.147 9.375 9.375 0 01-3.676-.743 9.38 9.38 0 01-3.002-2.023 9.397 9.397 0 01-2.023-3.002 9.375 9.375 0 01-.743-3.676c0-.546.049-1.093.142-1.628.018.032.033.064.051.095l-.034-.199-.017.103A5.586 5.586 0 012 7.615c0-1.493.582-2.898 1.637-3.953A5.555 5.555 0 017.59 2.024c.915 0 1.818.228 2.622.655l-.1.02.199.031c-.032-.018-.066-.034-.098-.051l.004-.001a9.543 9.543 0 011.788-.169 9.41 9.41 0 016.678 2.766 9.4 9.4 0 012.024 3.002 9.375 9.375 0 01.743 3.676c0 .575-.054 1.15-.157 1.712-.018-.031-.033-.064-.052-.095l.034.201c.007-.035.011-.071.018-.106.461.829.707 1.767.707 2.721zm-5.183-2.248c0-1.331-.613-2.743-3.033-3.282l-2.209-.49c-.84-.192-1.807-.444-1.807-1.237 0-.794.679-1.348 1.903-1.348 2.468 0 2.243 1.696 3.468 1.696.645 0 1.209-.379 1.209-1.031 0-1.521-2.435-2.663-4.5-2.663-2.242 0-4.63.952-4.63 3.488 0 1.221.436 2.521 2.839 3.123l2.984.745c.903.223 1.129.731 1.129 1.189 0 .762-.758 1.507-2.129 1.507-2.679 0-2.307-2.062-3.743-2.062-.645 0-1.113.444-1.113 1.078 0 1.236 1.501 2.886 4.856 2.886 3.195 0 4.776-1.538 4.776-3.599z"/></g></svg>';
break;
case 'spotify':
$svg = '<svg class="social-logo social-logo-spotify" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2m4.586 14.424a.622.622 0 01-.857.207c-2.348-1.435-5.304-1.76-8.785-.964a.622.622 0 11-.277-1.215c3.809-.871 7.077-.496 9.713 1.115a.623.623 0 01.206.857M17.81 13.7a.78.78 0 01-1.072.257c-2.687-1.652-6.785-2.131-9.965-1.166A.779.779 0 116.32 11.3c3.632-1.102 8.147-.568 11.234 1.328a.78.78 0 01.256 1.072m.105-2.835c-3.223-1.914-8.54-2.09-11.618-1.156a.935.935 0 11-.542-1.79c3.532-1.072 9.404-.865 13.115 1.338a.936.936 0 11-.955 1.608"/></g></svg>';
break;
case 'squarespace':
$svg = '<svg class="social-logo social-logo-squarespace" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M20.87 9.271a3.86 3.86 0 00-5.458 0l-6.141 6.141a.964.964 0 101.365 1.364l6.14-6.14a1.929 1.929 0 112.729 2.729l-6.022 6.022a1.929 1.929 0 002.729 0l4.658-4.658a3.86 3.86 0 000-5.458zm-2.047 2.047a.965.965 0 00-1.365 0l-6.14 6.14a1.929 1.929 0 01-2.729 0 .964.964 0 10-1.364 1.364 3.86 3.86 0 005.458 0l6.14-6.14a.966.966 0 000-1.364zm-2.047-6.141a3.858 3.858 0 00-5.458 0l-6.14 6.14a.964.964 0 101.364 1.364l6.141-6.14a1.929 1.929 0 012.729 0 .965.965 0 101.364-1.364zm-2.047 2.047a.964.964 0 00-1.364 0l-6.14 6.141a1.929 1.929 0 11-2.729-2.729l6.022-6.022a1.929 1.929 0 00-2.729 0L3.13 9.271a3.86 3.86 0 005.458 5.458l6.14-6.141a.963.963 0 00.001-1.364z"/></g></svg>';
break;
case 'stumbleupon':
$svg = '<svg class="social-logo social-logo-stumbleupon" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12 4.294a4.471 4.471 0 00-4.471 4.471v6.353a1.059 1.059 0 11-2.118 0v-2.824H2v2.941a4.471 4.471 0 008.942 0v-6.47a1.059 1.059 0 112.118 0v1.294l1.412.647 2-.647V8.765A4.473 4.473 0 0012 4.294zm1.059 8.059v2.882a4.471 4.471 0 008.941 0v-2.824h-3.412v2.824a1.059 1.059 0 11-2.118 0v-2.882l-2 .647-1.411-.647z"/></g></svg>';
break;
case 'telegram':
$svg = '<svg class="social-logo social-logo-telegram" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm3.08 14.757s-.25.625-.936.325l-2.541-1.949-1.63 1.486s-.127.096-.266.036c0 0-.12-.011-.27-.486-.15-.475-.911-2.972-.911-2.972L6 12.349s-.387-.137-.425-.438c-.037-.3.437-.462.437-.462l10.03-3.934s.824-.362.824.238l-1.786 9.004z"/></g></svg>';
break;
case 'threads':
$svg = '<svg class="social-logo social-logo-threads" height="24" width="24" viewBox="0 0 192 192" xmlns="http://www.w3.org/2000/svg"><g><path class="x19hqcy" d="M141.537 88.988a66.667 66.667 0 00-2.518-1.143c-1.482-27.307-16.403-42.94-41.457-43.1h-.34c-14.986 0-27.449 6.396-35.12 18.036l13.779 9.452c5.73-8.695 14.724-10.548 21.348-10.548h.229c8.249.053 14.474 2.452 18.503 7.129 2.932 3.405 4.893 8.111 5.864 14.05-7.314-1.243-15.224-1.626-23.68-1.14-23.82 1.371-39.134 15.264-38.105 34.568.522 9.792 5.4 18.216 13.735 23.719 7.047 4.652 16.124 6.927 25.557 6.412 12.458-.683 22.231-5.436 29.049-14.127 5.178-6.6 8.453-15.153 9.899-25.93 5.937 3.583 10.337 8.298 12.767 13.966 4.132 9.635 4.373 25.468-8.546 38.376-11.319 11.308-24.925 16.2-45.488 16.351-22.809-.169-40.06-7.484-51.275-21.742C35.236 139.966 29.808 120.682 29.605 96c.203-24.682 5.63-43.966 16.133-57.317C56.954 24.425 74.204 17.11 97.013 16.94c22.975.17 40.526 7.52 52.171 21.847 5.71 7.026 10.015 15.86 12.853 26.162l16.147-4.308c-3.44-12.68-8.853-23.606-16.219-32.668C147.036 9.607 125.202.195 97.07 0h-.113C68.882.194 47.292 9.642 32.788 28.08 19.882 44.485 13.224 67.315 13.001 95.932L13 96v.067c.224 28.617 6.882 51.447 19.788 67.854C47.292 182.358 68.882 191.806 96.957 192h.113c24.96-.173 42.554-6.708 57.048-21.189 18.963-18.945 18.392-42.692 12.142-57.27-4.484-10.454-13.033-18.945-24.723-24.553zM98.44 129.507c-10.44.588-21.286-4.098-21.82-14.135-.397-7.442 5.296-15.746 22.461-16.735 1.966-.114 3.895-.169 5.79-.169 6.235 0 12.068.606 17.371 1.765-1.978 24.702-13.58 28.713-23.802 29.274z"/></g></svg>';
break;
case 'tiktok-alt':
$svg = '<svg class="social-logo social-logo-tiktok-alt" height="24" width="24" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M5 3a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2V5a2 2 0 00-2-2H5zm7.531 3h2.053s-.114 2.635 2.85 2.82v2.04s-1.582.099-2.85-.87l.021 4.207a3.804 3.804 0 11-3.802-3.802h.533v2.082a1.73 1.73 0 00-1.922.648 1.727 1.727 0 001.947 2.646 1.73 1.73 0 001.19-1.642L12.53 6z"/></g></svg>';
break;
case 'tiktok':
$svg = '<svg class="social-logo social-logo-tiktok" height="24" width="24" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M12.22 2h3.42s-.19 4.394 4.75 4.702v3.396s-2.636.166-4.75-1.448l.037 7.011a6.338 6.338 0 11-6.34-6.338h.89v3.472a2.882 2.882 0 102.024 2.752L12.22 2z"/></g></svg>';
break;
case 'tumblr-alt':
$svg = '<svg class="social-logo social-logo-tumblr-alt" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M16.749 17.396c-.357.17-1.041.319-1.551.332-1.539.041-1.837-1.081-1.85-1.896V9.847h3.861v-2.91h-3.847V2.039h-2.817c-.046 0-.127.041-.138.144-.165 1.499-.867 4.13-3.783 5.181v2.484h1.945v6.282c0 2.151 1.587 5.206 5.775 5.135 1.413-.024 2.982-.616 3.329-1.126l-.924-2.743z"/></g></svg>';
break;
case 'tumblr':
$svg = '<svg class="social-logo social-logo-tumblr" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M19 3H5a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2V5a2 2 0 00-2-2zm-5.569 14.265c-2.446.042-3.372-1.742-3.372-2.998v-3.668H8.923v-1.45c1.703-.614 2.113-2.15 2.209-3.025.007-.06.054-.084.081-.084h1.645V8.9h2.246v1.7H12.85v3.495c.008.476.182 1.131 1.081 1.107.298-.008.697-.094.906-.194l.54 1.601c-.205.296-1.121.641-1.946.656z"/></g></svg>';
break;
case 'twitch':
$svg = '<svg class="social-logo social-logo-twitch" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M16.499 8.089h-1.636v4.91h1.636v-4.91zm-4.499 0h-1.637v4.91H12v-4.91zM4.228 3.178L3 6.451v13.092h4.499V22h2.456l2.454-2.456h3.681L21 14.636V3.178H4.228zm15.136 10.638L16.5 16.681H12l-2.453 2.453V16.68H5.863V4.814h13.501v9.002z"/></g></svg>';
break;
case 'twitter-alt':
$svg = '<svg class="social-logo social-logo-twitter-alt" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M22.23 5.924a8.212 8.212 0 01-2.357.646 4.115 4.115 0 001.804-2.27 8.221 8.221 0 01-2.606.996 4.103 4.103 0 00-6.991 3.742 11.647 11.647 0 01-8.457-4.287 4.087 4.087 0 00-.556 2.063 4.1 4.1 0 001.825 3.415 4.09 4.09 0 01-1.859-.513v.052a4.104 4.104 0 003.292 4.023 4.099 4.099 0 01-1.853.07 4.11 4.11 0 003.833 2.85 8.236 8.236 0 01-5.096 1.756 8.33 8.33 0 01-.979-.057 11.617 11.617 0 006.29 1.843c7.547 0 11.675-6.252 11.675-11.675 0-.178-.004-.355-.012-.531a8.298 8.298 0 002.047-2.123z"/></g></svg>';
break;
case 'twitter':
$svg = '<svg class="social-logo social-logo-twitter" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M19 3H5a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2V5a2 2 0 00-2-2zm-2.534 6.71c.004.099.007.198.007.298 0 3.045-2.318 6.556-6.556 6.556a6.52 6.52 0 01-3.532-1.035 4.626 4.626 0 003.412-.954 2.307 2.307 0 01-2.152-1.6 2.295 2.295 0 001.04-.04 2.306 2.306 0 01-1.848-2.259v-.029c.311.173.666.276 1.044.288a2.303 2.303 0 01-.713-3.076 6.54 6.54 0 004.749 2.407 2.305 2.305 0 013.926-2.101 4.602 4.602 0 001.463-.559 2.31 2.31 0 01-1.013 1.275c.466-.056.91-.18 1.323-.363-.31.461-.7.867-1.15 1.192z"/></g></svg>';
break;
case 'vimeo':
$svg = '<svg class="social-logo social-logo-vimeo" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M22.396 7.164c-.093 2.026-1.507 4.799-4.245 8.32C15.322 19.161 12.928 21 10.97 21c-1.214 0-2.24-1.119-3.079-3.359l-1.68-6.159c-.623-2.239-1.29-3.36-2.005-3.36-.156 0-.701.328-1.634.98l-.978-1.261c1.027-.902 2.04-1.805 3.037-2.708C6.001 3.95 7.03 3.327 7.715 3.264c1.619-.156 2.616.951 2.99 3.321.404 2.557.685 4.147.841 4.769.467 2.121.981 3.181 1.542 3.181.435 0 1.09-.688 1.963-2.065.871-1.376 1.338-2.422 1.401-3.142.125-1.187-.343-1.782-1.401-1.782-.498 0-1.012.115-1.541.341 1.023-3.35 2.977-4.977 5.862-4.884 2.139.063 3.148 1.45 3.024 4.161z"/></g></svg>';
break;
case 'whatsapp':
$svg = '<svg class="social-logo social-logo-whatsapp" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M2.048 22l1.406-5.136a9.894 9.894 0 01-1.323-4.955C2.133 6.446 6.579 2 12.042 2a9.848 9.848 0 017.011 2.906 9.85 9.85 0 012.9 7.011c-.002 5.464-4.448 9.91-9.91 9.91h-.004a9.913 9.913 0 01-4.736-1.206L2.048 22zm5.497-3.172l.301.179a8.214 8.214 0 004.193 1.148h.003c4.54 0 8.235-3.695 8.237-8.237a8.189 8.189 0 00-2.41-5.828 8.182 8.182 0 00-5.824-2.416c-4.544 0-8.239 3.695-8.241 8.237a8.222 8.222 0 001.259 4.384l.196.312-.832 3.04 3.118-.819zm9.49-4.554c-.062-.103-.227-.165-.475-.289-.248-.124-1.465-.723-1.692-.806-.227-.083-.392-.124-.557.124-.165.248-.64.806-.784.971-.144.165-.289.186-.536.062-.248-.124-1.046-.385-1.991-1.229-.736-.657-1.233-1.468-1.378-1.715-.144-.248-.015-.382.109-.505.111-.111.248-.289.371-.434.124-.145.165-.248.248-.413.083-.165.041-.31-.021-.434s-.557-1.343-.763-1.839c-.202-.483-.407-.417-.559-.425-.144-.007-.31-.009-.475-.009a.91.91 0 00-.66.31c-.226.248-.866.847-.866 2.066 0 1.219.887 2.396 1.011 2.562.124.165 1.746 2.666 4.23 3.739.591.255 1.052.408 1.412.522.593.189 1.133.162 1.56.098.476-.071 1.465-.599 1.671-1.177.206-.58.206-1.075.145-1.179z"/></g></svg>';
break;
case 'woocommerce':
$svg = '<svg class="social-logo social-logo-woocommerce" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M19 2H5C3.3 2 2 3.3 2 5v11c0 1.7 1.3 3 3 3h4l6 3-1-3h5c1.7 0 3-1.3 3-3V5c0-1.7-1.3-3-3-3zm-1.6 4.5c-.4.8-.8 2.1-1 3.9-.3 1.8-.4 3.1-.3 4.1 0 .3 0 .5-.1.7s-.3.4-.6.4-.6-.1-.9-.4c-1-1-1.8-2.6-2.4-4.6-.7 1.4-1.2 2.4-1.6 3.1-.6 1.2-1.2 1.8-1.6 1.9-.3 0-.5-.2-.8-.7-.5-1.4-1.1-4.2-1.7-8.2 0-.3 0-.5.2-.7.1-.2.4-.3.7-.4.5 0 .9.2.9.8.3 2.3.7 4.2 1.1 5.7l2.4-4.5c.2-.4.4-.6.8-.6.5 0 .8.3.9.9.3 1.4.6 2.6 1 3.7.3-2.7.8-4.7 1.4-5.9.2-.3.4-.5.7-.5.2 0 .5.1.7.2.2.2.3.4.3.6s0 .4-.1.5z"/></g></svg>';
break;
case 'wordpress': // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText
$svg = '<svg class="social-logo social-logo-wordpress" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12.158 12.786L9.46 20.625a8.984 8.984 0 005.526-.144.852.852 0 01-.065-.124l-2.763-7.571zM3.009 12a8.993 8.993 0 005.067 8.092L3.788 8.341A8.952 8.952 0 003.009 12zm15.06-.454c0-1.112-.399-1.881-.741-2.48-.456-.741-.883-1.368-.883-2.109 0-.826.627-1.596 1.51-1.596.04 0 .078.005.116.007A8.963 8.963 0 0012 3.009a8.982 8.982 0 00-7.512 4.052c.211.007.41.011.579.011.94 0 2.396-.114 2.396-.114.484-.028.541.684.057.741 0 0-.487.057-1.029.085l3.274 9.739 1.968-5.901-1.401-3.838c-.484-.028-.943-.085-.943-.085-.485-.029-.428-.769.057-.741 0 0 1.484.114 2.368.114.94 0 2.397-.114 2.397-.114.485-.028.542.684.057.741 0 0-.488.057-1.029.085l3.249 9.665.897-2.996c.456-1.169.684-2.137.684-2.907zm1.82-3.86c.039.286.06.593.06.924 0 .912-.171 1.938-.684 3.22l-2.746 7.94a8.984 8.984 0 004.47-7.771 8.922 8.922 0 00-1.1-4.313zM12 22C6.486 22 2 17.514 2 12S6.486 2 12 2s10 4.486 10 10-4.486 10-10 10z"/></g></svg>';
break;
case 'x':
$svg = '<svg class="social-logo social-logo-x" height="24" width="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M13.982 10.622L20.54 3h-1.554l-5.693 6.618L8.745 3H3.5l6.876 10.007L3.5 21h1.554l6.012-6.989L15.868 21h5.245l-7.131-10.378zm-2.128 2.474l-.697-.997-5.543-7.93H8l4.474 6.4.697.996 5.815 8.318h-2.387l-4.745-6.787z"/></g></svg>';
break;
case 'xanga':
$svg = '<svg class="social-logo social-logo-xanga" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M9 9h6v6H9V9zM3 9h6V3H3v6zm12 0h6V3h-6v6zm0 12h6v-6h-6v6zM3 21h6v-6H3v6z"/></g></svg>';
break;
case 'youtube':
$svg = '<svg class="social-logo social-logo-youtube" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M21.8 8.001s-.195-1.378-.795-1.985c-.76-.797-1.613-.801-2.004-.847-2.799-.202-6.997-.202-6.997-.202h-.009s-4.198 0-6.997.202c-.39.047-1.242.051-2.003.847-.6.607-.795 1.985-.795 1.985S2 9.62 2 11.238v1.517c0 1.618.2 3.237.2 3.237s.195 1.378.795 1.985c.761.797 1.76.771 2.205.855 1.6.153 6.8.201 6.8.201s4.203-.006 7.001-.209c.391-.047 1.243-.051 2.004-.847.6-.607.795-1.985.795-1.985s.2-1.618.2-3.237v-1.517c0-1.618-.2-3.237-.2-3.237zM9.935 14.594l-.001-5.62 5.404 2.82-5.403 2.8z"/></g></svg>';
break;
default:
$svg = 'Need to specify a social logo';
break;
}
return $svg;
}
@@ -0,0 +1,353 @@
<?php
/**
* Sharing Buttons Block.
*
* @since 13.1
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Sharing_Button_Block;
use Automattic\Jetpack\Blocks;
use Automattic\Jetpack\Modules;
use Automattic\Jetpack\Status\Host;
use Jetpack_Gutenberg;
use WP_Block_Template;
require_once __DIR__ . '/class-sharing-source-block.php';
require_once __DIR__ . '/components/social-icons.php';
const PARENT_BLOCK_NAME = 'jetpack/sharing-buttons';
const INNER_BLOCK_NAME = 'jetpack/sharing-button';
/**
* 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(
__DIR__,
array( 'render_callback' => __NAMESPACE__ . '\render_block' )
);
/*
* Automatically add the sharing block to the end of single posts.
*/
add_filter( 'hooked_block_types', __NAMESPACE__ . '\add_block_to_single_posts_template', 10, 4 );
add_filter( 'hooked_block_' . PARENT_BLOCK_NAME, __NAMESPACE__ . '\add_default_services_to_block', 10, 5 );
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* Sharing Buttons block registration/dependency declaration.
*
* @param array $attr Array containing the Sharing Buttons block attributes.
* @param string $content String containing the Sharing Buttons block content.
* @param object $block Object containing block data.
*
* @return string
*/
function render_block( $attr, $content, $block ) {
$service_name = $attr['service'];
$title = $attr['label'] ?? $service_name;
$icon = get_social_logo( $service_name );
$style_type = $block->context['styleType'] ?? 'icon-text';
$post_id = $block->context['postId'] ?? 0;
$data_shared = sprintf(
'sharing-%1$s-%2$d',
$service_name,
$post_id
);
$services = get_services();
if ( ! array_key_exists( $service_name, $services ) ) {
return $content;
}
$service = new $services[ $service_name ]( $service_name, array() );
$link_props = $service->get_link(
$post_id,
'share=' . esc_attr( $service_name ) . '&nb=1',
esc_attr( $data_shared )
);
$link_url = $link_props['url'];
$link_classes = sprintf(
'jetpack-sharing-button__button style-%1$s share-%2$s',
$style_type,
$service_name
);
$link_aria_label = sprintf(
/* translators: %s refers to a string representation of sharing service, e.g. Facebook */
esc_html__( 'Share on %s', 'jetpack' ),
esc_html( $title )
);
$link_props = $service->get_link(
$post_id,
'share=' . esc_attr( $service_name ) . '&nb=1',
false,
esc_attr( $data_shared )
);
$block_class_name = 'jetpack-sharing-button__list-item';
if ( $service_name === 'share' ) {
$block_class_name .= ' tooltip';
/* translators: aria label for SMS sharing button */
$link_aria_label = esc_attr__( 'Share using Native tools', 'jetpack' );
$link_props = $service->get_link( $post_id );
}
$link_url = $link_props['url'];
$link_classes = sprintf(
'jetpack-sharing-button__button style-%1$s share-%2$s',
$style_type,
$service_name
);
$styles = array();
if (
array_key_exists( 'iconColorValue', $block->context )
&& ! empty( $block->context['iconColorValue'] )
) {
$styles['color'] = $block->context['iconColorValue'];
}
if (
array_key_exists( 'iconBackgroundColorValue', $block->context )
&& ! empty( $block->context['iconBackgroundColorValue'] )
) {
$styles['background-color'] = $block->context['iconBackgroundColorValue'];
}
$link_styles = '';
foreach ( $styles as $property => $value ) {
$link_styles .= $property . ':' . $value . ';';
}
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
$component = '<li class="' . esc_attr( $block_class_name ) . '">';
$component .= sprintf(
'<a href="%1$s" target="_blank" rel="nofollow noopener noreferrer" class="%2$s" style="%3$s" data-service="%4$s" data-shared="%5$s" aria-label="%6$s">',
esc_url( $link_url ),
esc_attr( $link_classes ),
esc_attr( $link_styles ),
esc_attr( $service_name ),
esc_attr( $data_shared ),
esc_attr( $link_aria_label )
);
$component .= $style_type !== 'text' ? $icon : '';
$component .= '<span class="jetpack-sharing-button__service-label" aria-hidden="true">' . esc_html( $title ) . '</span>';
if ( $service_name === 'share' ) {
$component .= '<span class="tooltiptext" aria-live="assertive">' . esc_html__( 'Copied to clipboard', 'jetpack' ) . '</span>';
}
$component .= '</a>';
$component .= '</li>';
return $component;
}
/**
* Get services for the Sharing Buttons block.
*
* @return array Array of services.
*/
function get_services() {
$services = array(
'print' => Share_Print_Block::class,
'mail' => Share_Email_Block::class,
'facebook' => Share_Facebook_Block::class,
'linkedin' => Share_LinkedIn_Block::class,
'reddit' => Share_Reddit_Block::class,
'twitter' => Share_Twitter_Block::class,
'tumblr' => Share_Tumblr_Block::class,
'pinterest' => Share_Pinterest_Block::class,
'pocket' => Share_Pocket_Block::class,
'telegram' => Share_Telegram_Block::class,
'threads' => Share_Threads_Block::class,
'whatsapp' => Jetpack_Share_WhatsApp_Block::class,
'mastodon' => Share_Mastodon_Block::class,
'nextdoor' => Share_Nextdoor_Block::class,
'bluesky' => Share_Bluesky_Block::class,
'x' => Share_X_Block::class,
'share' => Share_Native_Block::class,
);
return $services;
}
/**
* Launch sharing requests on page load when a specific query string is used.
*
* @return void
*/
function sharing_process_requests() {
global $post;
// Only process if: single post and share={service} defined
if ( ( is_page() || is_single() ) && isset( $_GET['share'] ) && is_string( $_GET['share'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$services = get_services();
$service_name = sanitize_text_field( wp_unslash( $_GET['share'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
// Only allow services that have been defined in get_services().
if ( ! array_key_exists( $service_name, $services ) ) {
return;
}
$service = new $services[ ( $service_name ) ]( $service_name, array() ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( $service ) {
$service->process_request( $post, $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
}
}
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only checking for the data being present.
if ( isset( $_GET['share'] ) ) {
add_action( 'template_redirect', __NAMESPACE__ . '\sharing_process_requests', 9 );
}
/**
* Automatically add the Sharing Buttons block to the end of the Single Posts template.
*
* @since 13.2
*
* @param array $hooked_block_types The list of hooked block types.
* @param string $relative_position The relative position of the hooked blocks. Can be one of 'before', 'after', 'first_child', or 'last_child'.
* @param string $anchor_block_type The anchor block type.
* @param WP_Block_Template|array $context The block template, template part, or pattern that the anchor block belongs to.
*
* @return array
*/
function add_block_to_single_posts_template( $hooked_block_types, $relative_position, $anchor_block_type, $context ) {
// Add the block at the end of the post content.
if (
'after' !== $relative_position
|| 'core/post-content' !== $anchor_block_type
) {
return $hooked_block_types;
}
// Only automate the addition of the block in block-based themes.
if ( ! wp_is_block_theme() ) {
return $hooked_block_types;
}
// Proceed if the user has toggled the auto-addition in Jetpack settings.
if ( ! get_option( 'jetpack_sharing_buttons_auto_add' ) ) {
return $hooked_block_types;
}
/*
* The Sharing module must be disabled.
* We do not want to automatically insert sharing buttons twice.
* On WordPress.com Simple the module is always active so we must check differently.
* There, we check if buttons are enabled on single posts and pages.
*/
if ( ( new Host() )->is_wpcom_simple() ) {
if ( ! class_exists( 'Sharing_Service' ) ) {
include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php';
}
$sharer = new \Sharing_Service();
$global = $sharer->get_global_options();
if (
! $global['show']
|| in_array( 'post', $global['show'], true )
|| in_array( 'page', $global['show'], true )
) {
return $hooked_block_types;
}
} elseif ( ( new Modules() )->is_active( 'sharedaddy' ) ) {
return $hooked_block_types;
}
// Only hook into page and single post templates.
if (
! $context instanceof WP_Block_Template
|| ! property_exists( $context, 'slug' )
|| empty( $context->slug )
|| ! preg_match( '/^(page|single)/', $context->slug )
) {
return $hooked_block_types;
}
$hooked_block_types[] = PARENT_BLOCK_NAME;
return $hooked_block_types;
}
/**
* Add default services to the block we add to the post content by default.
*
* @since 13.2
*
* @param array $parsed_hooked_block The parsed block array for the given hooked block type.
* @param string $hooked_block_type The hooked block type name.
* @param string $relative_position The relative position of the hooked block.
* @param array $parsed_anchor_block The anchor block, in parsed block array format.
* @param WP_Block_Template|array $context The block template, template part, or pattern that the anchor block.
*
* @return array
*/
function add_default_services_to_block( $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
// Is the hooked block adjacent to the anchor block?
if ( 'after' !== $relative_position ) {
return $parsed_hooked_block;
}
// Use the icon style by default.
$parsed_hooked_block['attrs']['styleType'] = 'icon';
// Add default services (inner blocks) to the block.
$parsed_hooked_block['innerBlocks'] = array(
array(
'blockName' => INNER_BLOCK_NAME,
'innerContent' => array(),
'attrs' => array(
'service' => 'facebook',
'label' => esc_html__( 'Facebook', 'jetpack' ),
),
),
array(
'blockName' => INNER_BLOCK_NAME,
'innerContent' => array(),
'attrs' => array(
'service' => 'x',
'label' => esc_html__( 'X', 'jetpack' ),
),
),
array(
'blockName' => INNER_BLOCK_NAME,
'innerContent' => array(),
'attrs' => array(
'service' => 'mastodon',
'label' => esc_html__( 'Mastodon', 'jetpack' ),
),
),
);
// Wrap inner blocks in our sharing buttons markup.
$parsed_hooked_block['innerContent'] = array(
'<ul class="wp-block-jetpack-sharing-buttons has-normal-icon-size jetpack-sharing-buttons__services-list" id="jetpack-sharing-serivces-list">',
null,
null,
null,
'</ul>',
);
// Wrap the whole thing in a group block.
return array(
'blockName' => 'core/group',
'attrs' => array(
// Does the anchor block have a layout attribute? If so, use it in the group to maintain the same alignment.
'layout' => $parsed_anchor_block['attrs']['layout'] ?? 'null',
),
'innerBlocks' => array( $parsed_hooked_block ),
'innerContent' => array(
'<div class="wp-block-group">',
null,
'</div>',
),
);
}
@@ -0,0 +1,41 @@
<?php
/**
* Sharing Buttons Block.
*
* @since 11.x
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Sharing_Buttons;
use Automattic\Jetpack\Blocks;
/**
* 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(
__DIR__,
array( 'render_callback' => __NAMESPACE__ . '\render_block' )
);
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* Sharing Buttons block registration/dependency declaration.
*
* @param array $attr Array containing the Sharing Buttons block attributes.
* @param string $content String containing the Sharing Buttons block content.
*
* @return string
*/
function render_block( $attr, $content ) {
// Render nothing in other contexts than frontend (i.e. feed, emails, API, etc.).
if ( ! jetpack_is_frontend() ) {
return '';
}
return $content;
}
@@ -0,0 +1,102 @@
<?php
/**
* Pay with PayPal block (aka Simple Payments).
*
* @since 9.0.0
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\SimplePayments;
use Automattic\Jetpack\Blocks;
use Jetpack_Simple_Payments;
use WP_Post;
const FEATURE_NAME = 'simple-payments';
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(
__DIR__,
array(
'render_callback' => __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::get_instance();
if ( ! $simple_payments->is_valid( $attr ) ) {
return '';
}
$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(
'#(<a class="jetpack-simple-payments-purchase".*)rel="(.*)"(.*>.*</a>)#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( '#<a class="jetpack-simple-payments-purchase(.*)</a>#i', $purchase_box, $content );
return $content;
}
/**
* Determine if AMP should be disabled on posts having "Pay with PayPal" 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 purchase 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 );
@@ -0,0 +1,225 @@
<?php
/**
* Slideshow Block.
*
* @since 7.1.0
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Slideshow;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
/**
* 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(
__DIR__,
array( 'render_callback' => __NAMESPACE__ . '\load_assets' )
);
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* Slideshow block registration/dependency declaration.
*
* @param array $attr Array containing the slideshow block attributes.
* @param string $content String containing the slideshow block content.
*
* @return string
*/
function load_assets( $attr, $content ) {
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
if ( Blocks::is_amp_request() ) {
return render_amp( $attr );
}
return $content;
}
/**
* Render slideshow block for AMP
*
* @param array $attr Array containing the slideshow block attributes.
*
* @return string
*/
function render_amp( $attr ) {
if ( empty( $attr['ids'] ) ) {
return '';
}
static $wp_block_jetpack_slideshow_id = 0;
++$wp_block_jetpack_slideshow_id;
$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 = Blocks::classes( Blocks::get_block_feature( __DIR__ ), $attr, $extras );
return sprintf(
'<div class="%1$s" id="wp-block-jetpack-slideshow__%2$d"><div class="wp-block-jetpack-slideshow_container swiper-container">%3$s%4$s%5$s</div></div>',
esc_attr( $classes ),
absint( $wp_block_jetpack_slideshow_id ),
amp_carousel( $attr, $wp_block_jetpack_slideshow_id ),
$autoplay ? autoplay_ui( $wp_block_jetpack_slideshow_id ) : '',
render_paginator( $ids, $wp_block_jetpack_slideshow_id )
);
}
/**
* Generate amp-carousel markup
*
* @param array $attr Array of block attributes.
* @param int $block_ordinal The ordinal number of the block, used in unique ID.
*
* @return string amp-carousel markup.
*/
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'] );
$autoplay = empty( $attr['autoplay'] ) ? false : $attr['autoplay'];
$width = empty( $first_image['width'] ) ? 800 : $first_image['width'];
$height = empty( $first_image['height'] ) ? 600 : $first_image['height'];
return sprintf(
'<amp-carousel width="%1$d" height="%2$d" layout="responsive" type="slides" data-next-button-aria-label="%3$s" data-prev-button-aria-label="%4$s" controls loop %5$s id="wp-block-jetpack-slideshow__amp-carousel__%6$s" on="slideChange:wp-block-jetpack-slideshow__amp-pagination__%6$s.toggle(index=event.index, value=true)">%7$s</amp-carousel>',
esc_attr( $width ),
esc_attr( $height ),
esc_attr__( 'Next Slide', 'jetpack' ),
esc_attr__( 'Previous Slide', 'jetpack' ),
$autoplay ? 'autoplay delay=' . esc_attr( $delay * 1000 ) : '',
absint( $block_ordinal ),
implode( '', slides( $ids, $width, $height ) )
);
}
/**
* Generate array of slides markup
*
* @param array $ids Array of image ids.
* @param int $width Width of the container.
* @param int $height Height of the container.
*
* @return array Array of slides markup.
*/
function slides( $ids = array(), $width = 400, $height = 300 ) {
return array_map(
function ( $id ) use ( $width, $height ) {
$caption = wp_get_attachment_caption( $id );
$figcaption = $caption ? sprintf(
'<figcaption class="wp-block-jetpack-slideshow_caption gallery-caption">%s</figcaption>',
wp_kses_post( $caption )
) : '';
$image = wp_get_attachment_image(
$id,
array( $width, $height ),
false,
array(
'class' => 'wp-block-jetpack-slideshow_image',
'object-fit' => 'contain',
)
);
return sprintf(
'<div class="wp-block-jetpack-slideshow_slide"><figure>%s%s</figure></div>',
$image,
$figcaption
);
},
$ids
);
}
/**
* Render blocks paginator section
*
* @param array $ids Array of image ids.
* @param int $block_ordinal The ordinal number of the block, used in unique ID.
*
* @return array Array of bullets markup.
*/
function render_paginator( $ids = array(), $block_ordinal = 0 ) {
$total = count( $ids );
if ( $total < 6 ) {
return bullets( $ids, $block_ordinal );
}
return sprintf(
'<div class="swiper-pagination-simple">%s / %s</div>',
absint( $block_ordinal ),
absint( $total )
);
}
/**
* Generate array of bullets markup
*
* @param array $ids Array of image ids.
* @param int $block_ordinal The ordinal number of the block, used in unique ID.
*
* @return array Array of bullets markup.
*/
function bullets( $ids = array(), $block_ordinal = 0 ) {
$buttons = array_map(
function ( $index ) {
$aria_label = sprintf(
/* translators: %d: Slide number. */
__( 'Go to slide %d', 'jetpack' ),
absint( $index + 1 )
);
return sprintf(
'<button option="%d" class="swiper-pagination-bullet" tabindex="0" role="button" aria-label="%s" %s></button>',
absint( $index ),
esc_attr( $aria_label ),
0 === $index ? 'selected' : ''
);
},
array_keys( $ids )
);
return sprintf(
'<amp-selector id="wp-block-jetpack-slideshow__amp-pagination__%1$d" class="wp-block-jetpack-slideshow_pagination swiper-pagination swiper-pagination-custom amp-pagination" on="select:wp-block-jetpack-slideshow__amp-carousel__%1$d.goToSlide(index=event.targetOption)" layout="container">%2$s</amp-selector>',
absint( $block_ordinal ),
implode( '', $buttons )
);
}
/**
* Generate autoplay play/pause UI.
*
* @param int $block_ordinal The ordinal number of the block, used in unique ID.
*
* @return string Autoplay UI markup.
*/
function autoplay_ui( $block_ordinal = 0 ) {
$block_id = sprintf(
'wp-block-jetpack-slideshow__%d',
absint( $block_ordinal )
);
$amp_carousel_id = sprintf(
'wp-block-jetpack-slideshow__amp-carousel__%d',
absint( $block_ordinal )
);
$autoplay_pause = sprintf(
'<a aria-label="%s" class="wp-block-jetpack-slideshow_button-pause" role="button" on="tap:%s.toggleAutoplay(toggleOn=false),%s.toggleClass(class=wp-block-jetpack-slideshow__autoplay-playing,force=false)"></a>',
esc_attr__( 'Pause Slideshow', 'jetpack' ),
esc_attr( $amp_carousel_id ),
esc_attr( $block_id )
);
$autoplay_play = sprintf(
'<a aria-label="%s" class="wp-block-jetpack-slideshow_button-play" role="button" on="tap:%s.toggleAutoplay(toggleOn=true),%s.toggleClass(class=wp-block-jetpack-slideshow__autoplay-playing,force=true)"></a>',
esc_attr__( 'Play Slideshow', 'jetpack' ),
esc_attr( $amp_carousel_id ),
esc_attr( $block_id )
);
return $autoplay_pause . $autoplay_play;
}
@@ -0,0 +1,501 @@
<?php
/**
* Story Block.
*
* @since 8.6.1
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Story;
use Automattic\Jetpack\Blocks;
use Automattic\Jetpack\Connection\Connection_Assets;
use Jetpack;
use Jetpack_Gutenberg;
use Jetpack_PostImages;
const EMBED_SIZE = array( 360, 640 ); // twice as many pixels for retina displays.
const CROP_UP_TO = 0.2;
const MAX_BULLETS = 7;
const IMAGE_BREAKPOINTS = '(max-width: 460px) 576w, (max-width: 614px) 768w, 120vw'; // 120vw to match the 20% CROP_UP_TO ratio
/**
* 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(
__DIR__,
array( 'render_callback' => __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.
*
* @return 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.
*
* @return 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'] && str_starts_with( $media_file['mime'], 'video' ) ) {
$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.
*
* @return 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.
*
* @return 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.
*
* @return 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(
'<img
title="%1$s"
alt="%2$s"
class="wp-block-jetpack-story_image wp-story-image %3$s"
src="%4$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.
*
* @return string The CSS class which will display a cropped image
*/
function get_image_crop_class( $width, $height ) {
$crop_class = '';
$width = (int) $width;
$height = (int) $height;
if ( ! $width || ! $height ) {
return $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.
*
* @return 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.
*
* @return 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(
'<video
title="%1$s"
type="%2$s"
class="wp-story-video intrinsic-ignore wp-video-%3$s"
data-id="%3$d"
src="%4$s">
</video>',
esc_attr( get_the_title( $media['id'] ) ),
esc_attr( $media['mime'] ),
absint( $media['id'] ),
esc_attr( $media['url'] )
);
}
/**
* Pick a thumbnail to render a static/embedded story
*
* @param array $media_files - list of Media files.
*
* @return 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(
'<div class="wp-story-slide" style="display: block;">
<figure>%s</figure>
</div>',
$media_template
);
}
/**
* Render the top right icon on top of the story embed
*
* @param array $settings - The block settings.
*
* @return 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(
'<div class="wp-story-embed-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" role="img" aria-hidden="true" focusable="false">
<path d="M0 0h24v24H0z" fill="none"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 3H14V17H6L6 3ZM4 3C4 1.89543 4.89543 1 6 1H14C15.1046 1 16 1.89543 16 3V17C16 18.1046 15.1046 19 14 19H6C4.89543 19 4 18.1046 4 17V3ZM18 5C19.1046 5 20 5.89543 20 7V21C20 22.1046 19.1046 23 18 23H10C8.89543 23 8 22.1046 8 21H18V5Z"></path>
</svg>
<span>%d</span>
</div>',
$slide_count
);
} else {
// Render the Fullscreen Gridicon.
return (
'<div class="wp-story-embed-icon-expand">
<svg class="gridicon gridicons-fullscreen" role="img" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path d="M21 3v6h-2V6.41l-3.29 3.3-1.42-1.42L17.59 5H15V3zM3 3v6h2V6.41l3.29 3.3 1.42-1.42L6.41 5H9V3zm18 18v-6h-2v2.59l-3.29-3.29-1.41 1.41L17.59 19H15v2zM9 21v-2H6.41l3.29-3.29-1.41-1.42L5 17.59V15H3v6z"></path>
</g>
</svg>
</div>'
);
}
}
/**
* 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.
*
* @return string
*/
function render_pagination_bullet( $slide_index, $class_name = '' ) {
return sprintf(
'<div class="wp-story-pagination-bullet %s" aria-label="%s">
<div class="wp-story-pagination-bullet-bar"></div>
</div>',
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.
*
* @return 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(
'<div class="wp-story-pagination wp-story-pagination-bullets">
%s
</div>',
implode( "\n", array_map( __NAMESPACE__ . '\render_pagination_bullet', range( 1, $bullet_count ) ) ) . $bullet_ellipsis
);
}
/**
* Render story block
*
* @param array $attributes - Block attributes.
*
* @return 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;
if ( 0 === $story_block_counter ) {
// @todo Fix the webpack tree shaking so the block's view.js no longer depends on jetpack-connection, then remove this.
Connection_Assets::register_assets();
}
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
$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(
'<div class="%1$s" data-id="%2$s" data-settings="%3$s">
<div class="wp-story-app">
<div class="wp-story-display-contents" style="display: contents;">
<a class="wp-story-container" href="%4$s" title="%5$s">
<div class="wp-story-meta">
<div class="wp-story-icon">
<img alt="%6$s" src="%7$s" width="40" height="40">
</div>
<div>
<div class="wp-story-title">
%8$s
</div>
</div>
</div>
<div class="wp-story-wrapper">
%9$s
</div>
<div class="wp-story-overlay">
%10$s
</div>
%11$s
</a>
</div>
</div>
</div>',
esc_attr( Blocks::classes( Blocks::get_block_feature( __DIR__ ), $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&amp;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 )
);
}
@@ -0,0 +1,96 @@
<?php
/**
* Adds support for Jetpack Subscription Site feature.
*
* @package automattic/jetpack
* @since 13.3
*/
namespace Automattic\Jetpack\Extensions\Subscriber_Login;
use WP_Block_Template;
use WP_Post;
/**
* Jetpack_Subscription_Site class.
*/
class Jetpack_Subscription_Site {
/**
* Jetpack_Subscription_Site singleton instance.
*
* @var Jetpack_Subscription_Site|null
*/
private static $instance;
/**
* Jetpack_Subscription_Site instance init.
*/
public static function init() {
if ( self::$instance === null ) {
self::$instance = new Jetpack_Subscription_Site();
}
return self::$instance;
}
/**
* Handles Subscriber Login block placements.
*
* @return void
*/
public function handle_subscriber_login_block_placements() {
$this->handle_subscriber_login_block_navigation_placement();
}
/**
* Returns true if context is recognized as a header element.
*
* @param WP_Block_Template|WP_Post|array $context The block template, template part, or pattern the anchor block belongs to.
*
* @return bool
*/
protected function is_header_context( $context ) {
if ( $context instanceof WP_Post && $context->post_type === 'wp_navigation' ) {
return true;
}
if ( $context instanceof WP_Block_Template && $context->area === 'header' ) {
return true;
}
return false;
}
/**
* Handles Subscriber Login block navigation placement.
*
* @return void
*/
protected function handle_subscriber_login_block_navigation_placement() {
$subscriber_login_navigation_enabled = get_option( 'jetpack_subscriptions_login_navigation_enabled', false );
if ( ! $subscriber_login_navigation_enabled ) {
return;
}
if ( ! wp_is_block_theme() ) { // TODO Fallback for classic themes.
return;
}
add_filter(
'hooked_block_types',
function ( $hooked_blocks, $relative_position, $anchor_block, $context ) {
if (
$anchor_block === 'core/navigation' &&
$relative_position === 'last_child' &&
self::is_header_context( $context )
) {
$hooked_blocks[] = 'jetpack/subscriber-login';
}
return $hooked_blocks;
},
10,
4
);
}
}
@@ -0,0 +1,155 @@
<?php
/**
* Subscriber Login Block.
*
* @since 13.1
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Subscriber_Login;
use Automattic\Jetpack\Blocks;
use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Abstract_Token_Subscription_Service;
use Automattic\Jetpack\Status\Host;
use Jetpack;
use Jetpack_Gutenberg;
use Jetpack_Memberships;
use Jetpack_Options;
require_once __DIR__ . '/class-jetpack-subscription-site.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() {
if (
! Jetpack::is_module_active( 'subscriptions' ) ||
! class_exists( 'Jetpack_Memberships' ) ||
! class_exists( 'Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Abstract_Token_Subscription_Service' )
) {
return;
}
Blocks::jetpack_register_block(
__DIR__,
array( 'render_callback' => __NAMESPACE__ . '\render_block' )
);
add_filter(
'jetpack_options_whitelist',
function ( $options ) {
$options[] = 'jetpack_subscriptions_login_navigation_enabled';
return $options;
}
);
// If called via REST API, we need to register later in the lifecycle
if ( ( new Host() )->is_wpcom_platform() && ! jetpack_is_frontend() ) {
add_action(
'restapi_theme_init',
function () {
Jetpack_Subscription_Site::init()->handle_subscriber_login_block_placements();
}
);
} else {
Jetpack_Subscription_Site::init()->handle_subscriber_login_block_placements();
}
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* Returns current URL.
*
* @return string
*/
function get_current_url() {
if ( ! isset( $_SERVER['HTTP_HOST'] ) || ! isset( $_SERVER['REQUEST_URI'] ) ) {
return '';
}
return ( is_ssl() ? 'https://' : 'http://' ) . wp_unslash( $_SERVER['HTTP_HOST'] ) . wp_unslash( $_SERVER['REQUEST_URI'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
}
/**
* Returns subscriber log in URL.
*
* @param string $redirect Path to redirect to on login.
*
* @return string
*/
function get_subscriber_login_url( $redirect ) {
$redirect = ! empty( $redirect ) ? $redirect : get_site_url();
if ( ( new Host() )->is_wpcom_simple() ) {
// On WPCOM we will redirect immediately
return wpcom_logmein_redirect_url( $redirect, false, null, 'link', get_current_blog_id() );
}
// On self-hosted we will save and hide the token
$redirect_url = get_site_url() . '/wp-json/jetpack/v4/subscribers/auth';
$redirect_url = add_query_arg( 'redirect_url', $redirect, $redirect_url );
return add_query_arg(
array(
'site_id' => intval( Jetpack_Options::get_option( 'id' ) ),
'redirect_url' => rawurlencode( $redirect_url ),
),
'https://subscribe.wordpress.com/memberships/jwt/'
);
}
/**
* Determines whether the current visitor is a logged in user or a subscriber.
*
* @return bool
*/
function is_subscriber_logged_in() {
return is_user_logged_in() || Abstract_Token_Subscription_Service::has_token_from_cookie();
}
/**
* Renders Subscriber Login block.
*
* @param array $attributes The block attributes.
*
* @return string
*/
function render_block( $attributes ) {
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
$block_template = '<div %1$s><a href="%2$s">%3$s</a></div>';
$redirect_url = ! empty( $attributes['redirectToCurrent'] ) ? get_current_url() : get_site_url();
$log_in_label = ! empty( $attributes['logInLabel'] ) ? sanitize_text_field( $attributes['logInLabel'] ) : esc_html__( 'Log in', 'jetpack' );
$log_out_label = ! empty( $attributes['logOutLabel'] ) ? sanitize_text_field( $attributes['logOutLabel'] ) : esc_html__( 'Log out', 'jetpack' );
$show_manage_link = ! empty( $attributes['showManageSubscriptionsLink'] );
$manage_subscriptions_label = ! empty( $attributes['manageSubscriptionsLabel'] ) ? sanitize_text_field( $attributes['manageSubscriptionsLabel'] ) : esc_html__( 'Manage subscription', 'jetpack' );
if ( ! is_subscriber_logged_in() ) {
return sprintf(
$block_template,
get_block_wrapper_attributes(),
get_subscriber_login_url( $redirect_url ),
$log_in_label
);
}
if ( $show_manage_link && Jetpack_Memberships::is_current_user_subscribed() ) {
return sprintf(
$block_template,
get_block_wrapper_attributes(),
'https://wordpress.com/read/site/subscription/' . Jetpack_Memberships::get_blog_id(),
$manage_subscriptions_label
);
}
return sprintf(
$block_template,
get_block_wrapper_attributes(),
wp_logout_url( $redirect_url ),
$log_out_label
);
}
@@ -0,0 +1,326 @@
<?php
/**
* Adds support for Jetpack Subscription Site feature.
*
* @package automattic/jetpack
* @since 13.2
*/
namespace Automattic\Jetpack\Extensions\Subscriptions;
use Jetpack_Memberships;
use WP_Block_Template;
use WP_Post;
/**
* Jetpack_Subscription_Site class.
*/
class Jetpack_Subscription_Site {
/**
* Jetpack_Subscription_Site singleton instance.
*
* @var Jetpack_Subscription_Site|null
*/
private static $instance;
/**
* Jetpack_Subscription_Site instance init.
*/
public static function init() {
if ( self::$instance === null ) {
self::$instance = new Jetpack_Subscription_Site();
}
return self::$instance;
}
/**
* Handles Subscribe block placements.
*
* @return void
*/
public function handle_subscribe_block_placements() {
$this->handle_subscribe_block_post_end_placement();
$this->handle_subscribe_block_navigation_placement();
}
/**
* Returns true if current user can view the post.
*
* @return bool
*/
protected function user_can_view_post() {
if ( ! class_exists( 'Jetpack_Memberships' ) ) {
return true;
}
return Jetpack_Memberships::user_can_view_post();
}
/**
* Returns post end placement hooked block attributes.
*
* @param array $default_attrs Deafult attributes.
* @param array $anchor_block The anchor block, in parsed block array format.
*
* @return array
*/
protected function get_post_end_placement_block_attributes( $default_attrs, $anchor_block ) {
if ( ! empty( $anchor_block['attrs']['layout']['type'] ) ) {
return array_merge(
$default_attrs,
array(
'layout' => array(
'type' => $anchor_block['attrs']['layout']['type'],
),
)
);
}
if ( ! empty( $anchor_block['attrs']['layout']['inherit'] ) ) {
return array_merge(
$default_attrs,
array(
'layout' => array(
'inherit' => $anchor_block['attrs']['layout']['inherit'],
),
)
);
}
return $default_attrs;
}
/**
* Returns true if context is recognized as a header element.
*
* @param WP_Block_Template|WP_Post|array $context The block template, template part, or pattern the anchor block belongs to.
*
* @return bool
*/
protected function is_header_context( $context ) {
if ( $context instanceof WP_Post && $context->post_type === 'wp_navigation' ) {
return true;
}
if ( $context instanceof WP_Block_Template && $context->area === 'header' ) {
return true;
}
return false;
}
/**
* Returns true if context is recognized as a single post element.
*
* @param WP_Block_Template|WP_Post|array $context The block template, template part, or pattern the anchor block belongs to.
*
* @return bool
*/
protected function is_single_post_context( $context ) {
return $context instanceof WP_Block_Template && $context->slug === 'single';
}
/**
* Handles Subscription block navigation placement.
*
* @return void
*/
protected function handle_subscribe_block_navigation_placement() {
$is_enabled = get_option( 'jetpack_subscriptions_subscribe_navigation_enabled', false );
if ( ! $is_enabled ) {
return;
}
if ( ! wp_is_block_theme() ) { // TODO Fallback for classic themes.
return;
}
add_filter(
'hooked_block_types',
function ( $hooked_blocks, $relative_position, $anchor_block, $context ) {
if (
$anchor_block === 'core/navigation' &&
$relative_position === 'last_child' &&
self::is_header_context( $context )
) {
$hooked_blocks[] = 'jetpack/subscriptions';
}
return $hooked_blocks;
},
10,
4
);
add_filter(
'hooked_block_jetpack/subscriptions',
function ( $hooked_block, $hooked_block_type, $relative_position, $anchor_block ) {
$is_navigation_anchor_block = isset( $anchor_block['blockName'] ) && $anchor_block['blockName'] === 'core/navigation';
if ( $is_navigation_anchor_block ) {
$class_name = ( ! empty( $hooked_block['attrs'] ) && ! empty( $hooked_block['attrs']['className'] ) )
? $hooked_block['attrs']['className'] . ' is-style-button'
: 'is-style-button';
$hooked_block['attrs']['className'] = $class_name;
$hooked_block['attrs']['appSource'] = 'subscribe-block-navigation';
}
return $hooked_block;
},
10,
4
);
}
/**
* Handles Subscribe block placement at the end of each post.
*
* @return void
*/
protected function handle_subscribe_block_post_end_placement() {
$subscribe_post_end_enabled = get_option( 'jetpack_subscriptions_subscribe_post_end_enabled', false );
if ( ! $subscribe_post_end_enabled ) {
return;
}
if ( ! wp_is_block_theme() ) { // Fallback for classic themes.
add_filter(
'the_content',
function ( $content ) {
// Check if we're inside the main loop in a single Post.
if (
is_single() &&
in_the_loop() &&
is_main_query() &&
$this->user_can_view_post()
) {
// translators: %s is the name of the site.
$discover_more_from_text = sprintf( __( 'Discover more from %s', 'jetpack' ), get_bloginfo( 'name' ) );
$subscribe_text = __( 'Subscribe to get the latest posts sent to your email.', 'jetpack' );
return $content . do_blocks(
<<<HTML
<!-- wp:group {"style":{"spacing":{"padding":{"top":"0px","bottom":"0px","left":"0px","right":"0px"},"margin":{"top":"32px","bottom":"32px"}},"border":{"width":"0px","style":"none"}},"className":"has-border-color","layout":{"type":"default"}} -->
<div class="wp-block-group has-border-color" style="border-style:none;border-width:0px;margin-top:32px;margin-bottom:32px;padding-top:0px;padding-right:0px;padding-bottom:0px;padding-left:0px">
<!-- wp:separator {"style":{"spacing":{"margin":{"bottom":"24px"}}},"className":"is-style-wide"} -->
<hr class="wp-block-separator has-alpha-channel-opacity is-style-wide" style="margin-bottom:24px"/>
<!-- /wp:separator -->
<!-- wp:heading {"textAlign":"center","level":3,"style":{"layout":{"selfStretch":"fit","flexSize":null},"spacing":{"margin":{"top":"4px","bottom":"10px"}}}} -->
<h3 class="wp-block-heading has-text-align-center" style="margin-top:4px;margin-bottom:10px">$discover_more_from_text</h3>
<!-- /wp:heading -->
<!-- wp:paragraph {"align":"center","style":{"typography":{"fontSize":"15px"},"spacing":{"margin":{"top":"10px","bottom":"10px"}}}} -->
<p class="has-text-align-center" style="margin-top:10px;margin-bottom:10px;font-size:15px">$subscribe_text</p>
<!-- /wp:paragraph -->
<!-- wp:group {"layout":{"type":"constrained","contentSize":"480px"}} -->
<div class="wp-block-group">
<!-- wp:jetpack/subscriptions {"appSource":"subscribe-block-post-end"} /-->
</div>
<!-- /wp:group -->
</div>
<!-- /wp:group -->
HTML
);
}
return $content;
},
100, // To insert it after the sharing blocks.
1
);
return;
}
add_filter(
'hooked_block_types',
function ( $hooked_blocks, $relative_position, $anchor_block, $context ) {
if (
$anchor_block === 'core/post-content' &&
$relative_position === 'after' &&
self::is_single_post_context( $context ) &&
$this->user_can_view_post()
) {
$hooked_blocks[] = 'jetpack/subscriptions';
}
return $hooked_blocks;
},
10,
4
);
add_filter(
'hooked_block_jetpack/subscriptions',
function ( $hooked_block, $hooked_block_type, $relative_position, $anchor_block ) {
$is_post_content_anchor_block = isset( $anchor_block['blockName'] ) && $anchor_block['blockName'] === 'core/post-content';
if ( $is_post_content_anchor_block && ( $relative_position === 'after' || $relative_position === 'before' ) ) {
$attrs = $this->get_post_end_placement_block_attributes(
array(
'style' => array(
'spacing' => array(
'margin' => array(
'top' => '48px',
'bottom' => '48px',
),
'padding' => array(
'top' => '5px',
'bottom' => '5px',
),
),
),
),
$anchor_block
);
// translators: %s is the name of the site.
$discover_more_from_text = sprintf( __( 'Discover more from %s', 'jetpack' ), get_bloginfo( 'name' ) );
$subscribe_text = __( 'Subscribe to get the latest posts sent to your email.', 'jetpack' );
$inner_content_begin = <<<HTML
<div class="wp-block-group" style="margin-top:48px;margin-bottom:48px;padding-top:5px;padding-bottom:5px">
<!-- wp:separator {"style":{"spacing":{"margin":{"bottom":"36px"}}},"className":"is-style-wide"} -->
<hr class="wp-block-separator has-alpha-channel-opacity is-style-wide" style="margin-bottom:36px"/>
<!-- /wp:separator -->
<!-- wp:heading {"textAlign":"center","level":3,"style":{"layout":{"selfStretch":"fit","flexSize":null},"spacing":{"margin":{"top":"4px","bottom":"10px"}}}} -->
<h3 class="wp-block-heading has-text-align-center" style="margin-top:4px;margin-bottom:10px">$discover_more_from_text</h3>
<!-- /wp:heading -->
<!-- wp:paragraph {"align":"center","style":{"typography":{"fontSize":"15px"},"spacing":{"margin":{"top":"4px","bottom":"0px"}}}} -->
<p class="has-text-align-center" style="margin-top:4px;margin-bottom:0px;font-size:15px">$subscribe_text</p>
<!-- /wp:paragraph -->
<!-- wp:group {"layout":{"type":"constrained","contentSize":"480px"}} -->
<div class="wp-block-group">
HTML;
$inner_content_end = <<<HTML
</div>
<!-- /wp:group -->
</div>
HTML;
$hooked_block['attrs']['appSource'] = 'subscribe-block-post-end';
return array(
'blockName' => 'core/group',
'attrs' => $attrs,
'innerBlocks' => array( $hooked_block ),
'innerContent' => array(
$inner_content_begin,
null,
$inner_content_end,
),
);
}
return $hooked_block;
},
10,
4
);
}
}
@@ -0,0 +1,17 @@
<?php
/**
* Constants for the subscriptions Block.
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Subscriptions;
const FEATURE_NAME = 'subscriptions';
const BLOCK_NAME = 'jetpack/' . FEATURE_NAME;
const NEWSLETTER_COLUMN_ID = 'newsletter_access';
const META_NAME_FOR_POST_LEVEL_ACCESS_SETTINGS = '_jetpack_newsletter_access';
const META_NAME_FOR_POST_DONT_EMAIL_TO_SUBS = '_jetpack_dont_email_post_to_subs';
const META_NAME_FOR_POST_TIER_ID_SETTINGS = '_jetpack_newsletter_tier_id';
const META_NAME_CONTAINS_PAYWALLED_CONTENT = '_jetpack_memberships_contains_paywalled_content';
const META_NAME_CONTAINS_PAID_CONTENT = '_jetpack_memberships_contains_paid_content';
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,213 @@
<?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Tiled Gallery block.
* Relies on Photon, but can be used even when the module is not active.
*
* @since 6.9.0
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions;
use Automattic\Jetpack\Blocks;
use Automattic\Jetpack\Current_Plan as Jetpack_Plan;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Status\Host;
use Jetpack;
use Jetpack_Gutenberg;
/**
* Jetpack Tiled Gallery Block class
*
* @since 7.3
*/
class Tiled_Gallery {
/* Values for building srcsets */
const IMG_SRCSET_WIDTH_MAX = 2000;
const IMG_SRCSET_WIDTH_MIN = 600;
const IMG_SRCSET_WIDTH_STEP = 300;
/**
* Register the block
*/
public static function register() {
if (
( defined( 'IS_WPCOM' ) && IS_WPCOM )
|| Jetpack::is_connection_ready()
|| ( new Status() )->is_offline_mode()
) {
Blocks::jetpack_register_block(
__DIR__,
array(
'render_callback' => array( __CLASS__, 'render' ),
)
);
}
}
/**
* Tiled gallery block registration
*
* @param array $attr Array containing the block attributes.
* @param string $content String containing the block content.
*
* @return string
*/
public static function render( $attr, $content ) {
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
$is_squareish_layout = self::is_squareish_layout( $attr );
// For backward compatibility (ensuring Tiled Galleries using now deprecated versions of the block are not affected).
// See isVIP() in utils/index.js.
$jetpack_plan = Jetpack_Plan::get();
wp_localize_script( 'jetpack-gallery-settings', 'jetpack_plan', array( 'data' => $jetpack_plan['product_slug'] ) );
if ( preg_match_all( '/<img [^>]+>/', $content, $images ) ) {
/**
* This block processes all of the images that are found and builds $find and $replace.
*
* The original img is added to the $find array and the replacement is made and added
* to the $replace array. This is so that the same find and replace operations can be
* made on the entire $content.
*/
$find = array();
$replace = array();
foreach ( $images[0] as $image_html ) {
if (
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
// for the srcset.
$src_parts = explode( '?', $img_src[1], 2 );
$orig_src = $src_parts[0];
$orig_height = absint( $img_height[1] );
$orig_width = absint( $img_width[1] );
// Because URLs are already "photon", the photon function used short-circuits
// before ssl is added. Detect ssl and add is if necessary.
$is_ssl = ! empty( $src_parts[1] ) && str_contains( $src_parts[1], 'ssl=1' );
if ( ! $orig_width || ! $orig_height || ! $orig_src ) {
continue;
}
$srcset_parts = array();
if ( $is_squareish_layout ) {
$min_width = min( self::IMG_SRCSET_WIDTH_MIN, $orig_width, $orig_height );
$max_width = min( self::IMG_SRCSET_WIDTH_MAX, $orig_width, $orig_height );
for ( $w = $min_width; $w <= $max_width; $w = min( $max_width, $w + self::IMG_SRCSET_WIDTH_STEP ) ) {
$srcset_src = add_query_arg(
array(
'resize' => $w . ',' . $w,
'strip' => 'info',
),
$orig_src
);
if ( $is_ssl ) {
$srcset_src = add_query_arg( 'ssl', '1', $srcset_src );
}
$srcset_parts[] = esc_url( $srcset_src ) . ' ' . $w . 'w';
if ( $w >= $max_width ) {
break;
}
}
} else {
$min_width = min( self::IMG_SRCSET_WIDTH_MIN, $orig_width );
$max_width = min( self::IMG_SRCSET_WIDTH_MAX, $orig_width );
for ( $w = $min_width; $w <= $max_width; $w = min( $max_width, $w + self::IMG_SRCSET_WIDTH_STEP ) ) {
$srcset_src = add_query_arg(
array(
'strip' => 'info',
'w' => $w,
),
$orig_src
);
if ( $is_ssl ) {
$srcset_src = add_query_arg( 'ssl', '1', $srcset_src );
}
$srcset_parts[] = esc_url( $srcset_src ) . ' ' . $w . 'w';
if ( $w >= $max_width ) {
break;
}
}
}
if ( ! empty( $srcset_parts ) ) {
$srcset = 'srcset="' . esc_attr( implode( ',', $srcset_parts ) ) . '"';
$find[] = $image_html;
$replace[] = str_replace( '<img', '<img ' . $srcset, $image_html );
}
}
}
if ( ! empty( $find ) ) {
$content = str_replace( $find, $replace, $content );
}
}
// Apply non-interactive markup last to clean up interactivity attributes.
$content = self::non_interactive_markup( $attr, $content );
/**
* Filter the output of the Tiled Galleries content.
*
* @module tiled-gallery
*
* @since 6.9.0
*
* @param string $content Tiled Gallery block content.
*/
return apply_filters( 'jetpack_tiled_galleries_block_content', $content );
}
/**
* Removes tabindex and role markup for images that should not be interactive.
*
* @param array $attr Attributes key/value array.
* @param string $content String containing the block content.
*/
private static function non_interactive_markup( $attr, $content ) {
$link_to = $attr['linkTo'] ?? 'none';
$host = new Host();
$is_module_active = $host->is_wpcom_simple()
? get_option( 'carousel_enable_it' )
: Jetpack::is_module_active( 'carousel' );
if ( $link_to === 'none' && ! $is_module_active ) {
// Remove tabIndex and role="button" by replacing the content
$content = preg_replace(
'/\s*(role="button"|tabindex="0")/',
'',
$content
);
}
return $content;
}
/**
* Determines whether a Tiled Gallery block uses square or circle images (1:1 ratio)
*
* Layouts are block styles and will be available as `is-style-[LAYOUT]` in the className
* attribute. The default (rectangular) will be omitted.
*
* @param array $attr Attributes key/value array.
* @return boolean True if layout is squareish, otherwise false.
*/
private static function is_squareish_layout( $attr ) {
return isset( $attr['className'] )
&& (
'is-style-square' === $attr['className']
|| 'is-style-circle' === $attr['className']
);
}
}
add_action( 'init', array( Tiled_Gallery::class, 'register' ) );
@@ -0,0 +1,65 @@
<?php
/**
* Tock Block.
*
* @since 12.3
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Tock;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
/**
* 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(
__DIR__,
array( 'render_callback' => __NAMESPACE__ . '\render_block' )
);
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* Render the widget and associated JS
*
* @param array $attr The block attributes.
*/
function render_block( $attr ) {
$content = '<div id="Tock_widget_container" data-tock-display-mode="Button" data-tock-color-mode="Blue" data-tock-locale="en-us" data-tock-timezone="America/New_York"></div>';
if ( empty( $attr['url'] ) ) {
if ( ! current_user_can( 'edit_posts' ) ) {
return;
}
return Jetpack_Gutenberg::notice(
__( 'The block will not be shown to your site visitors until a Tock business name is set.', 'jetpack' ),
'warning',
Blocks::classes( Blocks::get_block_feature( __DIR__ ), $attr )
);
}
wp_enqueue_script( 'tock-widget', 'https://www.exploretock.com/tock.js', array(), JETPACK__VERSION, true );
// Add CSS to hide direct link.
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
wp_add_inline_script(
'tock-widget',
"!function(t,o){if(!t.tock){var e=t.tock=function(){e.callMethod?
e.callMethod.apply(e,arguments):e.queue.push(arguments)};t._tock||(t._tock=e),
e.push=e,e.loaded=!0,e.version='1.0',e.queue=[];}}(window,document);
tock('init', '" . esc_js( $attr['url'] ) . "');",
'before'
);
return sprintf(
'<div class="%1$s">%2$s</div>',
esc_attr( Blocks::classes( Blocks::get_block_feature( __DIR__ ), $attr ) ),
$content
);
}
@@ -0,0 +1,114 @@
<?php
/**
* Top Posts Block.
*
* @since 13.0
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Top_Posts;
use Automattic\Jetpack\Blocks;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Status;
use Jetpack_Gutenberg;
use Jetpack_Top_Posts_Helper;
if ( ! class_exists( 'Jetpack_Top_Posts_Helper' ) ) {
require_once JETPACK__PLUGIN_DIR . '/_inc/lib/class-jetpack-top-posts-helper.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() {
if ( ( new Connection_Manager( 'jetpack' ) )->has_connected_owner() && ! ( new Status() )->is_offline_mode() ) {
Blocks::jetpack_register_block(
__DIR__,
array( 'render_callback' => __NAMESPACE__ . '\load_assets' )
);
}
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* Top Posts block registration/dependency declaration.
*
* @param array $attributes Array containing the Top Posts block attributes.
*
* @return string
*/
function load_assets( $attributes ) {
// Do not render in contexts outside the front-end (eg. emails, API).
if ( ! jetpack_is_frontend() ) {
return;
}
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
/*
* We cannot rely on obtaining posts from the block because
* top posts might have changed since then. As such, we must
* check for updated stats.
*/
$period = $attributes['period'];
$number = $attributes['postsToShow'];
$types = implode( ',', array_keys( array_filter( $attributes['postTypes'] ) ) );
$data = Jetpack_Top_Posts_Helper::get_top_posts( $period, $number, $types );
if ( ! is_array( $data ) ) {
return;
}
$wrapper_attributes = \WP_Block_Supports::get_instance()->apply_block_supports();
$output = sprintf(
'<div class="jetpack-top-posts%s%s%s"%sdata-item-count="%s"><div class="jetpack-top-posts-wrapper">',
! empty( $attributes['className'] ) ? ' ' . esc_attr( $attributes['className'] ) : '',
! empty( $wrapper_attributes['class'] ) ? ' ' . esc_attr( $wrapper_attributes['class'] ) : '',
' is-' . esc_attr( $attributes['layout'] ) . '-layout',
! empty( $wrapper_attributes['style'] ) ? ' style="' . esc_attr( $wrapper_attributes['style'] ) . '"' : '',
count( $data )
);
foreach ( $data as $item ) {
$output .= '<div class="jetpack-top-posts-item">';
if ( $attributes['displayThumbnail'] ) {
$output .= '<a class="jetpack-top-posts-thumbnail-link" href="' . esc_url( $item['href'] ) . '">';
if ( ! empty( $item['thumbnail'] ) ) {
$output .= '<img class="jetpack-top-posts-thumbnail" src="' . esc_url( $item['thumbnail'] ) . '" alt="' . esc_attr( $item['title'] ) . '">';
} else {
$output .= '<div class="jetpack-top-posts-mock-thumbnail"></div>';
}
$output .= '</a>';
}
$output .= '<span class="jetpack-top-posts-title"><a href="' . esc_url( $item['href'] ) . '">' . esc_html( $item['title'] ) . '</a></span>';
if ( $attributes['displayDate'] ) {
$output .= '<span class="jetpack-top-posts-date has-small-font-size">' . esc_html( $item['date'] ) . '</span>';
}
if ( $attributes['displayAuthor'] ) {
$output .= '<span class="jetpack-top-posts-author has-small-font-size">' . esc_html( $item['author'] ) . '</span>';
}
if ( $attributes['displayContext'] && ! empty( $item['context'] ) && is_array( $item['context'] ) ) {
$context = reset( $item['context'] );
$output .= '<span class="jetpack-top-posts-context has-small-font-size"><a href="' . esc_url( get_category_link( $context->term_id ) ) . '">' . esc_html( $context->name ) . '</a></span>';
}
$output .= '</div>';
}
$output .= '</div></div>';
return $output;
}
@@ -0,0 +1,43 @@
<?php
/**
* VideoPress Block.
*
* @since 11.1.0
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\VideoPress;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
const FEATURE_NAME = 'videopress-block';
const FEATURE_FOLDER = 'videopress';
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' );
/**
* VideoPress block registration/dependency declaration.
*
* @param array $attrs Array containing the VideoPress block attributes.
* @param string $content String containing the VideoPress block content.
*
* @return string
*/
function load_assets( $attrs, $content ) {
Jetpack_Gutenberg::load_assets_as_required( FEATURE_FOLDER );
return $content;
}
@@ -0,0 +1,59 @@
<?php
/**
* "Voice to content" Block.
*
* @since 12.5
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions\Voice_To_Content;
use Automattic\Jetpack\Blocks;
use Jetpack_Gutenberg;
/**
* Registers our block for use in Gutenberg
* This is done via an action so that we can disable
* registration if we need to.
*/
function register_block() {
/**
* Register the block only if we are on an A8C P2 site.
* TODO: when opening it to Jetpack sites, do the same checks
* we do on the AI Assistant block: the jetpack_ai_enabled filter
* and the Jetpack connection:
* - apply_filters( 'jetpack_ai_enabled', true )
* - ( new Host() )->is_wpcom_simple() || ! ( new Status() )->is_offline_mode()
*/
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
if ( function_exists( 'wpcom_is_automattic_p2_site' ) && wpcom_is_automattic_p2_site() ) {
Blocks::jetpack_register_block(
__DIR__,
array( 'render_callback' => __NAMESPACE__ . '\load_assets' )
);
}
}
}
add_action( 'init', __NAMESPACE__ . '\register_block' );
/**
* "Voice to content" block registration/dependency declaration.
*
* @param array $attr Array containing the "Voice to content" block attributes.
* @param string $content String containing the "Voice to content" block content.
*
* @return string
*/
function load_assets( $attr, $content ) {
/*
* Enqueue necessary scripts and styles.
*/
Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
return sprintf(
'<div class="%1$s">%2$s</div>',
esc_attr( Blocks::classes( Blocks::get_block_feature( __DIR__ ), $attr ) ),
$content
);
}
@@ -0,0 +1,175 @@
<?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* Ads Block.
*
* @since 7.1.0
*
* @package automattic/jetpack
*/
namespace Automattic\Jetpack\Extensions;
use Automattic\Jetpack\Blocks;
use Automattic\Jetpack\Current_Plan as Jetpack_Plan;
use Jetpack;
use Jetpack_Gutenberg;
/**
* Jetpack's Ads Block class.
*
* @since 7.1.0
*/
class WordAds {
/**
* Mapping array of gutenberg ad snippet with the WordAds_Smart formats.
*
* @var array
*/
private static $gutenberg_ad_snippet_x_smart_format = array(
'gutenberg_300x250' => 'gutenberg_rectangle',
'gutenberg_728x90' => 'gutenberg_leaderboard',
'gutenberg_320x50' => 'gutenberg_mobile_leaderboard',
'gutenberg_160x600' => 'gutenberg_skyscraper',
);
/**
* Check if site is on WP.com Simple.
*
* @return bool
*/
private static function is_wpcom() {
return defined( 'IS_WPCOM' ) && IS_WPCOM;
}
/**
* Check if the WordAds module is active.
*
* @return bool
*/
private static function is_jetpack_module_active() {
return method_exists( 'Jetpack', 'is_module_active' ) && Jetpack::is_module_active( 'wordads' );
}
/**
* Check if the site is approved for ads for WP.com Simple sites.
*
* @return bool
*/
private static function is_available() {
if ( self::is_wpcom() ) {
return has_any_blog_stickers( array( 'wordads', 'wordads-approved', 'wordads-approved-misfits' ), get_current_blog_id() );
}
return Jetpack_Plan::supports( 'wordads' );
}
/**
* Register the WordAds block.
*/
public static function register() {
if ( self::is_available() ) {
Blocks::jetpack_register_block(
__DIR__,
array(
'render_callback' => array( __CLASS__, 'gutenblock_render' ),
)
);
}
}
/**
* Set if the WordAds block is available.
*/
public static function set_availability() {
$block_name = 'wordads';
if ( ! self::is_available() ) {
Jetpack_Gutenberg::set_extension_unavailable( $block_name, 'WordAds unavailable' );
return;
}
// Make the block available. Just in case it wasn't registered before.
Jetpack_Gutenberg::set_extension_available( $block_name );
}
/**
* Renders the WordAds block.
*
* @param array $attr Block attributes.
*
* @return string Block HTML.
*/
public static function gutenblock_render( $attr ) {
global $wordads;
/** If the WordAds module is not active, don't render the block. */
if ( ! self::is_jetpack_module_active() ) {
return '';
}
/** This filter is already documented in modules/wordads/class-wordads.php `insert_ad()` */
if (
empty( $wordads )
|| empty( $wordads->params )
|| is_feed()
|| apply_filters( 'wordads_inpost_disable', false )
) {
return '';
}
if ( ! empty( $attr['hideMobile'] ) && $wordads->params->is_mobile() ) {
return '';
}
if ( ! self::is_wpcom() && $wordads->option( 'wordads_house' ) ) {
return $wordads->get_ad( 'inline', 'house' );
}
// 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';
if ( isset( $attr['align'] ) && in_array( $attr['align'], array( 'left', 'center', 'right' ), true ) ) {
$align = $attr['align'];
}
$align = 'align' . $align;
$ad_tag_ids = $wordads->get_ad_tags();
$format = 'mrec';
if ( isset( $attr['format'] ) && isset( $ad_tag_ids[ $attr['format'] ] ) ) {
$format = $attr['format'];
}
$height = $ad_tag_ids[ $format ]['height'];
$width = $ad_tag_ids[ $format ]['width'];
$location = 'gutenberg';
$snippet = $wordads->get_ad_snippet( $section_id, $height, $width, $location, $wordads->get_solo_unit_css() );
$key = "{$location}_{$width}x{$height}";
$smart_format = self::$gutenberg_ad_snippet_x_smart_format[ $key ] ?? null;
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$is_watl_enabled = $smart_format && ( isset( $_GET[ $smart_format ] ) && 'true' === $_GET[ $smart_format ] );
$ad_div = $wordads->get_ad_div( 'inline', $snippet, array( $align ) );
// Render IPW div if WATL is not enabled.
if ( ! $is_watl_enabled ) {
return $ad_div;
}
// Remove linebreaks and sanitize.
$snippet = esc_js( str_replace( array( "\n", "\t", "\r" ), '', $ad_div ) );
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
$fallback_snippet = <<<HTML
<script>
var sas_fallback = sas_fallback || [];
sas_fallback.push(
{ tag: "$snippet", type: '$smart_format' }
);
</script>
HTML;
return $fallback_snippet . $wordads->get_watl_ad_html_tag( $smart_format );
}
}
add_action( 'init', array( 'Automattic\\Jetpack\\Extensions\\WordAds', 'register' ) );
add_action( 'jetpack_register_gutenberg_extensions', array( 'Automattic\\Jetpack\\Extensions\\WordAds', 'set_availability' ) );