* @package automattic/jetpack
namespace Automattic\Jetpack\Extensions\Subscriptions;
use Automattic\Jetpack\Blocks;
use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Abstract_Token_Subscription_Service;
use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Jetpack_Token_Subscription_Service;
use Automattic\Jetpack\Modules;
use Automattic\Jetpack\Status\Host;
use Automattic\Jetpack\Status\Request;
use Jetpack_Subscriptions_Widget;
if ( ! defined( 'ABSPATH' ) ) {
require_once __DIR__ . '/class-jetpack-subscription-site.php';
require_once __DIR__ . '/constants.php';
require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
* These block defaults should match ./constants.js
const DEFAULT_BORDER_RADIUS_VALUE = 0;
const DEFAULT_BORDER_WEIGHT_VALUE = 1;
const DEFAULT_FONTSIZE_VALUE = '16px';
const DEFAULT_PADDING_VALUE = 15;
const DEFAULT_SPACING_VALUE = 10;
const DEFAULT_BUTTON_WIDTH = 'auto';
* 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() {
* Disable the feature on P2 blogs
if ( function_exists( '\WPForTeams\is_wpforteams_site' ) &&
\WPForTeams\is_wpforteams_site( get_current_blog_id() ) ) {
* Do not proceed if the newsletter feature (Subscriptions module) is not enabled
if ( ! ( new Modules() )->is_active( 'subscriptions' ) ) {
require_once JETPACK__PLUGIN_DIR . '/modules/memberships/class-jetpack-memberships.php';
if ( \Jetpack_Memberships::should_enable_monetize_blocks_in_editor() ) {
Blocks::jetpack_register_block(
'render_callback' => __NAMESPACE__ . '\render_block',
'render_email_callback' => __NAMESPACE__ . '\render_email',
'align' => array( 'wide', 'full' ),
META_NAME_FOR_POST_LEVEL_ACCESS_SETTINGS,
'auth_callback' => function () {
return wp_get_current_user()->has_cap( 'edit_posts' );
META_NAME_FOR_POST_DONT_EMAIL_TO_SUBS,
'auth_callback' => function () {
return wp_get_current_user()->has_cap( 'edit_posts' );
META_NAME_FOR_POST_TIER_ID_SETTINGS,
'auth_callback' => function () {
return wp_get_current_user()->has_cap( 'edit_posts' );
META_NAME_CONTAINS_PAYWALLED_CONTENT,
'auth_callback' => function () {
return wp_get_current_user()->has_cap( 'edit_posts' );
// This ensures Jetpack will sync this post meta to WPCOM.
'jetpack_sync_post_meta_whitelist',
function ( $allowed_meta ) {
META_NAME_FOR_POST_LEVEL_ACCESS_SETTINGS,
META_NAME_FOR_POST_DONT_EMAIL_TO_SUBS,
META_NAME_CONTAINS_PAYWALLED_CONTENT,
// Hide the content – Priority 8 makes it run before do_blocks gets called for the content
add_filter( 'the_content', __NAMESPACE__ . '\add_paywall', 8 );
// Close comments on the front-end
add_filter( 'comments_open', __NAMESPACE__ . '\maybe_close_comments', 10, 2 );
add_filter( 'pings_open', __NAMESPACE__ . '\maybe_close_comments', 10, 2 );
// Hide existing comments
add_filter( 'get_comment', __NAMESPACE__ . '\maybe_gate_existing_comments' );
// Add a 'Newsletter' column to the Edit posts page
// We only display the "Newsletter" column if we have configured the paid newsletter plan
if ( defined( 'WP_ADMIN' ) && WP_ADMIN && Jetpack_Memberships::has_configured_plans_jetpack_recurring_payments( 'newsletter' ) ) {
add_action( 'manage_post_posts_columns', __NAMESPACE__ . '\register_newsletter_access_column' );
add_action( 'manage_post_posts_custom_column', __NAMESPACE__ . '\render_newsletter_access_rows', 10, 2 );
add_action( 'admin_head', __NAMESPACE__ . '\newsletter_access_column_styles' );
add_action( 'init', __NAMESPACE__ . '\maybe_prevent_super_cache_caching' );
add_action( 'wp_after_insert_post', __NAMESPACE__ . '\add_paywalled_content_post_meta', 99, 2 );
'jetpack_options_whitelist',
$options[] = 'jetpack_subscriptions_subscribe_post_end_enabled';
$options[] = 'jetpack_subscriptions_subscribe_navigation_enabled';
// If called via REST API, we need to register later in the lifecycle
if ( ( new Host() )->is_wpcom_platform() && ! Request::is_frontend() ) {
Jetpack_Subscription_Site::init()->handle_subscribe_block_placements();
Jetpack_Subscription_Site::init()->handle_subscribe_block_placements();
add_action( 'init', __NAMESPACE__ . '\register_block', 9 );
* Returns true when in a WP.com environment.
return defined( 'IS_WPCOM' ) && IS_WPCOM;
* Adds a 'Newsletter' column after the 'Title' column in the post list
* @param array $columns An array of column names.
* @return array An array of column names.
function register_newsletter_access_column( $columns ) {
$position = array_search( 'title', array_keys( $columns ), true );
$new_column = array( NEWSLETTER_COLUMN_ID => __( 'Newsletter', 'jetpack' ) );
array_slice( $columns, 0, $position + 1, true ),
array_slice( $columns, $position, null, true )
* Add a meta to prevent publication on firehose, ES AI or Reader
* @param int $post_id Post id being saved.
* @param \WP_Post $post Post being saved.
function add_paywalled_content_post_meta( int $post_id, \WP_Post $post ) {
if ( $post->post_type !== 'post' ) {
$access_level = get_post_meta( $post_id, META_NAME_FOR_POST_LEVEL_ACCESS_SETTINGS, true );
switch ( $access_level ) {
case Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS_ALL_TIERS:
case Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS:
case Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS:
update_post_meta( $post_id, META_NAME_CONTAINS_PAYWALLED_CONTENT, $is_paywalled );
delete_post_meta( $post_id, META_NAME_CONTAINS_PAYWALLED_CONTENT );
* Displays the newsletter access level.
* @param string $column_id The ID of the column to display.
* @param int $post_id The current post ID.
function render_newsletter_access_rows( $column_id, $post_id ) {
if ( NEWSLETTER_COLUMN_ID !== $column_id ) {
$access_level = get_post_meta( $post_id, META_NAME_FOR_POST_LEVEL_ACCESS_SETTINGS, true );
switch ( $access_level ) {
case Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS_ALL_TIERS:
echo esc_html__( 'Paid Subscribers (all plans)', 'jetpack' );
case Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS:
echo esc_html__( 'Paid Subscribers', 'jetpack' );
case Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS:
echo esc_html__( 'Subscribers', 'jetpack' );
case Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY:
echo esc_html__( 'Everybody', 'jetpack' );
* Adds the Newsletter column styles
function newsletter_access_column_styles() {
echo '<style id="jetpack-newsletter-newsletter-access-column"> table.fixed .column-newsletter_access { width: 10%; } </style>';
* Determine the amount of folks currently subscribed to the blog, splitted out in total_subscribers, email_subscribers, social_followers & paid_subscribers.
* @return array containing ['value' => ['total_subscribers' => 0, 'email_subscribers' => 0, 'paid_subscribers' => 0, 'social_followers' => 0]]
function fetch_subscriber_counts() {
'value' => \wpcom_fetch_subs_counts( true ),
$cache_key = 'wpcom_subscribers_totals';
$subs_count = get_transient( $cache_key );
if ( false === $subs_count || 'failed' === $subs_count['status'] ) {
$xml = new \Jetpack_IXR_Client();
$xml->query( 'jetpack.fetchSubscriberCounts' );
if ( $xml->isError() ) { // If we get an error from .com, set the status to failed so that we will try again next time the data is requested.
'code' => $xml->getErrorCode(),
'message' => $xml->getErrorMessage(),
'value' => ( isset( $subs_count['value'] ) ) ? $subs_count['value'] : array(
'total_subscribers' => 0,
'email_subscribers' => 0,
'value' => $xml->getResponse(),
set_transient( $cache_key, $subs_count, 3600 ); // Try to cache the result for at least 1 hour.
* Returns subscriber count based on include_social_followers attribute
* @param bool $include_social_followers Whether to include social followers in the count.
function get_subscriber_count( $include_social_followers ) {
$counts = fetch_subscriber_counts();
if ( $include_social_followers ) {
$subscriber_count = $counts['value']['total_subscribers'] + $counts['value']['social_followers'];
$subscriber_count = $counts['value']['total_subscribers'];
return $subscriber_count;
* Returns true if the block attributes contain a value for the given key.
* @param array $attributes Array containing the block attributes.
* @param string $key Block attribute key.
function has_attribute( $attributes, $key ) {
return isset( $attributes[ $key ] ) && $attributes[ $key ] !== 'undefined';
* Returns the value for the given attribute key, with the option of providing a default fallback value.
* @param array $attributes Array containing the block attributes.
* @param string $key Block attribute key.
* @param mixed $default Optional fallback value in case the key doesn't exist.
function get_attribute( $attributes, $key, $default = null ) {
return has_attribute( $attributes, $key ) ? $attributes[ $key ] : $default;
* Mimics getColorClassName, getFontSizeClass and getGradientClass from @wordpress/block-editor js package.
* @param string $setting Setting name.
* @param string $value Setting value.
function get_setting_class_name( $setting, $value ) {
if ( ! $setting || ! $value ) {
return sprintf( 'has-%s-%s', $value, $setting );
* Uses block attributes to generate an array containing the classes for various block elements.
* Based on Jetpack_Subscriptions_Widget::do_subscription_form() which the block was originally using.
* @param array $attributes Array containing the block attributes.
function get_element_class_names_from_attributes( $attributes ) {
$text_color_class = get_setting_class_name( 'color', get_attribute( $attributes, 'textColor' ) );
$font_size_class = get_setting_class_name( 'font-size', get_attribute( $attributes, 'fontSize' ) );
$border_class = get_setting_class_name( 'border-color', get_attribute( $attributes, 'borderColor' ) );
$button_background_class = get_setting_class_name( 'background-color', get_attribute( $attributes, 'buttonBackgroundColor' ) );
$button_gradient_class = get_setting_class_name( 'gradient-background', get_attribute( $attributes, 'buttonGradient' ) );
$email_field_background_class = get_setting_class_name( 'background-color', get_attribute( $attributes, 'emailFieldBackgroundColor' ) );
$email_field_gradient_class = get_setting_class_name( 'gradient-background', get_attribute( $attributes, 'emailFieldGradient' ) );
$submit_button_classes = array_filter(
'wp-block-button__link' => true,
'no-border-radius' => 0 === get_attribute( $attributes, 'borderRadius', 0 ),
$font_size_class => true,
'has-text-color' => ! empty( $text_color_class ),
$text_color_class => true,
'has-background' => ! empty( $button_background_class ) || ! empty( $button_gradient_class ),
$button_background_class => ! empty( $button_background_class ),
$button_gradient_class => ! empty( $button_gradient_class ),
$email_field_classes = array_filter(
'no-border-radius' => 0 === get_attribute( $attributes, 'borderRadius', 0 ),
$font_size_class => true,
$email_field_background_class => true,
$email_field_gradient_class => true,
$block_wrapper_classes = array_filter(
'wp-block-jetpack-subscriptions__supports-newline' => true,
'wp-block-jetpack-subscriptions__use-newline' => (bool) get_attribute( $attributes, 'buttonOnNewLine' ),
'wp-block-jetpack-subscriptions__show-subs' => (bool) get_attribute( $attributes, 'showSubscribersTotal' ),
'block_wrapper' => implode( ' ', array_keys( $block_wrapper_classes ) ),
'email_field' => implode( ' ', array_keys( $email_field_classes ) ),
'submit_button' => implode( ' ', array_keys( $submit_button_classes ) ),
* Checks if block style is "button only"
* @param string $class_name Block attribute className; multiple names are spearated by space.
function is_button_only_style( $class_name ) {
if ( empty( $class_name ) ) {
$class_names = explode( ' ', $class_name );
return in_array( 'is-style-button', $class_names, true );
* Uses block attributes to generate an array containing the styles for various block elements.
* Based on Jetpack_Subscriptions_Widget::do_subscription_form() which the block was originally using.
* @param array $attributes Array containing the block attributes.
function get_element_styles_from_attributes( $attributes ) {
$is_button_only_style = is_button_only_style( get_attribute( $attributes, 'className', '' ) );
$button_background_style = ! has_attribute( $attributes, 'buttonBackgroundColor' ) && has_attribute( $attributes, 'customButtonGradient' )
? get_attribute( $attributes, 'customButtonGradient' )
: get_attribute( $attributes, 'customButtonBackgroundColor' );
$email_field_styles = '';
$submit_button_wrapper_styles = '';
$submit_button_styles = '';
if ( ! empty( $button_background_style ) ) {
$submit_button_styles .= sprintf( 'background: %s;', $button_background_style );
if ( has_attribute( $attributes, 'customTextColor' ) ) {
$submit_button_styles .= sprintf( 'color: %s;', get_attribute( $attributes, 'customTextColor' ) );
if ( has_attribute( $attributes, 'buttonWidth' ) ) {
$submit_button_wrapper_styles .= sprintf( 'width: %s;', get_attribute( $attributes, 'buttonWidth', DEFAULT_BUTTON_WIDTH ) );
$submit_button_wrapper_styles .= 'max-width: 100%;';
// Account for custom margins on inline forms.
$submit_button_styles .= true === get_attribute( $attributes, 'buttonOnNewLine' )
: sprintf( 'width: calc(100%% - %dpx);', get_attribute( $attributes, 'spacing', DEFAULT_SPACING_VALUE ) );
$font_size = get_attribute( $attributes, 'customFontSize', DEFAULT_FONTSIZE_VALUE );
$style = sprintf( 'font-size: %s%s;', $font_size, is_numeric( $font_size ) ? 'px' : '' );
$submit_button_styles .= $style;
$email_field_styles .= $style;