updated plugin Jetpack Protect version 4.0.0

This commit is contained in:
2025-04-29 21:19:56 +00:00
committed by Gitium
parent eb9181b250
commit ebd40ef928
265 changed files with 11864 additions and 3987 deletions

View File

@ -0,0 +1,112 @@
<?php
/**
* Sets up the Historically Active Modules rest api endpoint and helper functions
*
* @package automattic/my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
use WP_Error;
/**
* Registers REST route for updating historically active modules
* and includes all helper functions for triggering an update elsewhere
*/
class Historically_Active_Modules {
public const UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY = 'update-historically-active-jetpack-modules';
/**
* Register the REST API routes.
*
* @return void
*/
public static function register_rest_endpoints() {
register_rest_route(
'my-jetpack/v1',
'site/update-historically-active-modules',
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::rest_trigger_historically_active_modules_update',
'permission_callback' => __CLASS__ . '::permissions_callback',
)
);
}
/**
* Check user capabilities to access historically active modules.
*
* @access public
* @static
*
* @return true|WP_Error
*/
public static function permissions_callback() {
return current_user_can( 'edit_posts' );
}
/**
* Update historically active Jetpack plugins
* Historically active is defined as the Jetpack plugins that are installed and active with the required connections
* This array will consist of any plugins that were active at one point in time and are still enabled on the site
*
* @return void
*/
public static function update_historically_active_jetpack_modules() {
$historically_active_modules = \Jetpack_Options::get_option( 'historically_active_modules', array() );
$products = Products::get_products();
$product_classes = Products::get_products_classes();
foreach ( $products as $product ) {
$product_slug = $product['slug'];
$status = $product_classes[ $product_slug ]::get_status();
// We want to leave modules in the array if they've been active in the past
// and were not manually disabled by the user.
if ( in_array( $status, Products::$broken_module_statuses, true ) ) {
continue;
}
// If the module is active and not already in the array, add it
if (
in_array( $status, Products::$active_module_statuses, true ) &&
! in_array( $product_slug, $historically_active_modules, true )
) {
$historically_active_modules[] = $product_slug;
}
// If the module has been disabled due to a manual user action,
// or because of a missing plan error, remove it from the array
if ( in_array( $status, Products::$disabled_module_statuses, true ) ) {
$historically_active_modules = array_values( array_diff( $historically_active_modules, array( $product_slug ) ) );
}
}
\Jetpack_Options::update_option( 'historically_active_modules', array_unique( $historically_active_modules ) );
}
/**
* REST API endpoint to trigger an update to the historically active Jetpack modules
*
* @return WP_Error|\WP_REST_Response
*/
public static function rest_trigger_historically_active_modules_update() {
self::update_historically_active_jetpack_modules();
$historically_active_modules = \Jetpack_Options::get_option( 'historically_active_modules', array() );
return rest_ensure_response( $historically_active_modules );
}
/**
* Set transient to queue an update to the historically active Jetpack modules on the next wp-admin load
*
* @param string $plugin The plugin that triggered the update. This will be present if the function was queued by a plugin activation.
*
* @return void
*/
public static function queue_historically_active_jetpack_modules_update( $plugin = null ) {
$plugin_filenames = Products::get_all_plugin_filenames();
if ( ! $plugin || in_array( $plugin, $plugin_filenames, true ) ) {
set_transient( self::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY, true );
}
}
}

View File

@ -21,14 +21,11 @@ use Automattic\Jetpack\JITMS\JITM;
use Automattic\Jetpack\Licensing;
use Automattic\Jetpack\Modules;
use Automattic\Jetpack\Plugins_Installer;
use Automattic\Jetpack\Protect_Status\Status as Protect_Status;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Status\Host as Status_Host;
use Automattic\Jetpack\Sync\Functions as Sync_Functions;
use Automattic\Jetpack\Terms_Of_Service;
use Automattic\Jetpack\Tracking;
use Automattic\Jetpack\VideoPress\Stats as VideoPress_Stats;
use Automattic\Jetpack\Waf\Waf_Runner;
use Jetpack;
use WP_Error;
@ -42,14 +39,14 @@ class Initializer {
*
* @var string
*/
const PACKAGE_VERSION = '4.35.3';
const PACKAGE_VERSION = '5.9.1';
/**
* HTML container ID for the IDC screen on My Jetpack page.
*/
const IDC_CONTAINER_ID = 'my-jetpack-identity-crisis-container';
private const IDC_CONTAINER_ID = 'my-jetpack-identity-crisis-container';
const JETPACK_PLUGIN_SLUGS = array(
public const JETPACK_PLUGIN_SLUGS = array(
'jetpack-backup',
'jetpack-boost',
'zerobscrm',
@ -60,11 +57,7 @@ class Initializer {
'jetpack-search',
);
const MY_JETPACK_SITE_INFO_TRANSIENT_KEY = 'my-jetpack-site-info';
const UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY = 'update-historically-active-jetpack-modules';
const MISSING_CONNECTION_NOTIFICATION_KEY = 'missing-connection';
const VIDEOPRESS_STATS_KEY = 'my-jetpack-videopress-stats';
const VIDEOPRESS_PERIOD_KEY = 'my-jetpack-videopress-period';
private const MY_JETPACK_SITE_INFO_TRANSIENT_KEY = 'my-jetpack-site-info';
/**
* Holds info/data about the site (from the /sites/%d endpoint)
@ -99,16 +92,8 @@ class Initializer {
// Add custom WP REST API endoints.
add_action( 'rest_api_init', array( __CLASS__, 'register_rest_endpoints' ) );
$page_suffix = Admin_Menu::add_menu(
__( 'My Jetpack', 'jetpack-my-jetpack' ),
__( 'My Jetpack', 'jetpack-my-jetpack' ),
'edit_posts',
'my-jetpack',
array( __CLASS__, 'admin_page' ),
-1
);
add_action( 'admin_menu', array( __CLASS__, 'add_my_jetpack_menu_item' ) );
add_action( 'load-' . $page_suffix, array( __CLASS__, 'admin_init' ) );
add_action( 'admin_init', array( __CLASS__, 'setup_historically_active_jetpack_modules_sync' ) );
// This is later than the admin-ui package, which runs on 1000
add_action( 'admin_init', array( __CLASS__, 'maybe_show_red_bubble' ), 1001 );
@ -166,6 +151,23 @@ class Initializer {
);
}
/**
* Add My Jetpack menu item to the admin menu.
*
* @return void
*/
public static function add_my_jetpack_menu_item() {
$page_suffix = Admin_Menu::add_menu(
__( 'My Jetpack', 'jetpack-my-jetpack' ),
__( 'My Jetpack', 'jetpack-my-jetpack' ),
'edit_posts',
'my-jetpack',
array( __CLASS__, 'admin_page' ),
-1
);
add_action( 'load-' . $page_suffix, array( __CLASS__, 'admin_init' ) );
}
/**
* Callback for the load my jetpack page hook.
*
@ -192,12 +194,20 @@ class Initializer {
return $tracking->should_enable_tracking( new Terms_Of_Service(), $status );
}
/**
* Enqueue admin page assets.
*
* @return void
*/
public static function enqueue_scripts() {
/**
* Fires after the My Jetpack page is initialized.
* Allows for enqueuing additional scripts only on the My Jetpack page.
*
* @since 4.35.7
*/
do_action( 'myjetpack_enqueue_scripts' );
Assets::register_script(
'my_jetpack_main_app',
'../build/index.js',
@ -217,12 +227,12 @@ class Initializer {
$previous_score = $speed_score_history->latest( 1 );
}
$latest_score['previousScores'] = $previous_score['scores'] ?? array();
$scan_data = Protect_Status::get_status();
self::update_historically_active_jetpack_modules();
$waf_config = array();
if ( class_exists( 'Automattic\Jetpack\Waf\Waf_Runner' ) ) {
$waf_config = Waf_Runner::get_config();
$sandboxed_domain = '';
$is_dev_version = false;
if ( class_exists( 'Jetpack' ) ) {
$is_dev_version = Jetpack::is_development_version();
$sandboxed_domain = defined( 'JETPACK__SANDBOX_DOMAIN' ) ? JETPACK__SANDBOX_DOMAIN : '';
}
wp_localize_script(
@ -232,9 +242,6 @@ class Initializer {
'products' => array(
'items' => Products::get_products(),
),
'purchases' => array(
'items' => array(),
),
'plugins' => Plugins_Installer::get_plugins(),
'themes' => Sync_Functions::get_themes(),
'myJetpackUrl' => admin_url( 'admin.php?page=my-jetpack' ),
@ -250,40 +257,25 @@ class Initializer {
'adminUrl' => esc_url( admin_url() ),
'IDCContainerID' => static::get_idc_container_id(),
'userIsAdmin' => current_user_can( 'manage_options' ),
'userIsNewToJetpack' => self::is_jetpack_user_new(),
'lifecycleStats' => array(
'jetpackPlugins' => self::get_installed_jetpack_plugins(),
'historicallyActiveModules' => \Jetpack_Options::get_option( 'historically_active_modules', array() ),
'ownedProducts' => Products::get_products_by_ownership( 'owned' ),
'unownedProducts' => Products::get_products_by_ownership( 'unowned' ),
'brokenModules' => self::check_for_broken_modules(),
'brokenModules' => Red_Bubble_Notifications::check_for_broken_modules(),
'isSiteConnected' => $connection->is_connected(),
'isUserConnected' => $connection->is_user_connected(),
'purchases' => self::get_purchases(),
'modules' => self::get_active_modules(),
),
'redBubbleAlerts' => self::get_red_bubble_alerts(),
'recommendedModules' => array(
'modules' => self::get_recommended_modules(),
'dismissed' => \Jetpack_Options::get_option( 'dismissed_recommendations', false ),
'modules' => self::get_recommended_modules(),
'isFirstRun' => \Jetpack_Options::get_option( 'recommendations_first_run', true ),
'dismissed' => \Jetpack_Options::get_option( 'dismissed_recommendations', false ),
),
'isStatsModuleActive' => $modules->is_active( 'stats' ),
'isUserFromKnownHost' => self::is_user_from_known_host(),
'isCommercial' => self::is_commercial_site(),
'canUserViewStats' => current_user_can( 'manage_options' ) || current_user_can( 'view_stats' ),
'sandboxedDomain' => $sandboxed_domain,
'isDevVersion' => $is_dev_version,
'isAtomic' => ( new Status_Host() )->is_woa_site(),
'jetpackManage' => array(
'isEnabled' => Jetpack_Manage::could_use_jp_manage(),
'isAgencyAccount' => Jetpack_Manage::is_agency_account(),
),
'latestBoostSpeedScores' => $latest_score,
'protect' => array(
'scanData' => $scan_data,
'wafConfig' => array_merge(
$waf_config,
array( 'blocked_logins' => (int) get_site_option( 'jetpack_protect_blocked_attempts', 0 ) )
),
),
'videopress' => self::get_videopress_stats(),
)
);
@ -305,86 +297,6 @@ class Initializer {
}
}
/**
* Get stats for VideoPress
*
* @return array|WP_Error
*/
public static function get_videopress_stats() {
$video_count = array_sum( (array) wp_count_attachments( 'video' ) );
if ( ! class_exists( 'Automattic\Jetpack\VideoPress\Stats' ) ) {
return array(
'videoCount' => $video_count,
);
}
$featured_stats = get_transient( self::VIDEOPRESS_STATS_KEY );
if ( $featured_stats ) {
return array(
'featuredStats' => $featured_stats,
'videoCount' => $video_count,
);
}
$stats_period = get_transient( self::VIDEOPRESS_PERIOD_KEY );
$videopress_stats = new VideoPress_Stats();
// If the stats period exists, retrieve that information without checking the view count.
// If it does not, check the view count of monthly stats and determine if we want to show yearly or monthly stats.
if ( $stats_period ) {
if ( $stats_period === 'day' ) {
$featured_stats = $videopress_stats->get_featured_stats( 60, 'day' );
} else {
$featured_stats = $videopress_stats->get_featured_stats( 2, 'year' );
}
} else {
$featured_stats = $videopress_stats->get_featured_stats( 60, 'day' );
if (
! is_wp_error( $featured_stats ) &&
$featured_stats &&
( $featured_stats['data']['views']['current'] < 500 || $featured_stats['data']['views']['previous'] < 500 )
) {
$featured_stats = $videopress_stats->get_featured_stats( 2, 'year' );
}
}
if ( is_wp_error( $featured_stats ) || ! $featured_stats ) {
return array(
'videoCount' => $video_count,
);
}
set_transient( self::VIDEOPRESS_PERIOD_KEY, $featured_stats['period'], WEEK_IN_SECONDS );
set_transient( self::VIDEOPRESS_STATS_KEY, $featured_stats, DAY_IN_SECONDS );
return array(
'featuredStats' => $featured_stats,
'videoCount' => $video_count,
);
}
/**
* Get product slugs of the active purchases
*
* @return array
*/
public static function get_purchases() {
$purchases = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases ) ) {
return array();
}
return array_map(
function ( $purchase ) {
return $purchase->product_slug;
},
(array) $purchases
);
}
/**
* Get installed Jetpack plugins
*
@ -474,16 +386,6 @@ class Initializer {
return true;
}
/**
* Determines whether the user has come from a host we can recognize.
*
* @return string
*/
public static function is_user_from_known_host() {
// Known (external) host is the one that has been determined and is not dotcom.
return ! in_array( ( new Status_Host() )->get_known_host_guess(), array( 'unknown', 'wpcom' ), true );
}
/**
* Build flags for My Jetpack UI
*
@ -516,10 +418,14 @@ class Initializer {
new REST_Products();
new REST_Purchases();
new REST_Zendesk_Chat();
new REST_Product_Data();
new REST_AI();
new REST_Recommendations_Evaluation();
Products::register_product_endpoints();
Historically_Active_Modules::register_rest_endpoints();
Jetpack_Manage::register_rest_endpoints();
Red_Bubble_Notifications::register_rest_endpoints();
register_rest_route(
'my-jetpack/v1',
'site',
@ -578,21 +484,6 @@ class Initializer {
return apply_filters( 'jetpack_my_jetpack_should_initialize', $should );
}
/**
* Set transient to queue an update to the historically active Jetpack modules on the next wp-admin load
*
* @param string $plugin The plugin that triggered the update. This will be present if the function was queued by a plugin activation.
*
* @return void
*/
public static function queue_historically_active_jetpack_modules_update( $plugin = null ) {
$plugin_filenames = Products::get_all_plugin_filenames();
if ( ! $plugin || in_array( $plugin, $plugin_filenames, true ) ) {
set_transient( self::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY, true );
}
}
/**
* Hook into several connection-based actions to update the historically active Jetpack modules
* If the transient that indicates the list needs to be synced, update it and delete the transient
@ -600,9 +491,11 @@ class Initializer {
* @return void
*/
public static function setup_historically_active_jetpack_modules_sync() {
if ( get_transient( self::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY ) && ! wp_doing_ajax() ) {
self::update_historically_active_jetpack_modules();
delete_transient( self::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY );
// yummmm. ham.
$ham = new Historically_Active_Modules();
if ( get_transient( $ham::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY ) && ! wp_doing_ajax() ) {
$ham::update_historically_active_jetpack_modules();
delete_transient( $ham::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY );
}
$actions = array(
@ -612,49 +505,11 @@ class Initializer {
);
foreach ( $actions as $action ) {
add_action( $action, array( __CLASS__, 'queue_historically_active_jetpack_modules_update' ), 5 );
add_action( $action, array( $ham, 'queue_historically_active_jetpack_modules_update' ), 5 );
}
// Modules are often updated async, so we need to update them right away as there will sometimes be no page reload.
add_action( 'jetpack_activate_module', array( __CLASS__, 'update_historically_active_jetpack_modules' ), 5 );
}
/**
* Update historically active Jetpack plugins
* Historically active is defined as the Jetpack plugins that are installed and active with the required connections
* This array will consist of any plugins that were active at one point in time and are still enabled on the site
*
* @return void
*/
public static function update_historically_active_jetpack_modules() {
$historically_active_modules = \Jetpack_Options::get_option( 'historically_active_modules', array() );
$products = Products::get_products();
foreach ( $products as $product ) {
$status = $product['status'];
$product_slug = $product['slug'];
// We want to leave modules in the array if they've been active in the past
// and were not manually disabled by the user.
if ( in_array( $status, Products::$broken_module_statuses, true ) ) {
continue;
}
// If the module is active and not already in the array, add it
if (
in_array( $status, Products::$active_module_statuses, true ) &&
! in_array( $product_slug, $historically_active_modules, true )
) {
$historically_active_modules[] = $product_slug;
}
// If the module has been disabled due to a manual user action,
// or because of a missing plan error, remove it from the array
if ( in_array( $status, Products::$disabled_module_statuses, true ) ) {
$historically_active_modules = array_values( array_diff( $historically_active_modules, array( $product_slug ) ) );
}
}
\Jetpack_Options::update_option( 'historically_active_modules', array_unique( $historically_active_modules ) );
add_action( 'jetpack_activate_module', array( $ham, 'update_historically_active_jetpack_modules' ), 5 );
}
/**
@ -794,12 +649,20 @@ class Initializer {
*/
public static function maybe_show_red_bubble() {
global $menu;
// Don't show red bubble alerts for non-admin users
// These alerts are generally only actionable for admins
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$rbn = new Red_Bubble_Notifications();
// filters for the items in this file
add_filter( 'my_jetpack_red_bubble_notification_slugs', array( __CLASS__, 'add_red_bubble_alerts' ) );
add_filter( 'my_jetpack_red_bubble_notification_slugs', array( $rbn, 'add_red_bubble_alerts' ) );
$red_bubble_alerts = array_filter(
self::get_red_bubble_alerts(),
$rbn::get_red_bubble_alerts(),
function ( $alert ) {
// We don't want to show silent alerts
// We don't want to show the red bubble for silent alerts
return empty( $alert['is_silent'] );
}
);
@ -816,24 +679,6 @@ class Initializer {
}
}
/**
* Collect all possible alerts that we might use a red bubble notification for
*
* @return array
*/
public static function get_red_bubble_alerts() {
static $red_bubble_alerts = array();
// using a static cache since we call this function more than once in the class
if ( ! empty( $red_bubble_alerts ) ) {
return $red_bubble_alerts;
}
// go find the alerts
$red_bubble_alerts = apply_filters( 'my_jetpack_red_bubble_notification_slugs', $red_bubble_alerts );
return $red_bubble_alerts;
}
/**
* Get list of module names sorted by their recommendation score
*
@ -850,111 +695,4 @@ class Initializer {
return array_keys( $recommendations_evaluation ); // Get only module names
}
/**
* Check for features broken by a disconnected user or site
*
* @return array
*/
public static function check_for_broken_modules() {
$connection = new Connection_Manager();
$is_user_connected = $connection->is_user_connected() || $connection->has_connected_owner();
$is_site_connected = $connection->is_connected();
$broken_modules = array(
'needs_site_connection' => array(),
'needs_user_connection' => array(),
);
if ( $is_user_connected && $is_site_connected ) {
return $broken_modules;
}
$products = Products::get_products_classes();
$historically_active_modules = \Jetpack_Options::get_option( 'historically_active_modules', array() );
foreach ( $products as $product ) {
if ( ! in_array( $product::$slug, $historically_active_modules, true ) ) {
continue;
}
if ( $product::$requires_user_connection && ! $is_user_connected ) {
if ( ! in_array( $product::$slug, $broken_modules['needs_user_connection'], true ) ) {
$broken_modules['needs_user_connection'][] = $product::$slug;
}
} elseif ( ! $is_site_connected ) {
if ( ! in_array( $product::$slug, $broken_modules['needs_site_connection'], true ) ) {
$broken_modules['needs_site_connection'][] = $product::$slug;
}
}
}
return $broken_modules;
}
/**
* Add relevant red bubble notifications
*
* @param array $red_bubble_slugs - slugs that describe the reasons the red bubble is showing.
* @return array
*/
public static function add_red_bubble_alerts( array $red_bubble_slugs ) {
if ( wp_doing_ajax() ) {
return array();
}
$connection = new Connection_Manager();
$welcome_banner_dismissed = \Jetpack_Options::get_option( 'dismissed_welcome_banner', false );
if ( self::is_jetpack_user_new() && ! $welcome_banner_dismissed ) {
$red_bubble_slugs['welcome-banner-active'] = array(
'is_silent' => $connection->is_connected(), // we don't display the red bubble if the user is connected
);
return $red_bubble_slugs;
} else {
return self::alert_if_missing_connection( $red_bubble_slugs );
}
}
/**
* Add an alert slug if the site is missing a site connection
*
* @param array $red_bubble_slugs - slugs that describe the reasons the red bubble is showing.
* @return array
*/
public static function alert_if_missing_connection( array $red_bubble_slugs ) {
$broken_modules = self::check_for_broken_modules();
$connection = new Connection_Manager();
if ( ! empty( $broken_modules['needs_user_connection'] ) ) {
$red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
'type' => 'user',
'is_error' => true,
);
return $red_bubble_slugs;
}
if ( ! empty( $broken_modules['needs_site_connection'] ) ) {
$red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
'type' => 'site',
'is_error' => true,
);
return $red_bubble_slugs;
}
if ( ! $connection->is_user_connected() && ! $connection->has_connected_owner() ) {
$red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
'type' => 'user',
'is_error' => false,
);
return $red_bubble_slugs;
}
if ( ! $connection->is_connected() ) {
$red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
'type' => 'site',
'is_error' => false,
);
return $red_bubble_slugs;
}
return $red_bubble_slugs;
}
}

View File

@ -13,6 +13,8 @@ use Automattic\Jetpack\Admin_UI\Admin_Menu;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Redirect;
use WP_Error;
use WP_Rest_Response;
/**
* Jetpack Manage features in My Jetpack.
@ -25,6 +27,35 @@ class Jetpack_Manage {
add_action( 'admin_menu', array( self::class, 'add_submenu_jetpack' ) );
}
/**
* Register the REST API routes.
*
* @return void
*/
public static function register_rest_endpoints() {
register_rest_route(
'my-jetpack/v1',
'jetpack-manage/data',
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_jetpack_manage_data',
'permission_callback' => __CLASS__ . '::permissions_callback',
)
);
}
/**
* Check user capabilities to access historically active modules.
*
* @access public
* @static
*
* @return true|WP_Error
*/
public static function permissions_callback() {
return current_user_can( 'manage_options' );
}
/**
* The page to be added to submenu
*
@ -121,4 +152,21 @@ class Jetpack_Manage {
return $partner->partner_type === 'agency';
}
/**
* Get Jetpack Manage data for REST API.
*
* @return WP_Error|WP_REST_Response
*/
public static function get_jetpack_manage_data() {
$is_enabled = self::could_use_jp_manage();
$is_agency_account = self::is_agency_account();
return rest_ensure_response(
array(
'isEnabled' => $is_enabled,
'isAgencyAccount' => $is_agency_account,
)
);
}
}

View File

@ -16,17 +16,21 @@ class Products {
*
* @var string
*/
const STATUS_SITE_CONNECTION_ERROR = 'site_connection_error';
const STATUS_USER_CONNECTION_ERROR = 'user_connection_error';
const STATUS_ACTIVE = 'active';
const STATUS_CAN_UPGRADE = 'can_upgrade';
const STATUS_INACTIVE = 'inactive';
const STATUS_MODULE_DISABLED = 'module_disabled';
const STATUS_PLUGIN_ABSENT = 'plugin_absent';
const STATUS_PLUGIN_ABSENT_WITH_PLAN = 'plugin_absent_with_plan';
const STATUS_NEEDS_PLAN = 'needs_plan';
const STATUS_NEEDS_ACTIVATION = 'needs_activation';
const STATUS_NEEDS_FIRST_SITE_CONNECTION = 'needs_first_site_connection';
public const STATUS_SITE_CONNECTION_ERROR = 'site_connection_error';
public const STATUS_USER_CONNECTION_ERROR = 'user_connection_error';
public const STATUS_ACTIVE = 'active';
public const STATUS_CAN_UPGRADE = 'can_upgrade';
public const STATUS_EXPIRING_SOON = 'expiring';
public const STATUS_EXPIRED = 'expired';
public const STATUS_INACTIVE = 'inactive';
public const STATUS_MODULE_DISABLED = 'module_disabled';
public const STATUS_PLUGIN_ABSENT = 'plugin_absent';
public const STATUS_PLUGIN_ABSENT_WITH_PLAN = 'plugin_absent_with_plan';
public const STATUS_NEEDS_PLAN = 'needs_plan';
public const STATUS_NEEDS_ACTIVATION = 'needs_activation';
public const STATUS_NEEDS_FIRST_SITE_CONNECTION = 'needs_first_site_connection';
public const STATUS_NEEDS_ATTENTION__WARNING = 'needs_attention_warning';
public const STATUS_NEEDS_ATTENTION__ERROR = 'needs_attention_error';
/**
* List of statuses that display the module as disabled
@ -64,6 +68,8 @@ class Products {
self::STATUS_USER_CONNECTION_ERROR,
self::STATUS_PLUGIN_ABSENT_WITH_PLAN,
self::STATUS_NEEDS_PLAN,
self::STATUS_NEEDS_ATTENTION__ERROR,
self::STATUS_NEEDS_ATTENTION__WARNING,
);
/**
@ -76,6 +82,16 @@ class Products {
self::STATUS_CAN_UPGRADE,
);
/**
* List of statuses that display the module as active
*
* @var array
*/
public static $expiring_or_expired_module_statuses = array(
self::STATUS_EXPIRING_SOON,
self::STATUS_EXPIRED,
);
/**
* List of all statuses that a product can have
*
@ -86,6 +102,8 @@ class Products {
self::STATUS_USER_CONNECTION_ERROR,
self::STATUS_ACTIVE,
self::STATUS_CAN_UPGRADE,
self::STATUS_EXPIRING_SOON,
self::STATUS_EXPIRED,
self::STATUS_INACTIVE,
self::STATUS_MODULE_DISABLED,
self::STATUS_PLUGIN_ABSENT,
@ -93,6 +111,8 @@ class Products {
self::STATUS_NEEDS_PLAN,
self::STATUS_NEEDS_ACTIVATION,
self::STATUS_NEEDS_FIRST_SITE_CONNECTION,
self::STATUS_NEEDS_ATTENTION__WARNING,
self::STATUS_NEEDS_ATTENTION__ERROR,
);
/**
@ -105,21 +125,28 @@ class Products {
*/
public static function get_products_classes() {
$classes = array(
'anti-spam' => Products\Anti_Spam::class,
'backup' => Products\Backup::class,
'boost' => Products\Boost::class,
'crm' => Products\Crm::class,
'creator' => Products\Creator::class,
'extras' => Products\Extras::class,
'jetpack-ai' => Products\Jetpack_Ai::class,
'scan' => Products\Scan::class,
'search' => Products\Search::class,
'social' => Products\Social::class,
'security' => Products\Security::class,
'protect' => Products\Protect::class,
'videopress' => Products\Videopress::class,
'stats' => Products\Stats::class,
'ai' => Products\Jetpack_Ai::class,
'anti-spam' => Products\Anti_Spam::class,
'backup' => Products\Backup::class,
'boost' => Products\Boost::class,
'crm' => Products\Crm::class,
'creator' => Products\Creator::class,
'extras' => Products\Extras::class,
'jetpack-ai' => Products\Jetpack_Ai::class,
// TODO: Remove this duplicate class ('ai')? See: https://github.com/Automattic/jetpack/pull/35910#pullrequestreview-2456462227
'ai' => Products\Jetpack_Ai::class,
'scan' => Products\Scan::class,
'search' => Products\Search::class,
'social' => Products\Social::class,
'security' => Products\Security::class,
'protect' => Products\Protect::class,
'videopress' => Products\Videopress::class,
'stats' => Products\Stats::class,
'growth' => Products\Growth::class,
'complete' => Products\Complete::class,
// Features
'newsletter' => Products\Newsletter::class,
'site-accelerator' => Products\Site_Accelerator::class,
'related-posts' => Products\Related_Posts::class,
);
/**
@ -149,17 +176,99 @@ class Products {
return $final_classes;
}
/**
* Register endpoints related to product classes
*
* @return void
*/
public static function register_product_endpoints() {
$classes = self::get_products_classes();
foreach ( $classes as $class ) {
$class::register_endpoints();
}
}
/**
* List of product slugs that are displayed on the main My Jetpack page
*
* @var array
*/
public static $shown_products = array(
'anti-spam',
'backup',
'boost',
'crm',
'jetpack-ai',
'search',
'social',
'protect',
'videopress',
'stats',
);
/**
* Gets the list of product slugs that are Not displayed on the main My Jetpack page
*
* @return array
*/
public static function get_not_shown_products() {
return array_diff( array_keys( static::get_products_classes() ), self::$shown_products );
}
/**
* Product data
*
* @param array $product_slugs (optional) An array of specified product slugs.
* @return array Jetpack products on the site and their availability.
*/
public static function get_products() {
$products = array();
foreach ( self::get_products_classes() as $class ) {
$product_slug = $class::$slug;
$products[ $product_slug ] = $class::get_info();
public static function get_products( $product_slugs = array() ) {
$all_classes = self::get_products_classes();
$products = array();
// If an array of $product_slugs are passed, return only the products specified in $product_slugs array
if ( $product_slugs ) {
foreach ( $product_slugs as $product_slug ) {
if ( isset( $all_classes[ $product_slug ] ) ) {
$class = $all_classes[ $product_slug ];
$products[ $product_slug ] = $class::get_info();
}
}
return $products;
}
// Otherwise return All products.
foreach ( $all_classes as $slug => $class ) {
$products[ $slug ] = $class::get_info();
}
return $products;
}
/**
* Get products data related to the wpcom api
*
* @param array $product_slugs - (optional) An array of specified product slugs.
* @return array
*/
public static function get_products_api_data( $product_slugs = array() ) {
$all_classes = self::get_products_classes();
$products = array();
// If an array of $product_slugs are passed, return only the products specified in $product_slugs array
if ( $product_slugs ) {
foreach ( $product_slugs as $product_slug ) {
if ( isset( $all_classes[ $product_slug ] ) ) {
$class = $all_classes[ $product_slug ];
$products[ $product_slug ] = $class::get_wpcom_info();
}
}
return $products;
}
// Otherwise return All products.
foreach ( $all_classes as $slug => $class ) {
$products[ $slug ] = $class::get_wpcom_info();
}
return $products;
}
@ -341,7 +450,7 @@ class Products {
'protect',
'crm',
'search',
'ai',
'jetpack-ai',
);
// Add plugin action links for the core Jetpack plugin.

View File

@ -0,0 +1,399 @@
<?php
/**
* Sets up the Red Bubble Notifications rest api endpoint and helper functions
*
* @package automattic/my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Jetpack_Options;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
/**
* Registers REST route for getting red bubble notification data
* and includes all helper functions related to red bubble notifications
*/
class Red_Bubble_Notifications {
private const MISSING_CONNECTION_NOTIFICATION_KEY = 'missing-connection';
private const MY_JETPACK_RED_BUBBLE_TRANSIENT_KEY = 'my-jetpack-red-bubble-transient';
/**
* Summary of register_rest_routes
*
* @return void
*/
public static function register_rest_endpoints() {
register_rest_route(
'my-jetpack/v1',
'red-bubble-notifications',
array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => __CLASS__ . '::rest_api_get_red_bubble_alerts',
'permission_callback' => __CLASS__ . '::permissions_callback',
'args' => array(
'dismissal_cookies' => array(
'type' => 'array',
'description' => 'Array of dismissal cookies to set for the red bubble notifications.',
'required' => false,
'items' => array(
'type' => 'string',
),
'sanitize_callback' => function ( $param ) {
return array_map( 'sanitize_text_field', $param );
},
),
),
)
);
}
/**
* Check user capability to access the endpoint.
*
* @access public
* @static
*
* @return true|WP_Error
*/
public static function permissions_callback() {
return current_user_can( 'edit_posts' );
}
/**
* Gets the plugins that need installed or activated for each paid plan.
*
* @return array
*/
public static function get_paid_plans_plugins_requirements() {
$plugin_requirements = array();
foreach ( Products::get_products_classes() as $slug => $product_class ) {
// Skip these- we don't show them in My Jetpack.
if ( in_array( $slug, Products::get_not_shown_products(), true ) ) {
continue;
}
if ( ! $product_class::has_paid_plan_for_product() ) {
continue;
}
$purchase = $product_class::get_paid_plan_purchase_for_product();
if ( ! $purchase ) {
continue;
}
// Check if required plugin needs installed or activated.
if ( ! $product_class::is_plugin_installed() ) {
// Plugin needs installed (and activated)
$plugin_requirements[ $purchase->product_slug ]['needs_installed'][] = $product_class::$slug;
} elseif ( ! $product_class::is_plugin_active() ) {
// Plugin is installed, but not activated.
$plugin_requirements[ $purchase->product_slug ]['needs_activated_only'][] = $product_class::$slug;
}
}
return $plugin_requirements;
}
/**
* Check for features broken by a disconnected user or site
*
* @return array
*/
public static function check_for_broken_modules() {
$connection = new Connection_Manager();
$is_user_connected = $connection->is_user_connected() || $connection->has_connected_owner();
$is_site_connected = $connection->is_connected();
$broken_modules = array(
'needs_site_connection' => array(),
'needs_user_connection' => array(),
);
if ( $is_user_connected && $is_site_connected ) {
return $broken_modules;
}
$products = Products::get_products_classes();
$historically_active_modules = Jetpack_Options::get_option( 'historically_active_modules', array() );
foreach ( $products as $product ) {
if ( ! in_array( $product::$slug, $historically_active_modules, true ) ) {
continue;
}
if ( $product::$requires_user_connection && ! $is_user_connected ) {
if ( ! in_array( $product::$slug, $broken_modules['needs_user_connection'], true ) ) {
$broken_modules['needs_user_connection'][] = $product::$slug;
}
} elseif ( ! $is_site_connected ) {
if ( ! in_array( $product::$slug, $broken_modules['needs_site_connection'], true ) ) {
$broken_modules['needs_site_connection'][] = $product::$slug;
}
}
}
return $broken_modules;
}
/**
* Add an alert slug if the site is missing a site connection
*
* @param array $red_bubble_slugs - slugs that describe the reasons the red bubble is showing.
* @return array
*/
public static function alert_if_missing_connection( array $red_bubble_slugs ) {
$broken_modules = self::check_for_broken_modules();
$connection = new Connection_Manager();
// Checking for site connection issues first.
if ( ! empty( $broken_modules['needs_site_connection'] ) ) {
$red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
'type' => 'site',
'is_error' => true,
);
return $red_bubble_slugs;
}
if ( ! empty( $broken_modules['needs_user_connection'] ) ) {
$red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
'type' => 'user',
'is_error' => true,
);
return $red_bubble_slugs;
}
if ( ! $connection->is_connected() ) {
$red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
'type' => 'site',
'is_error' => false,
);
return $red_bubble_slugs;
}
return $red_bubble_slugs;
}
/**
* Add an alert slug if Backups are failing or having an issue.
*
* @param array $red_bubble_slugs - slugs that describe the reasons the red bubble is showing.
* @return array
*/
public static function alert_if_last_backup_failed( array $red_bubble_slugs ) {
// Make sure the Notice wasn't previously dismissed.
if ( ! empty( $_COOKIE['backup_failure_dismissed'] ) ) {
return $red_bubble_slugs;
}
// Make sure there's a Backup paid plan
if ( ! Products\Backup::is_plugin_active() || ! Products\Backup::has_paid_plan_for_product() ) {
return $red_bubble_slugs;
}
// Make sure the plan isn't just recently purchased in last 30min.
// Give some time to queue & run the first backup.
$purchase = Products\Backup::get_paid_plan_purchase_for_product();
if ( $purchase ) {
$thirty_minutes_after_plan_purchase = strtotime( $purchase->subscribed_date . ' +30 minutes' );
if ( strtotime( 'now' ) < $thirty_minutes_after_plan_purchase ) {
return $red_bubble_slugs;
}
}
$backup_failed_status = Products\Backup::does_module_need_attention();
if ( $backup_failed_status ) {
$red_bubble_slugs['backup_failure'] = $backup_failed_status;
}
return $red_bubble_slugs;
}
/**
* Add an alert slug if Protect has scan threats/vulnerabilities.
*
* @param array $red_bubble_slugs - slugs that describe the reasons the red bubble is showing.
* @return array
*/
public static function alert_if_protect_has_threats( array $red_bubble_slugs ) {
// Make sure the Notice hasn't been dismissed.
if ( ! empty( $_COOKIE['protect_threats_detected_dismissed'] ) ) {
return $red_bubble_slugs;
}
// Make sure we're dealing with the Protect product only
if ( ! Products\Protect::has_paid_plan_for_product() ) {
return $red_bubble_slugs;
}
$protect_threats_status = Products\Protect::does_module_need_attention();
if ( $protect_threats_status ) {
$red_bubble_slugs['protect_has_threats'] = $protect_threats_status;
}
return $red_bubble_slugs;
}
/**
* Add an alert slug if any paid plan/products are expiring or expired.
*
* @param array $red_bubble_slugs - slugs that describe the reasons the red bubble is showing.
* @return array
*/
public static function alert_if_paid_plan_expiring( array $red_bubble_slugs ) {
$connection = new Connection_Manager();
if ( ! $connection->is_connected() ) {
return $red_bubble_slugs;
}
$product_classes = Products::get_products_classes();
$products_included_in_expiring_plan = array();
foreach ( $product_classes as $key => $product ) {
// Skip these- we don't show them in My Jetpack.
if ( in_array( $key, Products::get_not_shown_products(), true ) ) {
continue;
}
if ( $product::has_paid_plan_for_product() ) {
$purchase = $product::get_paid_plan_purchase_for_product();
if ( $purchase ) {
$redbubble_notice_data = array(
'product_slug' => $purchase->product_slug,
'product_name' => $purchase->product_name,
'expiry_date' => $purchase->expiry_date,
'expiry_message' => $purchase->expiry_message,
'manage_url' => $product::get_manage_paid_plan_purchase_url(),
);
if ( $product::is_paid_plan_expired() && empty( $_COOKIE[ "$purchase->product_slug--plan_expired_dismissed" ] ) ) {
$red_bubble_slugs[ "$purchase->product_slug--plan_expired" ] = $redbubble_notice_data;
if ( ! $product::is_bundle_product() ) {
$products_included_in_expiring_plan[ "$purchase->product_slug--plan_expired" ][] = $product::get_name();
}
}
if ( $product::is_paid_plan_expiring() && empty( $_COOKIE[ "$purchase->product_slug--plan_expiring_soon_dismissed" ] ) ) {
$red_bubble_slugs[ "$purchase->product_slug--plan_expiring_soon" ] = $redbubble_notice_data;
$red_bubble_slugs[ "$purchase->product_slug--plan_expiring_soon" ]['manage_url'] = $product::get_renew_paid_plan_purchase_url();
if ( ! $product::is_bundle_product() ) {
$products_included_in_expiring_plan[ "$purchase->product_slug--plan_expiring_soon" ][] = $product::get_name();
}
}
}
}
}
foreach ( $products_included_in_expiring_plan as $expiring_plan => $products ) {
$red_bubble_slugs[ $expiring_plan ]['products_effected'] = $products;
}
return $red_bubble_slugs;
}
/**
* Add an alert slug if a site's paid plan requires a plugin install and/or activation.
*
* @param array $red_bubble_slugs - slugs that describe the reasons the red bubble is showing.
* @return array
*/
public static function alert_if_paid_plan_requires_plugin_install_or_activation( array $red_bubble_slugs ) {
$connection = new Connection_Manager();
// Don't trigger red bubble (and show notice) when the site is not connected or if the
// user doesn't have plugin installation/activation permissions.
if ( ! $connection->is_connected() || ! current_user_can( 'activate_plugins' ) ) {
return $red_bubble_slugs;
}
$plugins_needing_installed_activated = self::get_paid_plans_plugins_requirements();
if ( empty( $plugins_needing_installed_activated ) ) {
return $red_bubble_slugs;
}
foreach ( $plugins_needing_installed_activated as $plan_slug => $plugins_requirements ) {
if ( empty( $_COOKIE[ "$plan_slug--plugins_needing_installed_dismissed" ] ) ) {
$red_bubble_slugs[ "$plan_slug--plugins_needing_installed_activated" ] = $plugins_requirements;
}
}
return $red_bubble_slugs;
}
/**
* Add relevant red bubble notifications
*
* @param array $red_bubble_slugs - slugs that describe the reasons the red bubble is showing.
* @return array
*/
public static function add_red_bubble_alerts( array $red_bubble_slugs ) {
if ( wp_doing_ajax() ) {
return array();
}
$connection = new Connection_Manager();
$welcome_banner_dismissed = Jetpack_Options::get_option( 'dismissed_welcome_banner', false );
if ( Initializer::is_jetpack_user_new() && ! $welcome_banner_dismissed ) {
$red_bubble_slugs['welcome-banner-active'] = array(
'is_silent' => $connection->is_connected(), // we don't display the red bubble if the user is connected
);
return $red_bubble_slugs;
} else {
return array_merge(
self::alert_if_missing_connection( $red_bubble_slugs ),
self::alert_if_last_backup_failed( $red_bubble_slugs ),
self::alert_if_paid_plan_expiring( $red_bubble_slugs ),
self::alert_if_protect_has_threats( $red_bubble_slugs ),
self::alert_if_paid_plan_requires_plugin_install_or_activation( $red_bubble_slugs )
);
}
}
/**
* Collect all possible alerts that we might use a red bubble notification for
*
* @param bool $bypass_cache - whether to bypass the red bubble cache.
* @return array
*/
public static function get_red_bubble_alerts( bool $bypass_cache = false ) {
static $red_bubble_alerts = array();
// check for stored alerts
$stored_alerts = get_transient( self::MY_JETPACK_RED_BUBBLE_TRANSIENT_KEY );
// Cache bypass for red bubbles should only happen on the My Jetpack page
if ( $stored_alerts !== false && ! ( $bypass_cache ) ) {
return $stored_alerts;
}
// go find the alerts
$red_bubble_alerts = apply_filters( 'my_jetpack_red_bubble_notification_slugs', $red_bubble_alerts );
// cache the alerts for one hour
set_transient( self::MY_JETPACK_RED_BUBBLE_TRANSIENT_KEY, $red_bubble_alerts, 3600 );
return $red_bubble_alerts;
}
/**
* Get the red bubble alerts, bypassing cache when called via the REST API
*
* @param WP_REST_Request $request The REST API request object.
*
* @return WP_Error|WP_REST_Response
*/
public static function rest_api_get_red_bubble_alerts( $request ) {
add_filter( 'my_jetpack_red_bubble_notification_slugs', array( __CLASS__, 'add_red_bubble_alerts' ) );
$cookies = $request->get_param( 'dismissal_cookies' );
// Update $_COOKIE superglobal with the provided cookies
if ( ! empty( $cookies ) && is_array( $cookies ) ) {
foreach ( $cookies as $cookie_string ) {
// Parse cookie string in format "name=value"
$parts = explode( '=', $cookie_string, 2 );
if ( count( $parts ) === 2 ) {
$name = trim( $parts[0] );
$value = trim( $parts[1] );
$_COOKIE[ $name ] = $value;
}
}
}
$red_bubble_alerts = self::get_red_bubble_alerts( true );
return rest_ensure_response( $red_bubble_alerts );
}
}

View File

@ -1,147 +0,0 @@
<?php
/**
* Sets up the Product Data REST API endpoints.
*
* @package automattic/my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Connection\Client;
use WP_Error;
/**
* Registers the REST routes for Product Data
*/
class REST_Product_Data {
/**
* Constructor.
*/
public function __construct() {
// Get backup undo event
register_rest_route(
'my-jetpack/v1',
'/site/backup/undo-event',
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_site_backup_undo_event',
'permission_callback' => __CLASS__ . '::permissions_callback',
)
);
register_rest_route(
'my-jetpack/v1',
'/site/backup/count-items',
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::count_things_that_can_be_backed_up',
'permission_callback' => __CLASS__ . '::permissions_callback',
)
);
}
/**
* Checks if the user has the correct permissions
*/
public static function permissions_callback() {
return current_user_can( 'manage_options' );
}
/**
* This will fetch the last rewindable event from the Activity Log and
* the last rewind_id prior to that.
*
* @return array|WP_Error|null
*/
public static function get_site_backup_undo_event() {
$blog_id = \Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_user(
'/sites/' . $blog_id . '/activity/rewindable?force=wpcom',
'v2',
array(),
null,
'wpcom'
);
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return null;
}
$body = json_decode( $response['body'], true );
if ( ! isset( $body['current'] ) ) {
return null;
}
// Preparing the response structure
$undo_event = array(
'last_rewindable_event' => null,
'undo_backup_id' => null,
);
// List of events that will not be considered to be undo.
// Basically we should not `undo` a full backup event, but we could
// use them to undo any other action like plugin updates.
$last_event_exceptions = array(
'rewind__backup_only_complete_full',
'rewind__backup_only_complete_initial',
'rewind__backup_only_complete',
'rewind__backup_complete_full',
'rewind__backup_complete_initial',
'rewind__backup_complete',
);
// Looping through the events to find the last rewindable event and the last backup_id.
// The idea is to find the last rewindable event and then the last rewind_id before that.
$found_last_event = false;
foreach ( $body['current']['orderedItems'] as $event ) {
if ( $event['is_rewindable'] ) {
if ( ! $found_last_event && ! in_array( $event['name'], $last_event_exceptions, true ) ) {
$undo_event['last_rewindable_event'] = $event;
$found_last_event = true;
} elseif ( $found_last_event ) {
$undo_event['undo_backup_id'] = $event['rewind_id'];
break;
}
}
}
return rest_ensure_response( $undo_event );
}
/**
* This will collect a count of all the items that could be backed up
* This is used to show what backup could be doing if it is not enabled
*
* @return WP_Error|\WP_REST_Response
*/
public static function count_things_that_can_be_backed_up() {
$image_mime_type = 'image';
$video_mime_type = 'video';
$audio_mime_type = 'audio';
$data = array();
// Add all post types together to get the total post count
$data['total_post_count'] = array_sum( (array) wp_count_posts( 'post' ) );
// Add all page types together to get the total page count
$data['total_page_count'] = array_sum( (array) wp_count_posts( 'page' ) );
// Add all comments together to get the total comment count
$comments = (array) wp_count_comments();
$data['total_comment_count'] = $comments ? $comments['total_comments'] : 0;
// Add all image attachments together to get the total image count
$data['total_image_count'] = array_sum( (array) wp_count_attachments( $image_mime_type ) );
// Add all video attachments together to get the total video count
$data['total_video_count'] = array_sum( (array) wp_count_attachments( $video_mime_type ) );
// Add all audio attachments together to get the total audio count
$data['total_audio_count'] = array_sum( (array) wp_count_attachments( $audio_mime_type ) );
return rest_ensure_response( $data );
}
}

View File

@ -23,47 +23,42 @@ class REST_Products {
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_products',
'permission_callback' => __CLASS__ . '::permissions_callback',
'callback' => __CLASS__ . '::get_products_api_data',
'permission_callback' => __CLASS__ . '::view_products_permissions_callback',
'args' => array(
'products' => array(
'description' => __( 'Comma seperated list of product slugs that should be retrieved.', 'jetpack-my-jetpack' ),
'type' => 'string',
'required' => false,
'validate_callback' => __CLASS__ . '::check_products_string',
),
),
),
'schema' => array( $this, 'get_products_schema' ),
)
);
$product_arg = array(
'description' => __( 'Product slug', 'jetpack-my-jetpack' ),
'type' => 'string',
'enum' => Products::get_products_slugs(),
$products_arg = array(
'description' => __( 'Array of Product slugs', 'jetpack-my-jetpack' ),
'type' => 'array',
'items' => array(
'enum' => Products::get_products_slugs(),
'type' => 'string',
),
'required' => true,
'validate_callback' => __CLASS__ . '::check_product_argument',
'validate_callback' => __CLASS__ . '::check_products_argument',
);
register_rest_route(
'my-jetpack/v1',
'site/products/(?P<product>[a-z\-]+)',
'site/products/install',
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_product',
'permission_callback' => __CLASS__ . '::permissions_callback',
'args' => array(
'product' => $product_arg,
),
),
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::activate_product',
'callback' => __CLASS__ . '::install_plugins',
'permission_callback' => __CLASS__ . '::edit_permissions_callback',
'args' => array(
'product' => $product_arg,
),
),
array(
'methods' => \WP_REST_Server::DELETABLE,
'callback' => __CLASS__ . '::deactivate_product',
'permission_callback' => __CLASS__ . '::edit_permissions_callback',
'args' => array(
'product' => $product_arg,
'products' => $products_arg,
),
),
)
@ -71,18 +66,45 @@ class REST_Products {
register_rest_route(
'my-jetpack/v1',
'site/products/(?P<product>[a-z\-]+)/install-standalone',
'site/products/activate',
array(
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::install_standalone',
'callback' => __CLASS__ . '::activate_products',
'permission_callback' => __CLASS__ . '::edit_permissions_callback',
'args' => array(
'product' => $product_arg,
'products' => $products_arg,
),
),
)
);
register_rest_route(
'my-jetpack/v1',
'site/products/deactivate',
array(
array(
'methods' => \WP_REST_Server::DELETABLE,
'callback' => __CLASS__ . '::deactivate_products',
'permission_callback' => __CLASS__ . '::edit_permissions_callback',
'args' => array(
'products' => $products_arg,
),
),
)
);
register_rest_route(
'my-jetpack/v1',
'site/products-ownership',
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_products_by_ownership',
'permission_callback' => __CLASS__ . '::view_products_permissions_callback',
),
)
);
}
/**
@ -112,7 +134,16 @@ class REST_Products {
}
/**
* Check Product arguments.
* Check if the user is permitted to view the product and product info
*
* @return bool
*/
public static function view_products_permissions_callback() {
return current_user_can( 'edit_posts' );
}
/**
* Check Products string (comma separated string).
*
* @access public
* @static
@ -120,7 +151,7 @@ class REST_Products {
* @param mixed $value - Value of the 'product' argument.
* @return true|WP_Error True if the value is valid, WP_Error otherwise.
*/
public static function check_product_argument( $value ) {
public static function check_products_string( $value ) {
if ( ! is_string( $value ) ) {
return new WP_Error(
'rest_invalid_param',
@ -129,28 +160,89 @@ class REST_Products {
);
}
$products_array = explode( ',', $value );
$all_products = Products::get_products_slugs();
foreach ( $products_array as $product_slug ) {
if ( ! in_array( $product_slug, $all_products, true ) ) {
return new WP_Error(
'rest_invalid_param',
esc_html(
sprintf(
/* translators: %s is the product_slug, it should Not be translated. */
__( 'The specified product argument %s is an invalid product.', 'jetpack-my-jetpack' ),
$product_slug
)
),
array( 'status' => 400 )
);
}
}
return true;
}
/**
* Check Products argument.
*
* @access public
* @static
*
* @param mixed $value - Value of the 'product' argument.
* @return true|WP_Error True if the value is valid, WP_Error otherwise.
*/
public static function check_products_argument( $value ) {
if ( ! is_array( $value ) ) {
return new WP_Error(
'rest_invalid_param',
esc_html__( 'The product argument must be an array.', 'jetpack-my-jetpack' ),
array( 'status' => 400 )
);
}
return true;
}
/**
* Site products endpoint.
*
* @return array of site products list.
* @param \WP_REST_Request $request The request object.
* @return WP_Error|\WP_REST_Response
*/
public static function get_products() {
$response = Products::get_products();
return rest_ensure_response( $response, 200 );
public static function get_products( $request ) {
$slugs = $request->get_param( 'products' );
$product_slugs = ! empty( $slugs ) ? array_map( 'trim', explode( ',', $slugs ) ) : array();
$response = Products::get_products( $product_slugs );
return rest_ensure_response( $response );
}
/**
* Site single product endpoint.
* Site API product data endpoint
*
* @param \WP_REST_Request $request The request object.
* @return array of site products list.
*
* @return WP_Error|\WP_REST_Response
*/
public static function get_product( $request ) {
$product_slug = $request->get_param( 'product' );
return rest_ensure_response( Products::get_product( $product_slug ), 200 );
public static function get_products_api_data( $request ) {
$slugs = $request->get_param( 'products' );
$product_slugs = ! empty( $slugs ) ? array_map( 'trim', explode( ',', $slugs ) ) : array();
$response = Products::get_products_api_data( $product_slugs );
return rest_ensure_response( $response );
}
/**
* Site products endpoint.
*
* @return \WP_REST_Response of site products list.
*/
public static function get_products_by_ownership() {
$response = array(
'unownedProducts' => Products::get_products_by_ownership( 'unowned' ),
'ownedProducts' => Products::get_products_by_ownership( 'owned' ),
);
return rest_ensure_response( $response );
}
/**
@ -169,81 +261,102 @@ class REST_Products {
}
/**
* Callback for activating a product
* Callback for activating products
*
* @param \WP_REST_Request $request The request object.
* @return \WP_REST_Response
* @return \WP_REST_Response|\WP_Error
*/
public static function activate_product( $request ) {
$product_slug = $request->get_param( 'product' );
$product = Products::get_product( $product_slug );
if ( ! isset( $product['class'] ) ) {
return new \WP_Error(
'not_implemented',
esc_html__( 'The product class handler is not implemented', 'jetpack-my-jetpack' ),
array( 'status' => 501 )
);
}
public static function activate_products( $request ) {
$products_array = $request->get_param( 'products' );
$activate_product_result = call_user_func( array( $product['class'], 'activate' ) );
if ( is_wp_error( $activate_product_result ) ) {
$activate_product_result->add_data( array( 'status' => 400 ) );
return $activate_product_result;
}
set_transient( 'my_jetpack_product_activated', $product_slug, 10 );
foreach ( $products_array as $product_slug ) {
$product = Products::get_product( $product_slug );
if ( ! isset( $product['class'] ) ) {
return new \WP_Error(
'product_class_handler_not_found',
sprintf(
/* translators: %s is the product_slug */
__( 'The product slug %s does not have an associated class handler.', 'jetpack-my-jetpack' ),
$product_slug
),
array( 'status' => 501 )
);
}
return rest_ensure_response( Products::get_product( $product_slug ), 200 );
$activate_product_result = call_user_func( array( $product['class'], 'activate' ) );
if ( is_wp_error( $activate_product_result ) ) {
$activate_product_result->add_data( array( 'status' => 400 ) );
return $activate_product_result;
}
}
set_transient( 'my_jetpack_product_activated', implode( ',', $products_array ), 10 );
return rest_ensure_response( Products::get_products( $products_array ) );
}
/**
* Callback for deactivating a product
* Callback for deactivating products
*
* @param \WP_REST_Request $request The request object.
* @return \WP_REST_Response
* @return \WP_REST_Response|\WP_Error
*/
public static function deactivate_product( $request ) {
$product_slug = $request->get_param( 'product' );
$product = Products::get_product( $product_slug );
if ( ! isset( $product['class'] ) ) {
return new \WP_Error(
'not_implemented',
esc_html__( 'The product class handler is not implemented', 'jetpack-my-jetpack' ),
array( 'status' => 501 )
);
public static function deactivate_products( $request ) {
$products_array = $request->get_param( 'products' );
foreach ( $products_array as $product_slug ) {
$product = Products::get_product( $product_slug );
if ( ! isset( $product['class'] ) ) {
return new \WP_Error(
'product_class_handler_not_found',
sprintf(
/* translators: %s is the product_slug */
__( 'The product slug %s does not have an associated class handler.', 'jetpack-my-jetpack' ),
$product_slug
),
array( 'status' => 501 )
);
}
$deactivate_product_result = call_user_func( array( $product['class'], 'deactivate' ) );
if ( is_wp_error( $deactivate_product_result ) ) {
$deactivate_product_result->add_data( array( 'status' => 400 ) );
return $deactivate_product_result;
}
}
$deactivate_product_result = call_user_func( array( $product['class'], 'deactivate' ) );
if ( is_wp_error( $deactivate_product_result ) ) {
$deactivate_product_result->add_data( array( 'status' => 400 ) );
return $deactivate_product_result;
}
return rest_ensure_response( Products::get_product( $product_slug ), 200 );
return rest_ensure_response( Products::get_products( $products_array ) );
}
/**
* Callback for installing the standalone plugin on a Hybrid Product.
* Callback for installing (and activating) multiple product plugins.
*
* @param \WP_REST_Request $request The request object.
* @return \WP_REST_Response
* @return \WP_REST_Response|\WP_Error
*/
public static function install_standalone( $request ) {
$product_slug = $request->get_param( 'product' );
$product = Products::get_product( $product_slug );
if ( ! isset( $product['class'] ) ) {
return new \WP_Error(
'not_implemented',
__( 'The product class handler is not implemented', 'jetpack-my-jetpack' ),
array( 'status' => 501 )
);
public static function install_plugins( $request ) {
$products_array = $request->get_param( 'products' );
foreach ( $products_array as $product_slug ) {
$product = Products::get_product( $product_slug );
if ( ! isset( $product['class'] ) ) {
return new \WP_Error(
'product_class_handler_not_found',
sprintf(
/* translators: %s is the product_slug */
__( 'The product slug %s does not have an associated class handler.', 'jetpack-my-jetpack' ),
$product_slug
),
array( 'status' => 501 )
);
}
$install_product_result = call_user_func( array( $product['class'], 'install_and_activate_standalone' ) );
if ( is_wp_error( $install_product_result ) ) {
$install_product_result->add_data( array( 'status' => 400 ) );
return $install_product_result;
}
}
$install_product_result = call_user_func( array( $product['class'], 'install_and_activate_standalone' ) );
if ( is_wp_error( $install_product_result ) ) {
$install_product_result->add_data( array( 'status' => 400 ) );
return $install_product_result;
}
return rest_ensure_response( Products::get_product( $product_slug ), 200 );
return rest_ensure_response( Products::get_products( $products_array ) );
}
}

View File

@ -58,7 +58,7 @@ class REST_Purchases {
/**
* Site purchases endpoint.
*
* @return array of site purchases.
* @return array|WP_Error of site purchases.
*/
public static function get_site_current_purchases() {
$site_id = \Jetpack_Options::get_option( 'id' );
@ -72,6 +72,6 @@ class REST_Purchases {
return new WP_Error( 'site_data_fetch_failed', 'Site data fetch failed', array( 'status' => $response_code ? $response_code : 400 ) );
}
return rest_ensure_response( $body, 200 );
return rest_ensure_response( $body );
}
}

View File

@ -141,6 +141,7 @@ class REST_Recommendations_Evaluation {
\Jetpack_Options::update_option( 'dismissed_recommendations', true );
if ( isset( $show_welcome_banner ) && $show_welcome_banner === 'true' ) {
\Jetpack_Options::update_option( 'recommendations_first_run', false );
\Jetpack_Options::delete_option( 'dismissed_welcome_banner' );
}

View File

@ -76,7 +76,7 @@ class REST_Zendesk_Chat {
public static function get_chat_authentication() {
$authentication = get_transient( self::ZENDESK_AUTH_TOKEN );
if ( $authentication ) {
return rest_ensure_response( $authentication, 200 );
return rest_ensure_response( $authentication );
}
$proxied = function_exists( 'wpcom_is_proxied_request' ) ? wpcom_is_proxied_request() : false;
@ -97,7 +97,7 @@ class REST_Zendesk_Chat {
}
set_transient( self::ZENDESK_AUTH_TOKEN, $body, self::TRANSIENT_EXPIRY );
return rest_ensure_response( $body, 200 );
return rest_ensure_response( $body );
}
/**
@ -117,6 +117,6 @@ class REST_Zendesk_Chat {
return new WP_Error( 'chat_config_data_fetch_failed', 'Chat config data fetch failed', array( 'status' => $response_code ) );
}
return rest_ensure_response( $body, 200 );
return rest_ensure_response( $body );
}
}

View File

@ -9,6 +9,7 @@ namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Current_Plan;
use Automattic\Jetpack\Status\Visitor;
use Jetpack_Options;
use WP_Error;
@ -349,6 +350,23 @@ class Wpcom_Products {
return $purchases;
}
/**
* Gets the site's currently active "plan" (bundle).
*
* @param bool $reload Whether to refresh data from wpcom or not.
* @return array
*/
public static function get_site_current_plan( $reload = false ) {
static $reloaded_already = false;
if ( $reload && ! $reloaded_already ) {
Current_Plan::refresh_from_wpcom();
$reloaded_already = true;
}
return Current_Plan::get();
}
/**
* Reset the request failures to retry the API requests.
*

View File

@ -36,6 +36,20 @@ class Anti_Spam extends Product {
*/
public static $plugin_slug = 'akismet';
/**
* The category of the product
*
* @var string
*/
public static $category = 'security';
/**
* The feature slug that identifies the paid plan
*
* @var string
*/
public static $feature_identifying_paid_plan = 'antispam';
/**
* Whether this product requires a user connection
*
@ -107,41 +121,32 @@ class Anti_Spam extends Product {
}
/**
* Determine if the site has an Akismet plan by checking for an API key
* Note that some Akismet Plans are free - we're just checking for an API key and don't have the perspective of the plan attached to it here
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return bool - whether an API key was found
* @return array
*/
public static function has_paid_plan_for_product() {
$products_with_anti_spam = array(
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_anti_spam',
'jetpack_complete',
'jetpack_security',
'jetpack_personal',
'jetpack_premium',
'jetpack_business',
'jetpack_anti_spam_monthly',
'jetpack_anti_spam_bi_yearly',
);
// Check if the site has an API key for Akismet
}
/**
* Check if the product has a free plan
* In this case we are only checking for an API key. The has_paid_plan_for_product will check to see if the specific site has a paid plan
*
* @return bool
*/
public static function has_free_plan_for_product() {
$akismet_api_key = apply_filters( 'akismet_get_api_key', defined( 'WPCOM_API_KEY' ) ? constant( 'WPCOM_API_KEY' ) : get_option( 'wordpress_api_key' ) );
$fallback = ! empty( $akismet_api_key );
// Check for existing plans
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return $fallback;
if ( ! empty( $akismet_api_key ) ) {
return true;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
foreach ( $products_with_anti_spam as $product ) {
if ( strpos( $purchase->product_slug, $product ) !== false ) {
return true;
}
}
}
}
return $fallback;
return false;
}
/**
@ -175,7 +180,7 @@ class Anti_Spam extends Product {
* @return boolean|array Products bundle list.
*/
public static function is_upgradable_by_bundle() {
return array( 'security' );
return array( 'security', 'complete' );
}
/**

View File

@ -1,6 +1,6 @@
<?php
/**
* Boost product
* Backup product
*
* @package my-jetpack
*/
@ -11,13 +11,13 @@ use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\My_Jetpack\Hybrid_Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use Automattic\Jetpack\Redirect;
use Jetpack_Options;
use WP_Error;
/**
* Class responsible for handling the Backup product
*/
class Backup extends Hybrid_Product {
public const BACKUP_STATUS_TRANSIENT_KEY = 'my-jetpack-backup-status';
/**
* The product slug
@ -44,6 +44,13 @@ class Backup extends Hybrid_Product {
*/
public static $plugin_slug = 'jetpack-backup';
/**
* The category of the product
*
* @var string
*/
public static $category = 'security';
/**
* Backup has a standalone plugin
*
@ -65,6 +72,32 @@ class Backup extends Hybrid_Product {
*/
public static $requires_plan = true;
/**
* The feature slug that identifies the paid plan
*
* @var string
*/
public static $feature_identifying_paid_plan = 'backups';
/**
* Backup initialization
*
* @return void
*/
public static function register_endpoints(): void {
parent::register_endpoints();
// Get backup undo event
register_rest_route(
'my-jetpack/v1',
'/site/backup/undo-event',
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_site_backup_undo_event',
'permission_callback' => __CLASS__ . '::permissions_callback',
)
);
}
/**
* Get the product name
*
@ -165,12 +198,82 @@ class Backup extends Hybrid_Product {
);
}
/**
* Checks if the user has the correct permissions
*/
public static function permissions_callback() {
return current_user_can( 'manage_options' );
}
/**
* This will fetch the last rewindable event from the Activity Log and
* the last rewind_id prior to that.
*
* @return array|WP_Error|null
*/
public static function get_site_backup_undo_event() {
$blog_id = \Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_user(
'/sites/' . $blog_id . '/activity/rewindable?force=wpcom',
'v2',
array(),
null,
'wpcom'
);
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return null;
}
$body = json_decode( $response['body'], true );
if ( ! isset( $body['current'] ) ) {
return null;
}
// Preparing the response structure
$undo_event = array(
'last_rewindable_event' => null,
'undo_backup_id' => null,
);
// List of events that will not be considered to be undo.
// Basically we should not `undo` a full backup event, but we could
// use them to undo any other action like plugin updates.
$last_event_exceptions = array(
'rewind__backup_only_complete_full',
'rewind__backup_only_complete_initial',
'rewind__backup_only_complete',
'rewind__backup_complete_full',
'rewind__backup_complete_initial',
'rewind__backup_complete',
);
// Looping through the events to find the last rewindable event and the last backup_id.
// The idea is to find the last rewindable event and then the last rewind_id before that.
$found_last_event = false;
foreach ( $body['current']['orderedItems'] as $event ) {
if ( $event['is_rewindable'] ) {
if ( ! $found_last_event && ! in_array( $event['name'], $last_event_exceptions, true ) ) {
$undo_event['last_rewindable_event'] = $event;
$found_last_event = true;
} elseif ( $found_last_event ) {
$undo_event['undo_backup_id'] = $event['rewind_id'];
break;
}
}
}
return rest_ensure_response( $undo_event );
}
/**
* Hits the wpcom api to check rewind status.
*
* @todo Maybe add caching.
*
* @return Object|WP_Error
* @return object|WP_Error
*/
private static function get_state_from_wpcom() {
static $status = null;
@ -179,9 +282,15 @@ class Backup extends Hybrid_Product {
return $status;
}
$site_id = Jetpack_Options::get_option( 'id' );
$site_id = \Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d/rewind', $site_id ) . '?force=wpcom', '2', array( 'timeout' => 2 ), null, 'wpcom' );
$response = Client::wpcom_json_api_request_as_blog(
sprintf( '/sites/%d/rewind', $site_id ) . '?force=wpcom',
'2',
array( 'timeout' => 2 ),
null,
'wpcom'
);
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
$status = new WP_Error( 'rewind_state_fetch_failed' );
@ -194,16 +303,98 @@ class Backup extends Hybrid_Product {
}
/**
* Checks whether the current plan (or purchases) of the site already supports the product
* Hits the wpcom api to retrieve the last 10 backup records.
*
* @return boolean
* @return object|WP_Error
*/
public static function has_paid_plan_for_product() {
$rewind_data = static::get_state_from_wpcom();
if ( is_wp_error( $rewind_data ) ) {
return false;
public static function get_latest_backups() {
static $backups = null;
if ( $backups !== null ) {
return $backups;
}
return is_object( $rewind_data ) && isset( $rewind_data->state ) && 'unavailable' !== $rewind_data->state;
$site_id = \Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_blog(
sprintf( '/sites/%d/rewind/backups', $site_id ) . '?force=wpcom',
'2',
array( 'timeout' => 2 ),
null,
'wpcom'
);
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
$backups = new WP_Error( 'rewind_backups_fetch_failed' );
return $backups;
}
$body = wp_remote_retrieve_body( $response );
$backups = json_decode( $body );
return $backups;
}
/**
* Determines whether the module/plugin/product needs the users attention.
* Typically due to some sort of error where user troubleshooting is needed.
*
* @return boolean|array
*/
public static function does_module_need_attention() {
$previous_backup_status = get_transient( self::BACKUP_STATUS_TRANSIENT_KEY );
// If we have a previous backup status, show it.
if ( ! empty( $previous_backup_status ) ) {
return $previous_backup_status === 'no_errors' ? false : $previous_backup_status;
}
$backup_failed_status = false;
// First check the status of Rewind for failure.
$rewind_state = self::get_state_from_wpcom();
if ( ! is_wp_error( $rewind_state ) ) {
if ( $rewind_state->state !== 'active' && $rewind_state->state !== 'provisioning' && $rewind_state->state !== 'awaiting_credentials' ) {
$backup_failed_status = array(
'type' => 'error',
'data' => array(
'source' => 'rewind',
'status' => isset( $rewind_state->reason ) && ! empty( $rewind_state->reason ) ? $rewind_state->reason : $rewind_state->state,
'last_updated' => $rewind_state->last_updated,
),
);
}
}
// Next check for a failed last backup.
$latest_backups = self::get_latest_backups();
if ( ! is_wp_error( $latest_backups ) ) {
// Get the last/latest backup record.
$last_backup = null;
foreach ( $latest_backups as $backup ) {
if ( $backup->is_backup ) {
$last_backup = $backup;
break;
}
}
if ( $last_backup && isset( $last_backup->status ) ) {
if ( $last_backup->status !== 'started' && ! preg_match( '/-will-retry$/', $last_backup->status ) && $last_backup->status !== 'finished' ) {
$backup_failed_status = array(
'type' => 'error',
'data' => array(
'source' => 'last_backup',
'status' => $last_backup->status,
'last_updated' => $last_backup->last_updated,
),
);
}
}
}
if ( is_array( $backup_failed_status ) && $backup_failed_status['type'] === 'error' ) {
set_transient( self::BACKUP_STATUS_TRANSIENT_KEY, $backup_failed_status, 5 * MINUTE_IN_SECONDS );
} else {
set_transient( self::BACKUP_STATUS_TRANSIENT_KEY, 'no_errors', HOUR_IN_SECONDS );
}
return $backup_failed_status;
}
/**
@ -213,7 +404,7 @@ class Backup extends Hybrid_Product {
* @return boolean|array Products bundle list.
*/
public static function is_upgradable_by_bundle() {
return array( 'security' );
return array( 'security', 'complete' );
}
/**
@ -239,4 +430,26 @@ class Backup extends Hybrid_Product {
return Redirect::get_url( 'my-jetpack-manage-backup' );
}
}
/**
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_backup_daily',
'jetpack_backup_daily_monthly',
'jetpack_backup_realtime',
'jetpack_backup_realtime_monthly',
'jetpack_backup_t1_yearly',
'jetpack_backup_t1_monthly',
'jetpack_backup_t1_bi_yearly',
'jetpack_backup_t2_yearly',
'jetpack_backup_t2_monthly',
'jetpack_backup_t0_yearly',
'jetpack_backup_t0_monthly',
);
}
}

View File

@ -44,6 +44,20 @@ class Boost extends Product {
*/
public static $plugin_slug = 'jetpack-boost';
/**
* The category of the product
*
* @var string
*/
public static $category = 'performance';
/**
* Defines whether or not to show a product interstitial as tiered pricing or not
*
* @var bool
*/
public static $is_tiered_pricing = true;
/**
* Boost has a standalone plugin
*
@ -65,6 +79,13 @@ class Boost extends Product {
*/
public static $has_free_offering = true;
/**
* The feature slug that identifies the paid plan
*
* @var string
*/
public static $feature_identifying_paid_plan = 'cloud-critical-css';
/**
* Get the product name
*
@ -134,7 +155,7 @@ class Boost extends Product {
public static function get_features_by_tier() {
return array(
array(
'name' => __( 'Optimize CSS Loading', 'jetpack-my-jetpack' ),
'name' => __( 'Auto CSS Optimization', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Move important styling information to the start of the page, which helps pages display your content sooner, so your users dont have to wait for the entire page to load. Commonly referred to as Critical CSS.',
@ -143,8 +164,8 @@ class Boost extends Product {
),
'tiers' => array(
self::FREE_TIER_SLUG => array(
'included' => true,
'description' => __( 'Must be done manually', 'jetpack-my-jetpack' ),
'included' => false,
'description' => __( 'Manual', 'jetpack-my-jetpack' ),
'info' => array(
'title' => __( 'Manual Critical CSS regeneration', 'jetpack-my-jetpack' ),
'content' => __(
@ -163,7 +184,7 @@ class Boost extends Product {
),
self::UPGRADED_TIER_SLUG => array(
'included' => true,
'description' => __( 'Automatically updated', 'jetpack-my-jetpack' ),
'description' => __( 'Included', 'jetpack-my-jetpack' ),
'info' => array(
'title' => __( 'Automatic Critical CSS regeneration', 'jetpack-my-jetpack' ),
'content' => __(
@ -176,15 +197,51 @@ class Boost extends Product {
),
),
array(
'name' => __( 'Defer non-essential JavaScript', 'jetpack-my-jetpack' ),
'name' => __( 'Automatic image size analysis', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Run non-essential JavaScript after the page has loaded so that styles and images can load more quickly.',
'Scan your site for images that arent properly sized for the device theyre being viewed on.',
'jetpack-my-jetpack'
),
'link' => array(
'id' => 'jetpack-boost-defer-js',
'title' => 'web.dev',
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Historical performance scores', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Get access to your historical performance scores and see advanced Core Web Vitals data.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Dedicated email support', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'<p>Paid customers get dedicated email support from our world-class Happiness Engineers to help with any issue.</p>
<p>All other questions are handled by our team as quickly as we are able to go through the WordPress support forum.</p>',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Page Cache', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Page caching speeds up load times by storing a copy of each web page on the first visit, allowing subsequent visits to be served instantly. This reduces server load and improves user experience by delivering content faster, without waiting for the page to be generated again.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
@ -193,15 +250,37 @@ class Boost extends Product {
),
),
array(
'name' => __( 'Lazy image loading', 'jetpack-my-jetpack' ),
'name' => __( 'Image CDN Quality Settings', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Improve page loading speed by only loading images when they are required.',
'Fine-tune image quality settings to your liking.',
'jetpack-my-jetpack'
),
'link' => array(
'id' => 'jetpack-boost-lazy-load',
'title' => 'web.dev',
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Image CDN Auto-Resize Lazy Images', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Optimizes lazy-loaded images by dynamically serving perfectly sized images for each device.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Image CDN', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Deliver images from Jetpack\'s Content Delivery Network. Automatically resizes your images to an appropriate size, converts them to modern efficient formats like WebP, and serves them from a worldwide network of servers.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
@ -223,10 +302,10 @@ class Boost extends Product {
),
),
array(
'name' => __( 'Image CDN', 'jetpack-my-jetpack' ),
'name' => __( 'Defer non-essential JavaScript', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Deliver images from Jetpack\'s Content Delivery Network. Automatically resizes your images to an appropriate size, converts them to modern efficient formats like WebP, and serves them from a worldwide network of servers.',
'Run non-essential JavaScript after the page has loaded so that styles and images can load more quickly.',
'jetpack-my-jetpack'
),
),
@ -236,16 +315,15 @@ class Boost extends Product {
),
),
array(
'name' => __( 'Dedicated email support', 'jetpack-my-jetpack' ),
'name' => __( 'Concatenate JS and CSS', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'<p>Paid customers get dedicated email support from our world-class Happiness Engineers to help with any issue.</p>
<p>All other questions are handled by our team as quickly as we are able to go through the WordPress support forum.</p>',
'Boost your website performance by merging and compressing JavaScript and CSS files, reducing site loading time and number of requests.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
@ -284,27 +362,6 @@ class Boost extends Product {
);
}
/**
* Checks whether the current plan (or purchases) of the site already supports the product
*
* @return boolean
*/
public static function has_paid_plan_for_product() {
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
// Boost is available as standalone bundle and as part of the Complete plan.
if ( strpos( $purchase->product_slug, 'jetpack_boost' ) !== false || str_starts_with( $purchase->product_slug, 'jetpack_complete' ) ) {
return true;
}
}
}
return false;
}
/**
* Get the URL where the user manages the product
*
@ -334,4 +391,28 @@ class Boost extends Product {
return $product_activation;
}
/**
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_boost_yearly',
'jetpack_boost_monthly',
'jetpack_boost_bi_yearly',
);
}
/**
* Return product bundles list
* that supports the product.
*
* @return boolean|array Products bundle list.
*/
public static function is_upgradable_by_bundle() {
return array( 'complete' );
}
}

View File

@ -0,0 +1,271 @@
<?php
/**
* Complete plan
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Module_Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use WP_Error;
/**
* Class responsible for handling the Complete plan
*/
class Complete extends Module_Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'complete';
/**
* The Jetpack module name
*
* @var string
*/
public static $module_name = 'complete';
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Complete Bundle';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Complete';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'The ultimate tool kit for best-in-class websites.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_long_description() {
return __( 'Get the full Jetpack suite with real-time security tools, improved site performance, and tools to grow your business.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
* Most of these are not translated as they are product names
*
* @return array Complete features list
*/
public static function get_features() {
return array(
'VaultPress Backup',
'Scan',
'Akismet Anti-spam',
'VideoPress',
'Boost',
'Social',
'Search',
'AI Assistant',
_x( 'Stats (100K site views, upgradeable)', 'Complete Product Feature', 'jetpack-my-jetpack' ),
_x( 'CRM Entrepreneur', 'Complete Product Feature', 'jetpack-my-jetpack' ),
_x( 'Newsletter and monetization tools', 'Complete Product Feature', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product pricing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
$product_slug = static::get_wpcom_product_slug();
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => $product_slug,
),
Wpcom_Products::get_product_pricing( $product_slug )
);
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_complete';
}
/**
* Checks whether the Jetpack module is active
*
* This is a bundle and not a product. We should not use this information for anything
*
* @return bool
*/
public static function is_module_active() {
return false;
}
/**
* Activates the product by installing and activating its plugin
*
* @param WP_Error|bool $current_result Is the result of the top level activation actions. You probably won't do anything if it is an WP_Error.
* @return bool|\WP_Error
*/
public static function do_product_specific_activation( $current_result ) {
$product_activation = parent::do_product_specific_activation( $current_result );
// A bundle is not a module. There's nothing in the plugin to be activated, so it's ok to fail to activate the module.
if ( is_wp_error( $product_activation ) && 'module_activation_failed' === $product_activation->get_error_code() ) {
return $product_activation;
}
// At this point, Jetpack plugin is installed. Let's activate each individual product.
$activation = Social::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
$activation = Stats::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
$activation = Anti_Spam::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
$activation = Backup::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
$activation = Scan::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
$activation = Boost::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
$activation = CRM::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
$activation = Search::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
$activation = VideoPress::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
return $activation;
}
/**
* Checks whether the Product is active
*
* Security is a bundle and not a module. Activation takes place on WPCOM. So lets consider it active if jetpack is active and has the plan.
*
* @return boolean
*/
public static function is_active() {
return static::is_jetpack_plugin_active() && static::has_required_plan();
}
/**
* Checks whether the current plan (or purchases) of the site already supports the product
*
* @return boolean
*/
public static function has_required_plan() {
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
if ( str_starts_with( $purchase->product_slug, 'jetpack_complete' ) ) {
return true;
}
}
}
return false;
}
/**
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_complete',
'jetpack_complete_monthly',
'jetpack_complete_bi_yearly',
);
}
/**
* Checks whether product is a bundle.
*
* @return boolean True
*/
public static function is_bundle_product() {
return true;
}
/**
* Return all the products it contains.
*
* @return array Product slugs
*/
public static function get_supported_products() {
return array(
'anti-spam',
'backup',
'boost',
'crm',
'scan',
'search',
'social',
'stats',
'videopress',
);
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return '';
}
}

View File

@ -319,24 +319,30 @@ class Creator extends Product {
}
/**
* Checks whether the current plan (or purchases) of the site already supports the product
* Get the product-slugs of the paid bundles/plans that this product/module is included in
*
* @return boolean
* @return array
*/
public static function has_paid_plan_for_product() {
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
// Creator is available as standalone bundle and as part of the Complete plan.
if ( strpos( $purchase->product_slug, 'jetpack_creator' ) !== false || str_starts_with( $purchase->product_slug, 'jetpack_complete' ) ) {
return true;
}
}
}
return false;
public static function get_paid_bundles_that_include_product() {
return array(
'jetpack_complete',
'jetpack_complete_monthly',
'jetpack_complete_bi-yearly',
);
}
/**
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_creator_yearly',
'jetpack_creator_monthly',
'jetpack_creator_bi_yearly',
);
}
/**

View File

@ -39,6 +39,13 @@ class Crm extends Product {
*/
public static $plugin_slug = 'zero-bs-crm';
/**
* The category of the product
*
* @var string
*/
public static $category = 'management';
/**
* Whether this product requires a user connection
*
@ -172,4 +179,27 @@ class Crm extends Product {
return false;
}
/**
* Get the product-slugs of the paid bundles/plans that this product/module is included in.
*
* @return array
*/
public static function get_paid_bundles_that_include_product() {
return array(
'jetpack_complete',
'jetpack_complete_monthly',
'jetpack_complete_bi_yearly',
);
}
/**
* Return product bundles list
* that supports the product.
*
* @return boolean|array Products bundle list.
*/
public static function is_upgradable_by_bundle() {
return array( 'complete' );
}
}

View File

@ -0,0 +1,223 @@
<?php
/**
* Growth plan
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Module_Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use WP_Error;
/**
* Class responsible for handling the Growth plan
*/
class Growth extends Module_Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'growth';
/**
* The Jetpack module name
*
* @var string
*/
public static $module_name = 'growth';
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Growth Bundle';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Growth';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Grow and track your audience effortlessly.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_long_description() {
return __( 'Essential tools to help you grow your audience, track visitor engagement, and turn leads into loyal customers and advocates.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized feature list
*
* @return array Growth features list
*/
public static function get_features() {
return array(
_x( 'Jetpack Social', 'Growth Product Feature', 'jetpack-my-jetpack' ),
_x( 'Jetpack Stats (10K site views, upgradeable)', 'Growth Product Feature', 'jetpack-my-jetpack' ),
_x( 'Unlimited subscriber imports', 'Growth Product Feature', 'jetpack-my-jetpack' ),
_x( 'Earn more from your content', 'Growth Product Feature', 'jetpack-my-jetpack' ),
_x( 'Accept payments with PayPal', 'Growth Product Feature', 'jetpack-my-jetpack' ),
_x( 'Increase earnings with WordAds', 'Growth Product Feature', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product pricing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
$product_slug = static::get_wpcom_product_slug();
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => $product_slug,
),
Wpcom_Products::get_product_pricing( $product_slug )
);
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_growth_yearly';
}
/**
* Checks whether the Jetpack module is active
*
* This is a bundle and not a product. We should not use this information for anything
*
* @return bool
*/
public static function is_module_active() {
return false;
}
/**
* Activates the product by installing and activating its plugin
*
* @param WP_Error|bool $current_result Is the result of the top level activation actions. You probably won't do anything if it is an WP_Error.
* @return bool|\WP_Error
*/
public static function do_product_specific_activation( $current_result ) {
$product_activation = parent::do_product_specific_activation( $current_result );
// A bundle is not a module. There's nothing in the plugin to be activated, so it's ok to fail to activate the module.
if ( is_wp_error( $product_activation ) && 'module_activation_failed' === $product_activation->get_error_code() ) {
return $product_activation;
}
// At this point, Jetpack plugin is installed. Let's activate each individual product.
$activation = Social::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
$activation = Stats::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
return $activation;
}
/**
* Checks whether the Product is active
*
* Growth is a bundle and not a module. Activation takes place on WPCOM. So lets consider it active if jetpack is active and has the plan.
*
* @return bool
*/
public static function is_active() {
return static::is_jetpack_plugin_active() && static::has_required_plan();
}
/**
* Checks whether the current plan (or purchase) of the site already supports the product
*
* @return bool
*/
public static function has_required_plan() {
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
if (
str_starts_with( $purchase->product_slug, 'jetpack_growth' ) ||
str_starts_with( $purchase->product_slug, 'jetpack_complete' )
) {
return true;
}
}
}
return false;
}
/**
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_growth_yearly',
'jetpack_growth_monthly',
'jetpack_growth_bi_yearly',
);
}
/**
* Checks whether the product is a bundle
*
* @return bool
*/
public static function is_bundle_product() {
return true;
}
/**
* Returns all products it contains.
*
* @return array Product slugs
*/
public static function get_supported_products() {
return array( 'social', 'stats' );
}
/**
* Get the URL where the user manages the product
*
* @return string
*/
public static function get_manage_url() {
return '';
}
}

View File

@ -28,6 +28,13 @@ class Jetpack_Ai extends Product {
*/
public static $slug = 'jetpack-ai';
/**
* The category of the product
*
* @var string
*/
public static $category = 'create';
/**
* Whether this product has a free offering
*
@ -36,20 +43,11 @@ class Jetpack_Ai extends Product {
public static $has_free_offering = true;
/**
* Get the Product info for the API
* The feature slug that identifies the paid plan
*
* @throws \Exception If required attribute is not declared in the child class.
* @return array
* @var string
*/
public static function get_info() {
// Call parent method to get the default info.
$info = parent::get_info();
// Populate the product with the feature data.
$info['ai-assistant-feature'] = self::get_ai_assistant_feature();
return $info;
}
public static $feature_identifying_paid_plan = 'ai-assistant';
/**
* Get the plugin slug - ovewrite it and return Jetpack's
@ -272,6 +270,7 @@ class Jetpack_Ai extends Product {
}
$features = array(
__( 'High request capacity *', 'jetpack-my-jetpack' ),
__( 'Generate text, tables, lists, and forms', 'jetpack-my-jetpack' ),
__( 'Easily refine content to your liking', 'jetpack-my-jetpack' ),
__( 'Make your content easier to read', 'jetpack-my-jetpack' ),
@ -444,23 +443,17 @@ class Jetpack_Ai extends Product {
}
/**
* Checks whether the site has a paid plan for this product
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return boolean
* @return array
*/
public static function has_paid_plan_for_product() {
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
if ( str_contains( $purchase->product_slug, 'jetpack_ai' ) ) {
return true;
}
}
}
return false;
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_ai_yearly',
'jetpack_ai_monthly',
'jetpack_ai_bi_yearly',
);
}
/**
@ -499,7 +492,7 @@ class Jetpack_Ai extends Product {
* @return ?string
*/
public static function get_post_checkout_url() {
return 'admin.php?page=my-jetpack#/jetpack-ai';
return self::get_manage_url();
}
/**
@ -508,7 +501,7 @@ class Jetpack_Ai extends Product {
* @return ?string
*/
public static function get_post_activation_url() {
return '/wp-admin/admin.php?page=my-jetpack#/jetpack-ai';
return self::get_manage_url();
}
/**
@ -517,7 +510,7 @@ class Jetpack_Ai extends Product {
* @return ?string
*/
public static function get_manage_url() {
return '/wp-admin/admin.php?page=my-jetpack#/jetpack-ai';
return admin_url( 'admin.php?page=my-jetpack#/jetpack-ai' );
}
/**
@ -610,7 +603,7 @@ class Jetpack_Ai extends Product {
* @return void
*/
public static function extend_plugin_action_links() {
add_action( 'admin_enqueue_scripts', array( static::class, 'admin_enqueue_scripts' ) );
add_action( 'myjetpack_enqueue_scripts', array( static::class, 'admin_enqueue_scripts' ) );
add_filter( 'default_content', array( static::class, 'add_ai_block' ), 10, 2 );
}
@ -648,9 +641,11 @@ class Jetpack_Ai extends Product {
* @param WP_Post $post The post object.
* @return string
*/
public static function add_ai_block( $content, WP_Post $post ) {
public static function add_ai_block( $content, $post ) {
if ( isset( $_GET['use_ai_block'] ) && isset( $_GET['_wpnonce'] )
&& wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'ai-assistant-content-nonce' )
&& ! empty( $post )
&& ! is_wp_error( $post )
&& current_user_can( 'edit_post', $post->ID )
&& '' === $content
) {

View File

@ -7,6 +7,7 @@
namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Jetpack;
use WP_Error;
@ -79,12 +80,42 @@ abstract class Module_Product extends Product {
return Jetpack::is_module_active( static::$module_name );
}
/**
* Get the product status.
* We don't use parent::get_status() to avoid complexity.
*
* @return string Product status.
*/
private static function get_feature_status() {
if ( ! static::is_plugin_installed() ) {
return Products::STATUS_PLUGIN_ABSENT;
}
if ( ! static::is_plugin_active() ) {
return Products::STATUS_INACTIVE;
}
if ( static::$requires_user_connection && ! ( new Connection_Manager() )->has_connected_owner() ) {
return Products::STATUS_USER_CONNECTION_ERROR;
}
if ( ! static::is_module_active() ) {
return Products::STATUS_MODULE_DISABLED;
}
return Products::STATUS_ACTIVE;
}
/**
* Gets the current status of the product
*
* @return string
*/
public static function get_status() {
if ( static::$is_feature ) {
return static::get_feature_status();
}
$status = parent::get_status();
if ( Products::STATUS_INACTIVE === $status && ! static::is_module_active() ) {
$status = Products::STATUS_MODULE_DISABLED;

View File

@ -0,0 +1,186 @@
<?php
/**
* Feature: Newsletter
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Module_Product;
use WP_Error;
/**
* Class responsible for handling the Newsletter module.
*/
class Newsletter extends Module_Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'newsletter';
/**
* The category of the product
*
* @var string
*/
public static $category = 'growth';
/**
* The slug of the plugin associated with this product.
* Newsletter is a feature available as part of the Jetpack plugin.
*
* @var string
*/
public static $plugin_slug = self::JETPACK_PLUGIN_SLUG;
/**
* The Plugin file associated with stats
*
* @var string|null
*/
public static $plugin_filename = self::JETPACK_PLUGIN_FILENAME;
/**
* The Jetpack module name associated with this product
*
* @var string|null
*/
public static $module_name = 'subscriptions';
/**
* Whether this module is a Jetpack feature
*
* @var boolean
*/
public static $is_feature = true;
/**
* Whether this product requires a user connection
*
* @var boolean
*/
public static $requires_user_connection = true;
/**
* Whether this product has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = false;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* Whether the product requires a plan to run
* The plan could be paid or free
*
* @var bool
*/
public static $requires_plan = false;
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Newsletter';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Newsletter';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Draw your readers from one post to another', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Draw your readers from one post to another, increasing overall traffic on your site', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized feature list
*
* @return array Newsletter features list
*/
public static function get_features() {
return array();
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array(
'available' => true,
'is_free' => true,
);
}
/**
* Checks whether the Product is active.
*
* @return boolean
*/
public static function is_active() {
return static::is_jetpack_plugin_active();
}
/**
* Checks whether the plugin is installed
*
* @return boolean
*/
public static function is_plugin_installed() {
return static::is_jetpack_plugin_installed();
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return admin_url( 'admin.php?page=jetpack#/settings?term=newsletter' );
}
/**
* Activates the Jetpack plugin
*
* @return null|WP_Error Null on success, WP_Error on invalid file.
*/
public static function activate_plugin(): ?WP_Error {
$plugin_filename = static::get_installed_plugin_filename( self::JETPACK_PLUGIN_SLUG );
if ( $plugin_filename ) {
return activate_plugin( $plugin_filename );
}
}
}

View File

@ -11,6 +11,7 @@ use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Modules;
use Automattic\Jetpack\Plugins_Installer;
use Automattic\Jetpack\Status;
use Jetpack_Options;
use WP_Error;
@ -47,6 +48,13 @@ abstract class Product {
*/
public static $plugin_slug = null;
/**
* The category of the product in the Jetpack ecosystem. The options are performance, growth, security, management, and create
*
* @var string
*/
public static $category = null;
/**
* The Jetpack plugin slug
*
@ -64,6 +72,27 @@ abstract class Product {
'jetpack-dev/jetpack.php',
);
/**
* The duration of time after the plan expiration date that we stop showing the plan status as "expired".
*
* @var string
*/
const EXPIRATION_CUTOFF_TIME = '+2 months';
/**
* Transient key for storing site features
*
* @var string;
*/
const MY_JETPACK_SITE_FEATURES_TRANSIENT_KEY = 'my-jetpack-site-features';
/**
* Whether this module is a Jetpack feature
*
* @var boolean
*/
public static $is_feature = false;
/**
* Whether this product requires a site connection
*
@ -100,6 +129,20 @@ abstract class Product {
*/
public static $requires_plan = false;
/**
* Defines whether or not to show a product interstitial as tiered pricing or not
*
* @var bool
*/
public static $is_tiered_pricing = false;
/**
* The feature slug that identifies the paid plan
*
* @var string
*/
public static $feature_identifying_paid_plan = '';
/**
* Get the plugin slug
*
@ -118,6 +161,24 @@ abstract class Product {
return static::$plugin_filename;
}
/**
* This method will be called in the class initializer to register the product's endpoints
*
* @return void
*/
public static function register_endpoints(): void {
// This method should be implemented in the child class.
}
/**
* Get data about the AI Assistant feature
*
* @return array
*/
public static function get_ai_assistant_feature() {
// This method should be optionally set in the child class.
return array();
}
/**
* Get the installed plugin filename, considering all possible filenames a plugin might have
*
@ -140,7 +201,7 @@ abstract class Product {
}
/**
* Get the Product info for the API
* Get the Static Product Info
*
* @throws \Exception If required attribute is not declared in the child class.
* @return array
@ -151,30 +212,29 @@ abstract class Product {
}
return array(
'slug' => static::$slug,
'plugin_slug' => static::$plugin_slug,
'plugin_slug' => static::get_plugin_slug(),
'name' => static::get_name(),
'title' => static::get_title(),
'category' => static::$category,
/* Maintain legacy compatibility with the old product info structure. See: #42271 */
'description' => static::get_description(),
'long_description' => static::get_long_description(),
'tiers' => static::get_tiers(),
'features' => static::get_features(),
'features_by_tier' => static::get_features_by_tier(),
/* End of legacy compatibility fields. */
'disclaimers' => static::get_disclaimers(),
'status' => static::get_status(),
'pricing_for_ui' => static::get_pricing_for_ui(),
'is_bundle' => static::is_bundle_product(),
'is_plugin_active' => static::is_plugin_active(),
'is_upgradable' => static::is_upgradable(),
'is_tiered_pricing' => static::$is_tiered_pricing,
'is_upgradable_by_bundle' => static::is_upgradable_by_bundle(),
'is_feature' => static::$is_feature,
'supported_products' => static::get_supported_products(),
'wpcom_product_slug' => static::get_wpcom_product_slug(),
'requires_user_connection' => static::$requires_user_connection,
'has_any_plan_for_product' => static::has_any_plan_for_product(),
'has_free_plan_for_product' => static::has_free_plan_for_product(),
'has_paid_plan_for_product' => static::has_paid_plan_for_product(),
'feature_identifying_paid_plan' => static::$feature_identifying_paid_plan,
'has_free_offering' => static::$has_free_offering,
'manage_url' => static::get_manage_url(),
'purchase_url' => static::get_purchase_url(),
'post_activation_url' => static::get_post_activation_url(),
'post_activation_urls_by_feature' => static::get_manage_urls_by_feature(),
'standalone_plugin_info' => static::get_standalone_info(),
@ -184,18 +244,60 @@ abstract class Product {
);
}
/**
* Get the Product Info that requires http requests to get
*
* @throws \Exception If required attribute is not declared in the child class.
* @return array
*/
public static function get_wpcom_info() {
if ( static::$slug === null ) {
throw new \Exception( 'Product classes must declare the $slug attribute.' );
}
$product_data = array(
'status' => static::get_status(),
'pricing_for_ui' => static::get_pricing_for_ui(),
'is_upgradable' => static::is_upgradable(),
'description' => static::get_description(),
'tiers' => static::get_tiers(),
'features' => static::get_features(),
'features_by_tier' => static::get_features_by_tier(),
'long_description' => static::get_long_description(),
'has_any_plan_for_product' => static::has_any_plan_for_product(),
'has_free_plan_for_product' => static::has_free_plan_for_product(),
'has_paid_plan_for_product' => static::has_paid_plan_for_product(),
'purchase_url' => static::get_purchase_url(),
'manage_paid_plan_purchase_url' => static::get_manage_paid_plan_purchase_url(),
'renew_paid_plan_purchase_url' => static::get_renew_paid_plan_purchase_url(),
'does_module_need_attention' => static::does_module_need_attention(),
);
if ( static::$slug === 'jetpack-ai' ) {
$product_data['ai-assistant-feature'] = static::get_ai_assistant_feature();
}
return $product_data;
}
/**
* Collect the site's active features
*
* @return WP_Error|array
*/
private static function get_site_features_from_wpcom() {
public static function get_site_features_from_wpcom() {
static $features = null;
if ( $features !== null ) {
return $features;
}
// Check for a cached value before doing lookup
$stored_features = get_transient( self::MY_JETPACK_SITE_FEATURES_TRANSIENT_KEY );
if ( $stored_features !== false ) {
return $stored_features;
}
$site_id = Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d/features', $site_id ), '1.1' );
@ -206,7 +308,13 @@ abstract class Product {
$body = wp_remote_retrieve_body( $response );
$feature_return = json_decode( $body );
$features = $feature_return->active;
$features = array(
'active' => $feature_return->active,
'available' => $feature_return->available,
);
// set a short transient to help with multiple lookups on the same page load.
set_transient( self::MY_JETPACK_SITE_FEATURES_TRANSIENT_KEY, $features, 15 );
return $features;
}
@ -228,7 +336,7 @@ abstract class Product {
return false;
}
return in_array( $feature, $features, true );
return in_array( $feature, $features['active'], true );
}
/**
@ -379,12 +487,42 @@ abstract class Product {
}
/**
* Checks whether the site has a paid plan for the product
* This ignores free products, it only checks if there is a purchase that supports the product
* Checks whether the site has a paid plan for the product.
*
* This function relies on the product's `$feature_identifying_paid_plan` and `get_paid_plan_product_slugs()` function.
* If the product does not define a `$feature_identifying_paid_plan`, be sure the product includes functions for both
* `get_paid_plan_product_slugs()` and `get_paid_bundles_that_include_product()` which return all the product slugs and
* bundle slugs that include the product, respectively.
*
* @return boolean
*/
public static function has_paid_plan_for_product() {
// First check site features (if there's a feature that identifies the paid plan)
if ( static::$feature_identifying_paid_plan ) {
if ( static::does_site_have_feature( static::$feature_identifying_paid_plan ) ) {
return true;
}
}
// Otherwise check site purchases
$plans_with_product = array_merge(
static::get_paid_bundles_that_include_product(),
static::get_paid_plan_product_slugs()
);
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
foreach ( $plans_with_product as $plan ) {
if ( strpos( $purchase->product_slug, $plan ) !== false ) {
return true;
}
}
}
}
return false;
}
@ -407,6 +545,164 @@ abstract class Product {
return static::has_paid_plan_for_product() || static::has_free_plan_for_product();
}
/**
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array();
}
/**
* Get the product-slugs of the paid bundles/plans that this product/module is included in.
*
* This function relies on the product's `$feature_identifying_paid_plan`
* If the product does not define a `$feature_identifying_paid_plan`, be sure to include this
* function in the product's class and have it return all the paid bundle slugs that include
* the product.
*
* @return array
*/
public static function get_paid_bundles_that_include_product() {
if ( static::is_bundle_product() ) {
return array();
}
$features = static::get_site_features_from_wpcom();
if ( is_wp_error( $features ) ) {
return array();
}
$idendifying_feature = static::$feature_identifying_paid_plan;
if ( empty( $features['available'] ) ) {
return array();
}
$paid_bundles = $features['available']->$idendifying_feature ?? array();
$current_bundle = Wpcom_Products::get_site_current_plan( true );
if ( in_array( static::$feature_identifying_paid_plan, $current_bundle['features']['active'], true ) ) {
$paid_bundles[] = $current_bundle['product_slug'];
}
return $paid_bundles;
}
/**
* Gets the paid plan's purchase/subsciption info, or null if no paid plan purchases.
*
* @return object|null
*/
public static function get_paid_plan_purchase_for_product() {
$paid_plans = array_merge(
static::get_paid_plan_product_slugs(),
static::get_paid_bundles_that_include_product()
);
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return null;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
foreach ( $paid_plans as $plan ) {
if ( strpos( $purchase->product_slug, $plan ) !== false ) {
return $purchase;
}
}
}
}
return null;
}
/**
* Gets the paid plan's expiry date.
*
* @return string
*/
public static function get_paid_plan_expiration_date() {
$purchase = static::get_paid_plan_purchase_for_product();
if ( ! $purchase ) {
return 'paid-plan-does-not-exist';
}
return $purchase->expiry_date;
}
/**
* Gets the paid plan's expiry status.
*
* @return string
*/
public static function get_paid_plan_expiration_status() {
$purchase = static::get_paid_plan_purchase_for_product();
if ( ! $purchase ) {
return 'paid-plan-does-not-exist';
}
return $purchase->expiry_status;
}
/**
* Checks if the paid plan is expired or not.
*
* @param bool $not_expired_after_cutoff - whether to not return the plan as expired if the plan has been expired for some duration of time.
* @return bool
*/
public static function is_paid_plan_expired( $not_expired_after_cutoff = false ) {
$expiry_status = static::get_paid_plan_expiration_status();
$expiry_date = static::get_paid_plan_expiration_date();
$expiry_cutoff = strtotime( $expiry_date . ' ' . self::EXPIRATION_CUTOFF_TIME );
return $not_expired_after_cutoff
? $expiry_status === Products::STATUS_EXPIRED && strtotime( 'now' ) < $expiry_cutoff
: $expiry_status === Products::STATUS_EXPIRED;
}
/**
* Checks if the paid plan is expiring soon or not.
*
* @return bool
*/
public static function is_paid_plan_expiring() {
$expiry_status = static::get_paid_plan_expiration_status();
return $expiry_status === Products::STATUS_EXPIRING_SOON;
}
/**
* Gets the url to manage the paid plan's purchased subscription (for plan renewal, canceling, removal, etc).
*
* @return string|null The url to the purchase management page.
*/
public static function get_manage_paid_plan_purchase_url() {
$purchase = static::get_paid_plan_purchase_for_product();
$site_suffix = ( new Status() )->get_site_suffix();
if ( $purchase && $site_suffix ) {
return 'https://wordpress.com/me/purchases/' . $site_suffix . '/' . $purchase->ID;
}
return null;
}
/**
* Gets the url to renew the paid plan's purchased subscription.
*
* @return string|null The url to the checkout renewal page.
*/
public static function get_renew_paid_plan_purchase_url() {
$purchase = static::get_paid_plan_purchase_for_product();
$site_suffix = ( new Status() )->get_site_suffix();
if ( $purchase && $site_suffix ) {
return 'https://wordpress.com/checkout/' . $purchase->product_slug . '/renew/' . $purchase->ID . '/' . $site_suffix;
}
return null;
}
/**
* Checks whether the product supports trial or not
*
@ -426,7 +722,7 @@ abstract class Product {
* @return boolean
*/
public static function is_upgradable() {
return false;
return ! static::has_paid_plan_for_product() && ! static::is_bundle_product();
}
/**
@ -506,6 +802,19 @@ abstract class Product {
}
} elseif ( static::$requires_user_connection && ! ( new Connection_Manager() )->has_connected_owner() ) {
$status = Products::STATUS_USER_CONNECTION_ERROR;
} elseif ( static::has_paid_plan_for_product() ) {
$needs_attention = static::does_module_need_attention();
if ( ! empty( $needs_attention ) && is_array( $needs_attention ) ) {
$status = Products::STATUS_NEEDS_ATTENTION__WARNING;
if ( isset( $needs_attention['type'] ) && 'error' === $needs_attention['type'] ) {
$status = Products::STATUS_NEEDS_ATTENTION__ERROR;
}
}
if ( static::is_paid_plan_expired() ) {
$status = Products::STATUS_EXPIRED;
} elseif ( static::is_paid_plan_expiring() ) {
$status = Products::STATUS_EXPIRING_SOON;
}
} elseif ( static::is_upgradable() ) {
$status = Products::STATUS_CAN_UPGRADE;
}
@ -772,4 +1081,14 @@ abstract class Product {
return true;
}
/**
* Determines whether the module/plugin/product needs the users attention.
* Typically due to some sort of error where user troubleshooting is needed.
*
* @return boolean
*/
public static function does_module_need_attention() {
return false;
}
}

View File

@ -7,16 +7,18 @@
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\My_Jetpack\Product;
use Automattic\Jetpack\My_Jetpack\Hybrid_Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use Jetpack_Options;
use Automattic\Jetpack\Protect_Status\Status as Protect_Status;
use Automattic\Jetpack\Redirect;
use Automattic\Jetpack\Waf\Waf_Runner;
use WP_Error;
use WP_REST_Response;
/**
* Class responsible for handling the Protect product
*/
class Protect extends Product {
class Protect extends Hybrid_Product {
const FREE_TIER_SLUG = 'free';
const UPGRADED_TIER_SLUG = 'upgraded';
@ -50,6 +52,20 @@ class Protect extends Product {
*/
public static $plugin_slug = 'jetpack-protect';
/**
* The category of the product
*
* @var string
*/
public static $category = 'security';
/**
* Defines whether or not to show a product interstitial as tiered pricing or not
*
* @var bool
*/
public static $is_tiered_pricing = true;
/**
* Whether this product requires a user connection
*
@ -71,6 +87,39 @@ class Protect extends Product {
*/
public static $has_standalone_plugin = true;
/**
* The feature slug that identifies the paid plan
*
* @var string
*/
public static $feature_identifying_paid_plan = 'scan';
/**
* Setup Protect REST API endpoints
*
* @return void
*/
public static function register_endpoints(): void {
parent::register_endpoints();
// Get Jetpack Protect data.
register_rest_route(
'my-jetpack/v1',
'/site/protect/data',
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_site_protect_data',
'permission_callback' => __CLASS__ . '::permissions_callback',
)
);
}
/**
* Checks if the user has the correct permissions
*/
public static function permissions_callback() {
return current_user_can( 'edit_posts' );
}
/**
* Get the product name
*
@ -121,33 +170,6 @@ class Protect extends Product {
);
}
/**
* Hits the wpcom api to check scan status.
*
* @todo Maybe add caching.
*
* @return Object|WP_Error
*/
private static function get_state_from_wpcom() {
static $status = null;
if ( $status !== null ) {
return $status;
}
$site_id = Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d/scan', $site_id ) . '?force=wpcom', '2', array( 'timeout' => 2 ), null, 'wpcom' );
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return new WP_Error( 'scan_state_fetch_failed' );
}
$body = wp_remote_retrieve_body( $response );
$status = json_decode( $body );
return $status;
}
/**
* Get the product's available tiers
*
@ -265,33 +287,55 @@ class Protect extends Product {
}
/**
* Checks whether the current plan (or purchases) of the site already supports the product
* Determines whether the module/plugin/product needs the users attention.
* Typically due to some sort of error where user troubleshooting is needed.
*
* @return boolean
* @return boolean|array
*/
public static function has_paid_plan_for_product() {
$plans_with_scan = array(
'jetpack_scan',
'jetpack_security',
'jetpack_complete',
'jetpack_premium',
'jetpack_business',
);
public static function does_module_need_attention() {
$protect_threat_status = false;
$scan_data = Protect_Status::get_status();
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
// Check if there are scan threats.
$protect_data = $scan_data;
if ( is_wp_error( $protect_data ) ) {
return $protect_threat_status; // false
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
foreach ( $plans_with_scan as $plan ) {
if ( strpos( $purchase->product_slug, $plan ) !== false ) {
return true;
}
}
}
$critical_threat_count = false;
if ( ! empty( $protect_data->threats ) ) {
$critical_threat_count = array_reduce(
$protect_data->threats,
function ( $accum, $threat ) {
return $threat->severity >= 5 ? ++$accum : $accum;
},
0
);
$protect_threat_status = array(
'type' => $critical_threat_count ? 'error' : 'warning',
'data' => array(
'threat_count' => count( $protect_data->threats ),
'critical_threat_count' => $critical_threat_count,
'fixable_threat_ids' => $protect_data->fixable_threat_ids,
),
);
}
return false;
return $protect_threat_status;
}
/**
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_scan',
'jetpack_scan_monthly',
'jetpack_scan_bi_yearly',
);
}
/**
@ -330,7 +374,13 @@ class Protect extends Product {
* @return ?string
*/
public static function get_manage_url() {
return admin_url( 'admin.php?page=jetpack-protect' );
// check standalone first
if ( static::is_standalone_plugin_active() ) {
return admin_url( 'admin.php?page=jetpack-protect' );
// otherwise, check for the main Jetpack plugin
} elseif ( static::is_jetpack_plugin_active() ) {
return Redirect::get_url( 'my-jetpack-manage-scan' );
}
}
/**
@ -354,4 +404,40 @@ class Protect extends Product {
public static function is_upgradable_by_bundle() {
return array( 'security', 'complete' );
}
/**
* Return site Jetpack Protect data for the REST API.
*
* @return WP_Rest_Response|WP_Error
*/
public static function get_site_protect_data() {
$scan_data = Protect_Status::get_status();
$waf_config = array();
$waf_supported = false;
$is_waf_enabled = false;
if ( class_exists( 'Automattic\Jetpack\Waf\Waf_Runner' ) ) {
// @phan-suppress-next-line PhanUndeclaredClassMethod
$waf_config = Waf_Runner::get_config();
// @phan-suppress-next-line PhanUndeclaredClassMethod
$is_waf_enabled = Waf_Runner::is_enabled();
// @phan-suppress-next-line PhanUndeclaredClassMethod
$waf_supported = Waf_Runner::is_supported_environment();
}
return rest_ensure_response(
array(
'scanData' => $scan_data,
'wafConfig' => array_merge(
$waf_config,
array(
'waf_supported' => $waf_supported,
'waf_enabled' => $is_waf_enabled,
),
array( 'blocked_logins' => (int) get_site_option( 'jetpack_protect_blocked_attempts', 0 ) )
),
)
);
}
}

View File

@ -0,0 +1,186 @@
<?php
/**
* Feature: Related Posts
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Module_Product;
use WP_Error;
/**
* Class responsible for handling the Related Posts module.
*/
class Related_Posts extends Module_Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'related-posts';
/**
* The slug of the plugin associated with this product.
* Related Posts is a feature available as part of the Jetpack plugin.
*
* @var string
*/
public static $plugin_slug = self::JETPACK_PLUGIN_SLUG;
/**
* The Plugin file associated with stats
*
* @var string|null
*/
public static $plugin_filename = self::JETPACK_PLUGIN_FILENAME;
/**
* The Jetpack module name associated with this product
*
* @var string|null
*/
public static $module_name = 'related-posts';
/**
* The category of the product
*
* @var string
*/
public static $category = 'growth';
/**
* Whether this module is a Jetpack feature
*
* @var boolean
*/
public static $is_feature = true;
/**
* Whether this product requires a user connection
*
* @var boolean
*/
public static $requires_user_connection = false;
/**
* Whether this product has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = false;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* Whether the product requires a plan to run
* The plan could be paid or free
*
* @var bool
*/
public static $requires_plan = false;
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Related Posts';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Related Posts';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Draw your readers from one post to another', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Draw your readers from one post to another, increasing overall traffic on your site', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized feature list
*
* @return array Newsletter features list
*/
public static function get_features() {
return array();
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array(
'available' => true,
'is_free' => true,
);
}
/**
* Checks whether the Product is active.
*
* @return boolean
*/
public static function is_active() {
return static::is_jetpack_plugin_active();
}
/**
* Checks whether the plugin is installed
*
* @return boolean
*/
public static function is_plugin_installed() {
return static::is_jetpack_plugin_installed();
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return admin_url( 'admin.php?page=jetpack#/traffic?term=related%20posts' );
}
/**
* Activates the Jetpack plugin
*
* @return null|WP_Error Null on success, WP_Error on invalid file.
*/
public static function activate_plugin(): ?WP_Error {
$plugin_filename = static::get_installed_plugin_filename( self::JETPACK_PLUGIN_SLUG );
if ( $plugin_filename ) {
return activate_plugin( $plugin_filename );
}
}
}

View File

@ -7,11 +7,9 @@
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\My_Jetpack\Module_Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use Automattic\Jetpack\Redirect;
use Jetpack_Options;
use WP_Error;
/**
@ -33,6 +31,20 @@ class Scan extends Module_Product {
*/
public static $module_name = 'scan';
/**
* The category of the product
*
* @var string
*/
public static $category = 'security';
/**
* The feature slug that identifies the paid plan
*
* @var string
*/
public static $feature_identifying_paid_plan = 'scan';
/**
* Get the product name
*
@ -107,47 +119,6 @@ class Scan extends Module_Product {
return 'jetpack_scan';
}
/**
* Hits the wpcom api to check scan status.
*
* @todo Maybe add caching.
*
* @return Object|WP_Error
*/
private static function get_state_from_wpcom() {
static $status = null;
if ( $status !== null ) {
return $status;
}
$site_id = Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d/scan', $site_id ) . '?force=wpcom', '2', array( 'timeout' => 2 ), null, 'wpcom' );
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
$status = new WP_Error( 'scan_state_fetch_failed' );
return $status;
}
$body = wp_remote_retrieve_body( $response );
$status = json_decode( $body );
return $status;
}
/**
* Checks whether the current plan (or purchases) of the site already supports the product
*
* @return boolean
*/
public static function has_required_plan() {
$scan_data = static::get_state_from_wpcom();
if ( is_wp_error( $scan_data ) ) {
return false;
}
return is_object( $scan_data ) && isset( $scan_data->state ) && 'unavailable' !== $scan_data->state;
}
/**
* Checks whether the Product is active
*
@ -156,7 +127,7 @@ class Scan extends Module_Product {
* @return boolean
*/
public static function is_active() {
return static::is_jetpack_plugin_active() && static::has_required_plan();
return static::is_jetpack_plugin_active();
}
/**
@ -188,6 +159,20 @@ class Scan extends Module_Product {
return true;
}
/**
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_scan',
'jetpack_scan_monthly',
'jetpack_scan_bi_yearly',
);
}
/**
* Return product bundles list
* that supports the product.
@ -195,7 +180,7 @@ class Scan extends Module_Product {
* @return boolean|array Products bundle list.
*/
public static function is_upgradable_by_bundle() {
return array( 'security' );
return array( 'security', 'complete' );
}
/**

View File

@ -36,6 +36,8 @@ class Search_Stats {
const CACHE_EXPIRY = 1 * MINUTE_IN_SECONDS;
const CACHE_GROUP = 'jetpack_search';
const POST_TYPE_BREAKDOWN_CACHE_KEY = 'post_type_break_down';
const TOTAL_POSTS_COUNT_CACHE_KEY = 'total-post-count';
const POST_COUNT_QUERY_LIMIT = 1e5;
/**
* Get stats from the WordPress.com API for the current blog ID.
@ -58,6 +60,25 @@ class Search_Stats {
return $response;
}
/**
* Queue querying the post type breakdown from WordPress.com API for the current blog ID.
*/
public function queue_post_count_query_from_wpcom() {
$blog_id = Jetpack_Options::get_option( 'id' );
if ( ! is_numeric( $blog_id ) ) {
return null;
}
Client::wpcom_json_api_request_as_blog(
'/sites/' . (int) $blog_id . '/jetpack-search/queue-post-count',
'2',
array(),
null,
'wpcom'
);
}
/**
* Estimate record counts via a local database query.
*/
@ -127,7 +148,7 @@ class Search_Stats {
}
/**
* Get raw post type breakdown from the database.
* Get raw post type breakdown from the database or a remote request if posts count is high.
*/
protected static function get_raw_post_type_breakdown() {
global $wpdb;
@ -137,6 +158,27 @@ class Search_Stats {
return $results;
}
$total_posts_count = wp_cache_get( self::TOTAL_POSTS_COUNT_CACHE_KEY, self::CACHE_GROUP );
if ( false === $total_posts_count ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery */
$total_posts_counts = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->posts}" );
wp_cache_set( self::TOTAL_POSTS_COUNT_CACHE_KEY, $total_posts_counts, self::CACHE_GROUP, self::CACHE_EXPIRY );
}
// Get post type breakdown from a remote request if the post count is high
if ( $total_posts_count > self::POST_COUNT_QUERY_LIMIT ) {
$search_stats = new Search_Stats();
$search_stats->queue_post_count_query_from_wpcom();
$wpcom_stats = json_decode( wp_remote_retrieve_body( $search_stats->get_stats_from_wpcom() ), true );
if ( ! empty( $wpcom_stats['raw_post_type_breakdown'] ) ) {
$results = $wpcom_stats['raw_post_type_breakdown'];
wp_cache_set( self::POST_TYPE_BREAKDOWN_CACHE_KEY, $results, self::CACHE_GROUP, self::CACHE_EXPIRY );
return $results;
} else {
return array();
}
}
$query = "SELECT post_type, post_status, COUNT( * ) AS num_posts
FROM {$wpdb->posts}
WHERE post_password = ''

View File

@ -13,7 +13,6 @@ use Automattic\Jetpack\Constants;
use Automattic\Jetpack\My_Jetpack\Hybrid_Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use Automattic\Jetpack\Search\Module_Control as Search_Module_Control;
use Jetpack_Options;
use WP_Error;
/**
@ -41,6 +40,13 @@ class Search extends Hybrid_Product {
*/
public static $plugin_slug = 'jetpack-search';
/**
* The category of the product
*
* @var string
*/
public static $category = 'performance';
/**
* Search has a standalone plugin
*
@ -80,6 +86,13 @@ class Search extends Hybrid_Product {
*/
public static $requires_user_connection = true;
/**
* The feature slug that identifies the paid plan
*
* @var string
*/
public static $feature_identifying_paid_plan = 'search';
/**
* Get the product name
*
@ -265,39 +278,6 @@ class Search extends Hybrid_Product {
return $pricings[ $record_count ];
}
/**
* Hits the wpcom api to check Search status.
*
* @todo Maybe add caching.
*
* @return Object|WP_Error
*/
private static function get_state_from_wpcom() {
static $status = null;
if ( $status !== null ) {
return $status;
}
$blog_id = Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_blog(
'/sites/' . $blog_id . '/jetpack-search/plan',
'2',
array( 'timeout' => 5 ),
null,
'wpcom'
);
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
return new WP_Error( 'search_state_fetch_failed' );
}
$body = wp_remote_retrieve_body( $response );
$status = json_decode( $body );
return $status;
}
/**
* Checks whether the product supports trial or not
*
@ -312,26 +292,16 @@ class Search extends Hybrid_Product {
}
/**
* Checks if the site purchases contain a paid search plan
* Get the product-slugs of the paid plans for this product (not including bundles)
*
* @return bool
* @return array
*/
public static function has_paid_plan_for_product() {
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
// Search is available as standalone product and as part of the Complete plan.
if (
( str_contains( $purchase->product_slug, 'jetpack_search' ) && ! str_contains( $purchase->product_slug, 'jetpack_search_free' ) ) ||
str_starts_with( $purchase->product_slug, 'jetpack_complete' ) ) {
return true;
}
}
}
return false;
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_search',
'jetpack_search_monthly',
'jetpack_search_bi_yearly',
);
}
/**
@ -391,4 +361,14 @@ class Search extends Hybrid_Product {
public static function get_manage_url() {
return admin_url( 'admin.php?page=jetpack-search' );
}
/**
* Return product bundles list
* that supports the product.
*
* @return boolean|array Products bundle list.
*/
public static function is_upgradable_by_bundle() {
return array( 'complete' );
}
}

View File

@ -36,7 +36,7 @@ class Security extends Module_Product {
* @return string
*/
public static function get_name() {
return 'Security';
return 'Security Bundle';
}
/**
@ -69,7 +69,7 @@ class Security extends Module_Product {
/**
* Get the internationalized features list
*
* @return array Boost features list
* @return array Security features list
*/
public static function get_features() {
return array(
@ -81,17 +81,18 @@ class Security extends Module_Product {
}
/**
* Get the product princing details
* Get the product pricing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
$product_slug = static::get_wpcom_product_slug();
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => static::get_wpcom_product_slug(),
'wpcom_product_slug' => $product_slug,
),
Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() )
Wpcom_Products::get_product_pricing( $product_slug )
);
}
@ -160,6 +161,22 @@ class Security extends Module_Product {
return static::is_jetpack_plugin_active() && static::has_required_plan();
}
/**
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_security_t1_yearly',
'jetpack_security_t1_monthly',
'jetpack_security_t1_bi_yearly',
'jetpack_security_t2_yearly',
'jetpack_security_t2_monthly',
);
}
/**
* Checks whether the current plan (or purchases) of the site already supports the product
*
@ -195,7 +212,7 @@ class Security extends Module_Product {
/**
* Return all the products it contains.
*
* @return Array Product slugs
* @return array Product slugs
*/
public static function get_supported_products() {
return array( 'backup', 'scan', 'anti-spam' );

View File

@ -0,0 +1,186 @@
<?php
/**
* Feature: Site Accelerator
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Module_Product;
use WP_Error;
/**
* Class responsible for handling the Site Accelerator module.
*/
class Site_Accelerator extends Module_Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'site-accelerator';
/**
* The category of the product
*
* @var string
*/
public static $category = 'performance';
/**
* The slug of the plugin associated with this product.
* Site Accelerator is a feature available as part of the Jetpack plugin.
*
* @var string
*/
public static $plugin_slug = self::JETPACK_PLUGIN_SLUG;
/**
* The Plugin file associated with stats
*
* @var string|null
*/
public static $plugin_filename = self::JETPACK_PLUGIN_FILENAME;
/**
* The Jetpack module name associated with this product
*
* @var string|null
*/
public static $module_name = 'site-accelerator';
/**
* Whether this module is a Jetpack feature
*
* @var boolean
*/
public static $is_feature = true;
/**
* Whether this product requires a user connection
*
* @var boolean
*/
public static $requires_user_connection = false;
/**
* Whether this product has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = false;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* Whether the product requires a plan to run
* The plan could be paid or free
*
* @var bool
*/
public static $requires_plan = false;
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Site Accelerator';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Site Accelerator';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Draw your readers from one post to another', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Draw your readers from one post to another, increasing overall traffic on your site', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized feature list
*
* @return array Site Accelerattor features list
*/
public static function get_features() {
return array();
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array(
'available' => true,
'is_free' => true,
);
}
/**
* Checks whether the Product is active.
*
* @return boolean
*/
public static function is_active() {
return static::is_jetpack_plugin_active();
}
/**
* Checks whether the plugin is installed
*
* @return boolean
*/
public static function is_plugin_installed() {
return static::is_jetpack_plugin_installed();
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return admin_url( 'admin.php?page=jetpack#/settings?term=site%20accelerator' );
}
/**
* Activates the Jetpack plugin
*
* @return null|WP_Error Null on success, WP_Error on invalid file.
*/
public static function activate_plugin(): ?WP_Error {
$plugin_filename = static::get_installed_plugin_filename( self::JETPACK_PLUGIN_SLUG );
if ( $plugin_filename ) {
return activate_plugin( $plugin_filename );
}
}
}

View File

@ -8,6 +8,7 @@
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Hybrid_Product;
use Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use Automattic\Jetpack\Status\Host;
@ -37,6 +38,13 @@ class Social extends Hybrid_Product {
*/
public static $plugin_slug = 'jetpack-social';
/**
* The category of the product
*
* @var string
*/
public static $category = 'growth';
/**
* Social has a standalone plugin
*
@ -62,6 +70,13 @@ class Social extends Hybrid_Product {
*/
public static $has_free_offering = true;
/**
* The feature slug that identifies the paid plan
*
* @var string
*/
public static $feature_identifying_paid_plan = 'social-enhanced-publishing';
/**
* Get the product name
*
@ -144,56 +159,77 @@ class Social extends Hybrid_Product {
return 'jetpack_social_v1_yearly';
}
/**
* Gets the 'status' of the Social product
*
* @return string
*/
public static function get_status() {
$status = parent::get_status();
if ( Products::STATUS_NEEDS_PLAN === $status ) {
// If the status says that the site needs a plan,
// My Jetpack shows "Learn more" CTA,
// We want to instead show the "Activate" CTA.
$status = Products::STATUS_NEEDS_ACTIVATION;
}
return $status;
}
/**
* Get the product-slugs of the paid plans for this product (not including bundles)
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_social_v1_yearly',
'jetpack_social_v1_monthly',
'jetpack_social_v1_bi_yearly',
'jetpack_social_basic_yearly',
'jetpack_social_monthly',
'jetpack_social_basic_monthly',
'jetpack_social_basic_bi_yearly',
'jetpack_social_advanced_yearly',
'jetpack_social_advanced_monthly',
'jetpack_social_advanced_bi_yearly',
);
}
/**
* Checks whether the current plan (or purchases) of the site already supports the product
*
* @return boolean
*/
public static function has_paid_plan_for_product() {
$plans_with_social = array(
'jetpack_social',
'jetpack_complete',
'jetpack_business',
'jetpack_premium',
'jetpack_personal',
);
if ( parent::has_paid_plan_for_product() ) {
return true;
}
// For atomic sites, do a feature check to see if the republicize feature is available
// This feature is available by default on all Jetpack sites
if ( ( new Host() )->is_woa_site() ) {
return static::does_site_have_feature( 'republicize' );
if ( ( new Host() )->is_woa_site() && static::does_site_have_feature( 'republicize' ) ) {
return true;
}
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
foreach ( $plans_with_social as $plan ) {
if ( strpos( $purchase->product_slug, $plan ) !== false ) {
return true;
}
}
}
}
return false;
}
/**
* Get the URL where the user manages the product.
*
* If the standalone plugin is active,
* it will redirect to the standalone plugin settings page.
* Otherwise, it will redirect to the Jetpack settings page.
*
* @return string
*/
public static function get_manage_url() {
if ( static::is_standalone_plugin_active() ) {
return admin_url( 'admin.php?page=jetpack-social' );
}
return admin_url( 'admin.php?page=jetpack-social' );
}
return admin_url( 'admin.php?page=jetpack#/settings?term=publicize' );
/**
* Return product bundles list
* that supports the product.
*
* @return boolean|array Products bundle list.
*/
public static function is_upgradable_by_bundle() {
return array( 'growth', 'complete' );
}
}

View File

@ -174,6 +174,19 @@ class Starter extends Module_Product {
return false;
}
/**
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_starter_yearly',
'jetpack_starter_monthly',
);
}
/**
* Checks whether product is a bundle.
*

View File

@ -32,6 +32,13 @@ class Stats extends Module_Product {
*/
public static $module_name = 'stats';
/**
* The category of the product
*
* @var string
*/
public static $category = 'growth';
/**
* The Plugin slug associated with stats
*
@ -67,6 +74,13 @@ class Stats extends Module_Product {
*/
public static $has_free_offering = true;
/**
* The feature slug that identifies the paid plan
*
* @var string
*/
public static $feature_identifying_paid_plan = 'stats-paid';
/**
* Get the product name
*
@ -194,7 +208,10 @@ class Stats extends Module_Product {
if ( ! is_wp_error( $purchases_data ) && is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
// Jetpack complete includes Stats commercial & cannot be upgraded
if ( str_starts_with( $purchase->product_slug, 'jetpack_complete' ) ) {
if (
str_starts_with( $purchase->product_slug, 'jetpack_complete' ) ||
str_starts_with( $purchase->product_slug, 'jetpack_growth' )
) {
return false;
} elseif (
// Stats commercial purchased with highest tier cannot be upgraded.
@ -217,24 +234,17 @@ class Stats extends Module_Product {
}
/**
* Checks if the site has a paid plan that supports this product
* Get the product-slugs of the paid plans for this product (not including bundles)
*
* @return boolean
* @return array
*/
public static function has_paid_plan_for_product() {
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
// Stats is available as standalone product and as part of the Complete plan.
if ( strpos( $purchase->product_slug, 'jetpack_stats' ) !== false || str_starts_with( $purchase->product_slug, 'jetpack_complete' ) ) {
return true;
}
}
}
return false;
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_stats_yearly',
'jetpack_stats_monthly',
'jetpack_stats_bi_yearly',
'jetpack_stats_pwyw_yearly',
);
}
/**
@ -289,6 +299,10 @@ class Stats extends Module_Product {
* @return ?string
*/
public static function get_purchase_url() {
$status = static::get_status();
if ( $status === Products::STATUS_NEEDS_FIRST_SITE_CONNECTION ) {
return null;
}
// The returning URL could be customized by changing the `redirect_uri` param with relative path.
return sprintf(
'%s#!/stats/purchase/%d?from=jetpack-my-jetpack%s&redirect_uri=%s',
@ -307,4 +321,14 @@ class Stats extends Module_Product {
public static function get_manage_url() {
return admin_url( 'admin.php?page=stats' );
}
/**
* Return product bundles list
* that supports the product.
*
* @return boolean|array Products bundle list.
*/
public static function is_upgradable_by_bundle() {
return array( 'growth', 'complete' );
}
}

View File

@ -9,11 +9,16 @@ namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Hybrid_Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use Automattic\Jetpack\VideoPress\Stats as VideoPress_Stats;
use WP_Error;
use WP_REST_Response;
/**
* Class responsible for handling the VideoPress product
*/
class Videopress extends Hybrid_Product {
private const VIDEOPRESS_STATS_KEY = 'my-jetpack-videopress-stats';
private const VIDEOPRESS_PERIOD_KEY = 'my-jetpack-videopress-period';
/**
* The product slug
@ -36,6 +41,13 @@ class Videopress extends Hybrid_Product {
*/
public static $plugin_slug = 'jetpack-videopress';
/**
* The category of the product
*
* @var string
*/
public static $category = 'performance';
/**
* The filename (id) of the plugin associated with this product.
*
@ -68,6 +80,39 @@ class Videopress extends Hybrid_Product {
*/
public static $has_free_offering = true;
/**
* The feature slug that identifies the paid plan
*
* @var string
*/
public static $feature_identifying_paid_plan = 'videopress';
/**
* Setup VideoPress REST API endpoints
*
* @return void
*/
public static function register_endpoints(): void {
parent::register_endpoints();
// Get Jetpack VideoPress data.
register_rest_route(
'my-jetpack/v1',
'/site/videopress/data',
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_site_videopress_data',
'permission_callback' => __CLASS__ . '::permissions_callback',
)
);
}
/**
* Checks if the user has the correct permissions
*/
public static function permissions_callback() {
return current_user_can( 'edit_posts' );
}
/**
* Get the product name
*
@ -174,30 +219,97 @@ class Videopress extends Hybrid_Product {
}
/**
* Checks whether the site has a paid plan for this product
* Get the product-slugs of the paid plans for this product (not including bundles)
*
* @return boolean
* @return array
*/
public static function has_paid_plan_for_product() {
$plans_with_videopress = array(
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_videopress',
'jetpack_complete',
'jetpack_business',
'jetpack_premium',
'jetpack_videopress_monthly',
'jetpack_videopress_bi_yearly',
);
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
/**
* Return product bundles list
* that supports the product.
*
* @return boolean|array Products bundle list.
*/
public static function is_upgradable_by_bundle() {
return array( 'complete' );
}
/**
* Get stats for VideoPress
*
* @return array|WP_Error
*/
private static function get_videopress_stats() {
$video_count = array_sum( (array) wp_count_attachments( 'video' ) );
if ( ! class_exists( 'Automattic\Jetpack\VideoPress\Stats' ) ) {
return array(
'videoCount' => $video_count,
);
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
foreach ( $plans_with_videopress as $plan ) {
if ( strpos( $purchase->product_slug, $plan ) !== false ) {
return true;
}
}
$featured_stats = get_transient( self::VIDEOPRESS_STATS_KEY );
if ( $featured_stats ) {
return array(
'featuredStats' => $featured_stats,
'videoCount' => $video_count,
);
}
$stats_period = get_transient( self::VIDEOPRESS_PERIOD_KEY );
$videopress_stats = new VideoPress_Stats();
// If the stats period exists, retrieve that information without checking the view count.
// If it does not, check the view count of monthly stats and determine if we want to show yearly or monthly stats.
if ( $stats_period ) {
if ( $stats_period === 'day' ) {
$featured_stats = $videopress_stats->get_featured_stats( 60, 'day' );
} else {
$featured_stats = $videopress_stats->get_featured_stats( 2, 'year' );
}
} else {
$featured_stats = $videopress_stats->get_featured_stats( 60, 'day' );
if (
! is_wp_error( $featured_stats ) &&
$featured_stats &&
( $featured_stats['data']['views']['current'] < 500 || $featured_stats['data']['views']['previous'] < 500 )
) {
$featured_stats = $videopress_stats->get_featured_stats( 2, 'year' );
}
}
return false;
if ( is_wp_error( $featured_stats ) || ! $featured_stats ) {
return array(
'videoCount' => $video_count,
);
}
set_transient( self::VIDEOPRESS_PERIOD_KEY, $featured_stats['period'], WEEK_IN_SECONDS );
set_transient( self::VIDEOPRESS_STATS_KEY, $featured_stats, DAY_IN_SECONDS );
return array(
'featuredStats' => $featured_stats,
'videoCount' => $video_count,
);
}
/**
* Get VideoPress data for the REST API
*
* @return WP_REST_Response|WP_Error
*/
public static function get_site_videopress_data() {
$videopress_stats = self::get_videopress_stats();
return rest_ensure_response( $videopress_stats );
}
}