<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName)
* Module Name: Newsletter
* Module Description: Grow your subscriber list and deliver your content directly to their email inbox.
* Recommendation Order: 8
* Requires Connection: Yes
* Requires User Connection: Yes
* Additional Search Queries: subscriptions, subscription, email, follow, followers, subscribers, signup, newsletter, creator
// phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move classes to appropriately-named class files.
use Automattic\Jetpack\Admin_UI\Admin_Menu;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Connection\XMLRPC_Async_Call;
use Automattic\Jetpack\Redirect;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Status\Host;
use Automattic\Jetpack\Subscribers_Dashboard\Dashboard as Subscribers_Dashboard;
if ( ! defined( 'ABSPATH' ) ) {
add_action( 'jetpack_modules_loaded', 'jetpack_subscriptions_load' );
// Loads the User Content Link Redirection feature.
require_once __DIR__ . '/subscriptions/jetpack-user-content-link-redirection.php';
* Loads the Subscriptions module.
function jetpack_subscriptions_load() {
Jetpack::enable_module_configurable( __FILE__ );
* Cherry picks keys from `$_SERVER` array.
* @return array An array of server data.
function jetpack_subscriptions_cherry_pick_server_data() {
foreach ( $_SERVER as $key => $value ) {
if ( ! is_string( $value ) || str_starts_with( $key, 'HTTP_COOKIE' ) ) {
if ( str_starts_with( $key, 'HTTP_' ) || in_array( $key, array( 'REMOTE_ADDR', 'REQUEST_URI', 'DOCUMENT_URI' ), true ) ) {
* Main class file for the Subscriptions module.
* @phan-constructor-used-for-side-effects
class Jetpack_Subscriptions {
* Whether Jetpack has been instantiated or not.
* Hash of the siteurl option.
public static function init() {
static $instance = false;
$instance = new Jetpack_Subscriptions();
* Jetpack_Subscriptions constructor.
public function __construct() {
$this->jetpack = Jetpack::init();
// Don't use COOKIEHASH as it could be shared across installs && is non-unique in multisite.
// @see: https://twitter.com/nacin/status/378246957451333632 .
self::$hash = md5( get_option( 'siteurl' ) );
add_filter( 'jetpack_xmlrpc_methods', array( $this, 'xmlrpc_methods' ) );
// @todo remove sync from subscriptions and move elsewhere...
// Add Configuration Page.
add_action( 'admin_init', array( $this, 'configure' ) );
// Catch subscription widget submits.
if ( isset( $_REQUEST['jetpack_subscriptions_widget'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce checked in widget_submit() for logged in users.
add_action( 'template_redirect', array( $this, 'widget_submit' ) );
// Set up the comment subscription checkboxes.
add_filter( 'comment_form_submit_field', array( $this, 'comment_subscribe_init' ), 10, 2 );
// Catch comment posts and check for subscriptions.
add_action( 'comment_post', array( $this, 'comment_subscribe_submit' ), 50, 2 );
// Adds post meta checkbox in the post submit metabox.
add_action( 'post_submitbox_misc_actions', array( $this, 'subscription_post_page_metabox' ) );
add_action( 'transition_post_status', array( $this, 'maybe_send_subscription_email' ), 10, 3 );
add_filter( 'jetpack_published_post_flags', array( $this, 'set_post_flags' ), 10, 2 );
add_filter( 'post_updated_messages', array( $this, 'update_published_message' ), 18, 1 );
// Set "social_notifications_subscribe" option during the first-time activation.
add_action( 'jetpack_activate_module_subscriptions', array( $this, 'set_social_notifications_subscribe' ) );
add_action( 'jetpack_activate_module_subscriptions', array( $this, 'set_featured_image_in_email_default' ) );
// Hide subscription messaging in Publish panel for posts that were published in the past
add_action( 'init', array( $this, 'register_post_meta' ), 20 );
add_action( 'transition_post_status', array( $this, 'maybe_set_first_published_status' ), 10, 3 );
// Add Subscribers menu to Jetpack navigation.
add_action( 'jetpack_admin_menu', array( $this, 'add_subscribers_menu' ) );
// Customize the configuration URL to lead to the Subscriptions settings.
'jetpack_module_configuration_url_subscriptions',
return Jetpack::admin_url( array( 'page' => 'jetpack#/newsletter' ) );
// Track categories created through the category editor page
add_action( 'wp_ajax_add-tag', array( $this, 'track_newsletter_category_creation' ), 1 );
$subscribers_dashboard = new Subscribers_Dashboard();
$subscribers_dashboard::init();
* Jetpack_Subscriptions::xmlrpc_methods()
* Register subscriptions methods with the Jetpack XML-RPC server.
* @param array $methods Methods being registered.
public function xmlrpc_methods( $methods ) {
'jetpack.subscriptions.subscribe' => array( $this, 'subscribe' ),
* Disable Subscribe on Single Post
public function subscription_post_page_metabox() {
* Filter whether or not to show the per-post subscription option.
* @param bool true = show checkbox option on all new posts | false = hide the option.
! apply_filters( 'jetpack_allow_per_post_subscriptions', false ) ) {
if ( has_filter( 'jetpack_subscriptions_exclude_these_categories' ) || has_filter( 'jetpack_subscriptions_include_only_these_categories' ) ) {
$disable_subscribe_value = get_post_meta( $post->ID, '_jetpack_dont_email_post_to_subs', true );
// only show checkbox if post hasn't been published and is a 'post' post type.
if ( get_post_status( $post->ID ) !== 'publish' && get_post_type( $post->ID ) === 'post' ) :
wp_nonce_field( 'disable_subscribe', 'disable_subscribe_nonce' );
<div class="misc-pub-section">
<label for="_jetpack_dont_email_post_to_subs"><?php esc_html_e( 'Jetpack Subscriptions:', 'jetpack' ); ?></label><br>
<input type="checkbox" name="_jetpack_dont_email_post_to_subs" id="jetpack-per-post-subscribe" value="1" <?php checked( $disable_subscribe_value, 1, true ); ?> />
<?php esc_html_e( 'Don’t send this to subscribers', 'jetpack' ); ?>
* Checks whether or not the post should be emailed to subscribers
* It checks for the following things in order:
* - Usage of filter jetpack_subscriptions_exclude_these_categories
* - Usage of filter jetpack_subscriptions_include_only_these_categories
* - Existence of the per-post checkbox option
* Only one of these can be used at any given time.
* @param string $new_status Tthe "new" post status of the transition when saved.
* @param string $old_status The "old" post status of the transition when saved.
* @param object $post obj The post object.
public function maybe_send_subscription_email( $new_status, $old_status, $post ) {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
// Make sure that the checkbox is preseved.
if ( ! empty( $_POST['disable_subscribe_nonce'] ) && wp_verify_nonce( $_POST['disable_subscribe_nonce'], 'disable_subscribe' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- WP Core doesn't unslash or sanitize nonces either.
$set_checkbox = isset( $_POST['_jetpack_dont_email_post_to_subs'] ) ? 1 : 0;
update_post_meta( $post->ID, '_jetpack_dont_email_post_to_subs', $set_checkbox );
* Message used when publishing a post.
* @param array $messages Message array for a post.
public function update_published_message( $messages ) {
if ( ! $this->should_email_post_to_subscribers( $post ) ) {
$view_post_link_html = sprintf(
' <a href="%1$s">%2$s</a>',
esc_url( get_permalink( $post ) ),
__( 'View post', 'jetpack' )
$messages['post'][6] = sprintf(
/* translators: Message shown after a post is published */
esc_html__( 'Post published and sending emails to subscribers.', 'jetpack' )
) . $view_post_link_html;
* Determine if a post should notifiy subscribers via email.
* @param object $post The post.
public function should_email_post_to_subscribers( $post ) {
if ( get_post_meta( $post->ID, '_jetpack_dont_email_post_to_subs', true ) ) {
// Only posts are currently supported.
if ( 'post' !== $post->post_type ) {
// Private posts are not sent to subscribers.
if ( 'private' === $post->post_status ) {
* Array of categories that will never trigger subscription emails.
* Will not send subscription emails from any post from within these categories.
* @param array $args Array of category slugs or ID's.
$excluded_categories = apply_filters( 'jetpack_subscriptions_exclude_these_categories', array() );
// Never email posts from these categories.
if ( ! empty( $excluded_categories ) && in_category( $excluded_categories, $post->ID ) ) {
* ONLY send subscription emails for these categories
* Will ONLY send subscription emails to these categories.
* @param array $args Array of category slugs or ID's.
$only_these_categories = apply_filters( 'jetpack_subscriptions_exclude_all_categories_except', array() );
// Only emails posts from these categories.
if ( ! empty( $only_these_categories ) && ! in_category( $only_these_categories, $post->ID ) ) {
* Retrieve which flags should be added to a particular post.
* @param array $flags Flags to be added.
* @param object $post A post object.
public function set_post_flags( $flags, $post ) {
$flags['send_subscription'] = $this->should_email_post_to_subscribers( $post );
* Jetpack_Subscriptions::configure()
* Jetpack Subscriptions configuration screen.
public function configure() {
__( 'Jetpack Subscriptions Settings', 'jetpack' ),
array( $this, 'subscriptions_settings_section' ),
/** Subscribe to Posts */
'jetpack_subscriptions_post_subscribe',
__( 'Follow Blog', 'jetpack' ),
array( $this, 'subscription_post_subscribe_setting' ),
/** Subscribe to Comments */
'jetpack_subscriptions_comment_subscribe',
__( 'Follow Comments', 'jetpack' ),
array( $this, 'subscription_comment_subscribe_setting' ),
/** Email me whenever: Someone subscribes to my blog */
__( 'Someone subscribes to my blog', 'jetpack' ),
array( $this, 'social_notifications_subscribe_section' ),
'jetpack_subscriptions_social_notifications_subscribe',
__( 'Email me whenever', 'jetpack' ),
array( $this, 'social_notifications_subscribe_field' ),
'social_notifications_subscribe',
array( $this, 'social_notifications_subscribe_validate' )
* Discussions setting section blurb.
public function subscriptions_settings_section() {
<p id="jetpack-subscriptions-settings"><?php esc_html_e( 'Change whether your visitors can subscribe to your posts or comments or both.', 'jetpack' ); ?></p>
* Post Subscriptions Toggle.
public function subscription_post_subscribe_setting() {
$stb_enabled = get_option( 'stb_enabled', 1 );
<input type="checkbox" name="stb_enabled" id="jetpack-post-subscribe" value="1" <?php checked( $stb_enabled, 1 ); ?> />
"Show a <em>'follow blog'</em> option in the comment form",
* Comments Subscriptions Toggle.
public function subscription_comment_subscribe_setting() {
$stc_enabled = get_option( 'stc_enabled', 1 );
<input type="checkbox" name="stc_enabled" id="jetpack-comment-subscribe" value="1" <?php checked( $stc_enabled, 1 ); ?> />
"Show a <em>'follow comments'</em> option in the comment form",
* Someone subscribes to my blog section
public function social_notifications_subscribe_section() {
// Atypical usage here. We emit jquery to move subscribe notification checkbox to be with the rest of the email notification settings.
<script type="text/javascript">
var table = $( '#social_notifications_subscribe' ).parents( 'table:first' ),
header = table.prevAll( 'h2:first' ),
newParent = $( '#moderation_notify' ).parent( 'label' ).parent();
if ( ! table.length || ! header.length || ! newParent.length ) {
newParent.append( '<br/>' ).append( table.end().parent( 'label' ).siblings().andSelf() );
* Someone subscribes to my blog Toggle
public function social_notifications_subscribe_field() {
$checked = (int) ( 'on' === get_option( 'social_notifications_subscribe', 'on' ) );