This commit is contained in:
2026-07-02 15:54:39 -06:00
commit 9883323161
17470 changed files with 4470592 additions and 0 deletions
@@ -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 );
}
}
}
@@ -0,0 +1,828 @@
<?php
/**
* WP Admin page with information and configuration shared among all Jetpack stand-alone plugins
*
* @package automattic/my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Admin_UI\Admin_Menu;
use Automattic\Jetpack\Agents_Manager\WP_REST_Jetpack_AI_JWT;
use Automattic\Jetpack\Assets;
use Automattic\Jetpack\Boost_Speed_Score\Speed_Score;
use Automattic\Jetpack\Boost_Speed_Score\Speed_Score_History;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Connection\Rest_Authentication as Connection_Rest_Authentication;
use Automattic\Jetpack\Constants as Jetpack_Constants;
use Automattic\Jetpack\ExPlat;
use Automattic\Jetpack\JITMS\JITM;
use Automattic\Jetpack\Licensing;
use Automattic\Jetpack\Modules;
use Automattic\Jetpack\Plugins_Installer;
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 Jetpack;
use WP_Error;
/**
* The main Initializer class that registers the admin menu and eneuque the assets.
*/
class Initializer {
/**
* My Jetpack package version
*
* @var string
*/
const PACKAGE_VERSION = '5.40.4';
/**
* HTML container ID for the IDC screen on My Jetpack page.
*/
private const IDC_CONTAINER_ID = 'my-jetpack-identity-crisis-container';
public const JETPACK_PLUGIN_SLUGS = array(
'jetpack-backup',
'jetpack-boost',
'zerobscrm',
'jetpack',
'jetpack-protect',
'jetpack-social',
'jetpack-videopress',
'jetpack-search',
);
private const MY_JETPACK_SITE_INFO_TRANSIENT_KEY = 'my-jetpack-site-info';
/**
* Holds info/data about the site (from the /sites/%d endpoint)
*
* @var object
*/
public static $site_info;
/**
* Initialize My Jetpack
*
* @return void
*/
public static function init() {
if ( ! self::should_initialize() || did_action( 'my_jetpack_init' ) ) {
return;
}
// Extend jetpack plugins action links.
Products::extend_plugins_action_links();
// Set up the REST authentication hooks.
Connection_Rest_Authentication::init();
if ( self::is_licensing_ui_enabled() ) {
Licensing::instance()->initialize();
}
// Initialize Boost Speed Score
new Speed_Score( array(), 'jetpack-my-jetpack' );
// Add custom WP REST API endoints.
add_action( 'rest_api_init', array( __CLASS__, 'register_rest_endpoints' ) );
add_action( 'admin_menu', array( __CLASS__, 'add_my_jetpack_menu_item' ) );
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 );
// Set up the ExPlat package endpoints
ExPlat::init();
// Sets up JITMS.
JITM::configure();
// Add "Jetpack Manage" menu item.
Jetpack_Manage::init();
/**
* Fires after the My Jetpack package is initialized
*
* @since 0.1.0
*/
do_action( 'my_jetpack_init' );
}
/**
* Acts as a feature flag, returning a boolean for whether we should show the licensing UI.
*
* @since 1.2.0
*
* @return boolean
*/
public static function is_licensing_ui_enabled() {
// Default changed to true in 1.5.0.
$is_enabled = true;
/*
* Bail if My Jetpack is not enabled,
* and thus the licensing UI shouldn't be enabled either.
*/
if ( ! self::should_initialize() ) {
$is_enabled = false;
}
/**
* Acts as a feature flag, returning a boolean for whether we should show the licensing UI.
*
* @param bool $is_enabled Defaults to true.
*
* @since 1.2.0
* @since 1.5.0 Update default value to true.
*/
return apply_filters(
'jetpack_my_jetpack_should_enable_add_license_screen',
$is_enabled
);
}
/**
* 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.
*
* @return void
*/
public static function admin_init() {
$connection = new Connection_Manager();
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- No nonce needed for redirect flow control
$step = isset( $_GET['step'] ) ? sanitize_text_field( wp_unslash( $_GET['step'] ) ) : '';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Checking for partner coupon redemption flow
$show_coupon_redemption = isset( $_GET['showCouponRedemption'] );
// Redirect to Jetpack dashboard for partner coupon redemption
if ( $show_coupon_redemption ) {
wp_safe_redirect( admin_url( 'admin.php?page=jetpack&showCouponRedemption=1#/dashboard' ) );
exit( 0 );
}
// Handle onboarding redirects based on connection status
$should_redirect = false;
$redirect_args = array( 'page' => 'my-jetpack' );
if ( ! $connection->is_connected() && $step !== 'onboarding' ) {
// Redirect to onboarding if not connected
$redirect_args['step'] = 'onboarding';
$should_redirect = true;
} elseif ( $connection->is_connected() && $step === 'onboarding' ) {
// Redirect away from onboarding if already connected
$should_redirect = true;
}
if ( $should_redirect ) {
$admin_page = add_query_arg( $redirect_args, admin_url( 'admin.php' ) );
$location = wp_sanitize_redirect( $admin_page );
// Remove wp_get_referer filter applied in `fix_redirect` method of `Jetpack_Admin` class
remove_filter( 'wp_redirect', 'wp_get_referer' );
wp_safe_redirect( $location );
exit( 0 );
}
// If the user reaches the onboarding page, add a class to the body
if ( $step === 'onboarding' ) {
add_filter( 'admin_body_class', array( __CLASS__, 'add_onboarding_admin_body_class' ) );
}
self::$site_info = self::get_site_info();
add_filter( 'identity_crisis_container_id', array( static::class, 'get_idc_container_id' ) );
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );
}
/**
* Add a body class to the My Jetpack onboarding page.
* This class hides the WP Admin toolbar and the sidebar menu.
*
* @param string $classes The body classes.
* @return string The modified body classes.
*/
public static function add_onboarding_admin_body_class( $classes ) {
$classes .= 'jetpack-admin-full-screen';
return $classes;
}
/**
* Returns whether we are in condition to track to use
* Analytics functionality like Tracks, MC, or GA.
*/
public static function can_use_analytics() {
$status = new Status();
$connection = new Connection_Manager();
$tracking = new Tracking( 'jetpack', $connection );
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',
__FILE__,
array(
'enqueue' => true,
'in_footer' => true,
'textdomain' => 'jetpack-my-jetpack',
)
);
$modules = new Modules();
$connection = new Connection_Manager();
$speed_score_history = new Speed_Score_History( get_site_url() );
$latest_score = $speed_score_history->latest();
$previous_score = array();
if ( $speed_score_history->count() > 1 ) {
$previous_score = $speed_score_history->latest( 1 );
}
$latest_score['previousScores'] = $previous_score['scores'] ?? array();
$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(
'my_jetpack_main_app',
'myJetpackInitialState',
array(
'products' => array(
'items' => Products::get_products(),
),
'plugins' => Plugins_Installer::get_plugins(),
'themes' => Sync_Functions::get_themes(),
'myJetpackUrl' => admin_url( 'admin.php?page=my-jetpack' ),
'myJetpackCheckoutUri' => admin_url( 'admin.php?page=my-jetpack' ),
'topJetpackMenuItemUrl' => Admin_Menu::get_top_level_menu_item_url(),
'siteSuffix' => ( new Status() )->get_site_suffix(),
'siteUrl' => esc_url( get_site_url() ),
'blogID' => Connection_Manager::get_site_id( true ),
'myJetpackVersion' => self::PACKAGE_VERSION,
'myJetpackFlags' => self::get_my_jetpack_flags(),
'fileSystemWriteAccess' => self::has_file_system_write_access(),
'loadAddLicenseScreen' => self::is_licensing_ui_enabled(),
'adminUrl' => esc_url( admin_url() ),
'IDCContainerID' => static::get_idc_container_id(),
'userIsAdmin' => current_user_can( 'manage_options' ),
'lifecycleStats' => array(
'jetpackPlugins' => self::get_installed_jetpack_plugins(),
'historicallyActiveModules' => \Jetpack_Options::get_option( 'historically_active_modules', array() ),
'brokenModules' => Red_Bubble_Notifications::check_for_broken_modules(),
'isSiteConnected' => $connection->is_connected(),
'isUserConnected' => $connection->is_user_connected(),
'modules' => self::get_active_modules(),
),
'recommendedModules' => array(
'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' ),
'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(),
'isJetpackPluginActive' => class_exists( 'Jetpack' ),
'latestBoostSpeedScores' => $latest_score,
'seoOptIn' => self::get_seo_opt_in_state(),
)
);
wp_localize_script(
'my_jetpack_main_app',
'myJetpackRest',
array(
'apiRoot' => esc_url_raw( rest_url() ),
'apiNonce' => wp_create_nonce( 'wp_rest' ),
)
);
// Connection Initial State.
Connection_Initial_State::render_script( 'my_jetpack_main_app' );
// Required for Analytics.
if ( self::can_use_analytics() ) {
Tracking::register_tracks_functions_scripts( true );
}
}
/**
* Get installed Jetpack plugins
*
* @return array
*/
public static function get_installed_jetpack_plugins() {
$plugin_slugs = array_keys( Plugins_Installer::get_plugins() );
$plugin_slugs = array_map(
static function ( $slug ) {
$parts = explode( '/', $slug );
// Return the last segment of the filepath without the PHP extension
return str_replace( '.php', '', $parts[ count( $parts ) - 1 ] );
},
$plugin_slugs
);
return array_values( array_intersect( self::JETPACK_PLUGIN_SLUGS, $plugin_slugs ) );
}
/**
* Get active modules (except ones enabled by default)
*
* @return array
*/
public static function get_active_modules() {
$modules = new Modules();
$active_modules = $modules->get_active();
// if the Jetpack plugin is active, filter out the modules that are active by default
if ( class_exists( 'Jetpack' ) && ! empty( $active_modules ) ) {
$active_modules = array_diff( $active_modules, Jetpack::get_default_modules() );
}
return array_values( $active_modules );
}
/**
* Determine if the current user is "new" to Jetpack
* This is used to vary some messaging in My Jetpack
*
* On the front-end, purchases are also taken into account
*
* @return bool
*/
public static function is_jetpack_user_new() {
// is the user connected?
$connection = new Connection_Manager();
if ( $connection->is_user_connected() ) {
return false;
}
// TODO: add a data point for the last known connection/ disconnection time
// are any modules active?
$active_modules = self::get_active_modules();
if ( ! empty( $active_modules ) ) {
return false;
}
// check for other Jetpack plugins that are installed on the site (active or not)
// If there's more than one Jetpack plugin active, this user is not "new"
$plugin_slugs = array_keys( Plugins_Installer::get_plugins() );
$plugin_slugs = array_map(
static function ( $slug ) {
$parts = explode( '/', $slug );
// Return the last segment of the filepath without the PHP extension
return str_replace( '.php', '', $parts[ count( $parts ) - 1 ] );
},
$plugin_slugs
);
$installed_jetpack_plugins = array_intersect( self::JETPACK_PLUGIN_SLUGS, $plugin_slugs );
if ( is_countable( $installed_jetpack_plugins ) && count( $installed_jetpack_plugins ) >= 2 ) {
return false;
}
// Does the site have any purchases?
$purchases = Wpcom_Products::get_site_current_purchases();
if ( ! empty( $purchases ) && ! is_wp_error( $purchases ) ) {
return false;
}
return true;
}
/**
* Build flags for My Jetpack UI
*
* @return array
*/
public static function get_my_jetpack_flags() {
$flags = array(
'videoPressStats' => Jetpack_Constants::is_true( 'JETPACK_MY_JETPACK_VIDEOPRESS_STATS_ENABLED' ),
'showFullJetpackStatsCard' => class_exists( 'Jetpack' ),
);
return $flags;
}
/**
* Build the state the My Jetpack "try the new SEO experience" opt-in card hydrates from.
*
* The card invites an existing self-hosted install to switch over to the new Jetpack SEO
* dashboard (JETPACK-1700). Gating lives server-side, where the signals actually are. The
* card shows only when all of:
*
* - the new SEO product is available — the `rsm_jetpack_seo` feature filter is on (the SEO
* package autoloads regardless, so `class_exists()` alone isn't enough; the filter is the
* real availability switch and the same one the SEO package gates its own surface behind);
* - the site is self-hosted — WordPress.com (Simple + Atomic) decides its own SEO surface, so
* the opt-in card is for self-hosted installs only;
* - the install hasn't opted in yet — `jetpack_seo_surface_visible` is still false. On wpcom
* the SEO package's `is_seo_surface_visible()` short-circuits to `true`, so the
* "not visible yet" check also doubles as the self-hosted guard, but we check the platform
* explicitly for clarity.
*
* Referenced through `class_exists()` rather than a hard composer dependency: both packages
* ship inside the Jetpack plugin and the SEO surface is feature-flagged, so a guarded read of
* its public API keeps this from adding plumbing to consumers that don't load SEO.
*
* The on-success destination is computed by the opt-in endpoint itself; we only seed the card
* with the same admin URL so the button has a sensible fallback before the request resolves.
*
* @return array{showCard: bool, redirect: string}
*/
public static function get_seo_opt_in_state() {
// Guard with method_exists rather than class_exists: an older bundled SEO package can ship
// the Initializer class without this method, and class_exists alone would still fatal.
$seo_initializer = 'Automattic\Jetpack\SEO\Initializer';
// @phan-suppress-next-line PhanUndeclaredClassReference -- optional SEO package, guarded by method_exists.
$show_card = method_exists( $seo_initializer, 'is_optin_available' ) && $seo_initializer::is_optin_available();
return array(
'showCard' => $show_card,
'redirect' => admin_url( 'admin.php?page=jetpack-seo' ),
);
}
/**
* Echoes the admin page content.
*
* @return void
*/
public static function admin_page() {
$step = isset( $_GET['step'] ) ? sanitize_text_field( wp_unslash( $_GET['step'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$is_onboarding = $step === 'onboarding';
// Add data attribute for onboarding, otherwise render normal container
echo '<div id="my-jetpack-container" ' . ( $is_onboarding ? 'data-route="onboarding"' : '' ) . '></div>';
}
/**
* Register the REST API routes.
*
* @return void
*/
public static function register_rest_endpoints() {
new REST_Products();
new REST_Purchases();
new REST_Zendesk_Chat();
( new WP_REST_Jetpack_AI_JWT() )->register_rest_route();
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',
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_site',
'permission_callback' => __CLASS__ . '::permissions_callback',
)
);
register_rest_route(
'my-jetpack/v1',
'site/dismiss-welcome-banner',
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::dismiss_welcome_banner',
'permission_callback' => __CLASS__ . '::permissions_callback',
)
);
}
/**
* Check user capability to access the endpoint.
*
* @access public
* @static
*
* @return true|WP_Error
*/
public static function permissions_callback() {
return current_user_can( 'manage_options' );
}
/**
* Return true if we should initialize the My Jetpack admin page.
*/
public static function should_initialize() {
$should = true;
// All options presented in My Jetpack require a connection to WordPress.com.
if ( ( new Status() )->is_offline_mode() ) {
$should = false;
}
/**
* Allows filtering whether My Jetpack should be initialized.
*
* @since 0.5.0-alpha
*
* @param bool $shoud_initialize Should we initialize My Jetpack?
*/
return apply_filters( 'jetpack_my_jetpack_should_initialize', $should );
}
/**
* 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
*
* @return void
*/
public static function setup_historically_active_jetpack_modules_sync() {
// 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(
'jetpack_site_registered',
'jetpack_user_authorized',
'activated_plugin',
);
foreach ( $actions as $action ) {
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( $ham, 'update_historically_active_jetpack_modules' ), 5 );
}
/**
* Site full-data endpoint.
*
* @return object Site data.
*/
public static function get_site() {
$site_id = \Jetpack_Options::get_option( 'id' );
$wpcom_endpoint = sprintf( '/sites/%d?force=wpcom', $site_id );
$wpcom_api_version = '1.1';
$response = Client::wpcom_json_api_request_as_blog( $wpcom_endpoint, $wpcom_api_version );
$response_code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ) );
if ( is_wp_error( $response ) || empty( $response['body'] ) ) {
return new WP_Error( 'site_data_fetch_failed', 'Site data fetch failed', array( 'status' => $response_code ) );
}
return rest_ensure_response( $body );
}
/**
* Populates the self::$site_info var with site data from the /sites/%d endpoint
*
* @return object|WP_Error
*/
public static function get_site_info() {
static $site_info = null;
if ( $site_info !== null ) {
return $site_info;
}
// Check for a cached value before doing lookup
$stored_site_info = get_transient( self::MY_JETPACK_SITE_INFO_TRANSIENT_KEY );
if ( $stored_site_info !== false ) {
return $stored_site_info;
}
$response = self::get_site();
if ( is_wp_error( $response ) ) {
return $response;
}
$site_info = $response->data;
set_transient( self::MY_JETPACK_SITE_INFO_TRANSIENT_KEY, $site_info, DAY_IN_SECONDS );
return $site_info;
}
/**
* Returns whether a site has been determined "commercial" or not.
*
* @return bool|null
*/
public static function is_commercial_site() {
if ( is_wp_error( self::$site_info ) ) {
return null;
}
return empty( self::$site_info->options->is_commercial ) ? false : self::$site_info->options->is_commercial;
}
/**
* Check if site is registered (has been connected before).
*
* @return bool
*/
public static function is_registered() {
return (bool) \Jetpack_Options::get_option( 'id' );
}
/**
* Dismiss the welcome banner.
*
* @return \WP_REST_Response
*/
public static function dismiss_welcome_banner() {
\Jetpack_Options::update_option( 'dismissed_welcome_banner', true );
return rest_ensure_response( array( 'success' => true ) );
}
/**
* Returns "yes" if the site has file write access to the plugins folder, "no" otherwise.
*
* @return string
**/
public static function has_file_system_write_access() {
$cache = get_transient( 'my_jetpack_write_access' );
if ( false !== $cache ) {
return $cache;
}
if ( ! function_exists( 'get_filesystem_method' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
require_once ABSPATH . 'wp-admin/includes/template.php';
$write_access = 'no';
$filesystem_method = get_filesystem_method( array(), WP_PLUGIN_DIR );
if ( 'direct' === $filesystem_method ) {
$write_access = 'yes';
}
if ( 'no' === $write_access ) {
ob_start();
$filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() );
ob_end_clean();
if ( $filesystem_credentials_are_stored ) {
$write_access = 'yes';
}
}
set_transient( 'my_jetpack_write_access', $write_access, 30 * MINUTE_IN_SECONDS );
return $write_access;
}
/**
* Get container IDC for the IDC screen.
*
* @return string
*/
public static function get_idc_container_id() {
return static::IDC_CONTAINER_ID;
}
/**
* Conditionally append the red bubble notification to the "Jetpack" menu item if there are alerts to show.
*
* On My Jetpack page: Uses blocking behavior to fetch fresh data.
* On other admin pages: Uses cached data only to avoid blocking, with async JS fetch if cache is empty.
*
* @return void
*/
public static function maybe_show_red_bubble() {
global $menu, $pagenow;
// 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;
}
// Don't show any red bubbles when Jetpack is disconnected
// Users can't act on most alerts without a connection
$connection = new Connection_Manager();
if ( ! $connection->is_connected() ) {
return;
}
// Check if we're on the My Jetpack page.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
$is_my_jetpack_page = $pagenow === 'admin.php' && $page === 'my-jetpack';
if ( $is_my_jetpack_page ) {
// On My Jetpack page: use blocking behavior for fresh data.
add_filter( 'my_jetpack_red_bubble_notification_slugs', array( Red_Bubble_Notifications::class, 'add_red_bubble_alerts' ) );
$red_bubble_alerts = Red_Bubble_Notifications::get_red_bubble_alerts();
} else {
// On other pages: use cached data only to avoid blocking.
$cached_alerts = Red_Bubble_Notifications::get_cached_alerts();
if ( false === $cached_alerts ) {
// No cache - fetch asynchronously via JS.
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_red_bubble_script' ) );
return;
}
$red_bubble_alerts = $cached_alerts;
}
// Filter out silent alerts.
$red_bubble_alerts = array_filter(
$red_bubble_alerts,
function ( $alert ) {
return empty( $alert['is_silent'] );
}
);
// The Jetpack menu item should be on index 3
if (
! empty( $red_bubble_alerts ) &&
isset( $menu[3] ) &&
$menu[3][0] === 'Jetpack'
) {
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$menu[3][0] .= sprintf( ' <span class="awaiting-mod">%d</span>', count( $red_bubble_alerts ) );
}
}
/**
* Enqueue the notification bubble script.
* Fetches fresh alert data via REST API without blocking page load.
*
* @return void
*/
public static function enqueue_red_bubble_script() {
Assets::register_script(
'my-jetpack-notification-bubble',
'../build/async-notification-bubble.js',
__FILE__,
array(
'enqueue' => true,
'in_footer' => true,
)
);
}
/**
* Get list of module names sorted by their recommendation score
*
* @return array|null
*/
public static function get_recommended_modules() {
$recommendations_evaluation = \Jetpack_Options::get_option( 'recommendations_evaluation', null );
if ( ! $recommendations_evaluation ) {
return null;
}
arsort( $recommendations_evaluation ); // Sort by scores in descending order
return array_keys( $recommendations_evaluation ); // Get only module names
}
}
@@ -0,0 +1,172 @@
<?php
/**
* Tools to manage things related to "Jetpack Manage"
* - Add Jetpack Manage menu item.
* - Check if user is an agency (used by the Jetpack Manage banner)
*
* @package automattic/my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
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.
*/
class Jetpack_Manage {
/**
* Initialize the class and hooks needed.
*/
public static function init() {
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
*
* @return void|null|string The resulting page's hook_suffix
*/
public static function add_submenu_jetpack() {
// Do not display the menu if the user has < 2 sites.
if ( ! self::could_use_jp_manage( 2 ) ) {
return;
}
$args = array();
$blog_id = Connection_Manager::get_site_id( true );
if ( $blog_id ) {
$args = array( 'site' => $blog_id );
}
return Admin_Menu::add_menu(
__( 'Jetpack Manage', 'jetpack-my-jetpack' ),
_x( 'Jetpack Manage', 'product name shown in menu', 'jetpack-my-jetpack' ) . ' <span aria-hidden="true">↗</span>',
'manage_options',
esc_url( Redirect::get_url( 'cloud-manage-dashboard-wp-menu', $args ) ),
null,
16
);
}
/**
* Check if the user has enough sites to be able to use Jetpack Manage.
*
* @param int $min_sites Minimum number of sites to be able to use Jetpack Manage.
*
* @return bool Return true if the user has enough sites to be able to use Jetpack Manage.
*/
public static function could_use_jp_manage( $min_sites = 2 ) {
// Only proceed if the user is connected to WordPress.com.
if ( ! ( new Connection_Manager() )->is_user_connected() ) {
return false;
}
// Do not display the menu if Jetpack plugin is not installed.
if ( ! class_exists( 'Jetpack' ) ) {
return false;
}
// Do not display the menu on Multisite.
if ( is_multisite() ) {
return false;
}
// Check if the user has the minimum number of sites.
$user_data = ( new Connection_Manager() )->get_connected_user_data( get_current_user_id() );
if ( ! isset( $user_data['site_count'] ) || $user_data['site_count'] < $min_sites ) {
return false;
}
return true;
}
/**
* Check if the user is a partner/agency.
*
* @return bool Return true if the user is a partner/agency, otherwise false.
*/
public static function is_agency_account() {
// Only proceed if the user is connected to WordPress.com.
if ( ! ( new Connection_Manager() )->is_user_connected() ) {
return false;
}
// Get the cached partner data.
$partner = get_transient( 'jetpack_partner_data' );
if ( $partner === false ) {
$wpcom_response = Client::wpcom_json_api_request_as_user( '/jetpack-partners' );
if ( 200 !== wp_remote_retrieve_response_code( $wpcom_response ) || is_wp_error( $wpcom_response ) ) {
return false;
}
$partner_data = json_decode( wp_remote_retrieve_body( $wpcom_response ) );
// The jetpack-partners endpoint will return only one partner data into an array, it uses Jetpack_Partner::find_by_owner.
if ( ! is_array( $partner_data ) || count( $partner_data ) !== 1 || ! is_object( $partner_data[0] ) ) {
return false;
}
$partner = $partner_data[0];
// Cache the partner data for 1 hour.
set_transient( 'jetpack_partner_data', $partner, HOUR_IN_SECONDS );
}
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,
)
);
}
}
@@ -0,0 +1,492 @@
<?php
/**
* Class for manipulating products
*
* @package automattic/my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
/**
* A class for everything related to product handling in My Jetpack
*/
class Products {
/**
* Constants for the status of a product on a site
*
* @var string
*/
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';
public const INTERSTITIALS_OPTION_NAME = 'my_jetpack_products_interstitials_state';
/**
* List of statuses that display the module as disabled
* This is defined as the statuses in which the user willingly has the module disabled whether it be by
* default, uninstalling the plugin, disabling the module, or not renewing their plan.
*
* @var array
*/
public static $disabled_module_statuses = array(
self::STATUS_INACTIVE,
self::STATUS_MODULE_DISABLED,
self::STATUS_PLUGIN_ABSENT,
self::STATUS_PLUGIN_ABSENT_WITH_PLAN,
self::STATUS_NEEDS_ACTIVATION,
self::STATUS_NEEDS_FIRST_SITE_CONNECTION,
);
/**
* List of statuses that display the module as broken
*
* @var array
*/
public static $broken_module_statuses = array(
self::STATUS_SITE_CONNECTION_ERROR,
self::STATUS_USER_CONNECTION_ERROR,
);
/**
* List of statuses that display the module as needing attention with a warning
*
* @var array
*/
public static $warning_module_statuses = array(
self::STATUS_SITE_CONNECTION_ERROR,
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,
);
/**
* List of statuses that display the module as active
*
* @var array
*/
public static $active_module_statuses = array(
self::STATUS_ACTIVE,
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
*
* @var array
*/
public static $all_statuses = array(
self::STATUS_SITE_CONNECTION_ERROR,
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,
self::STATUS_PLUGIN_ABSENT_WITH_PLAN,
self::STATUS_NEEDS_PLAN,
self::STATUS_NEEDS_ACTIVATION,
self::STATUS_NEEDS_FIRST_SITE_CONNECTION,
self::STATUS_NEEDS_ATTENTION__WARNING,
self::STATUS_NEEDS_ATTENTION__ERROR,
);
/**
* Get the list of Products classes
*
* Here's where all the existing Products are registered
*
* @throws \Exception If the result of a filter has invalid classes.
* @return array List of class names
*/
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,
// 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,
'jetpack-forms' => Products\Jetpack_Forms::class,
);
/**
* This filter allows plugin to override the Product class of a given product. The new class must be a child class of the default one declared in My Jetpack
*
* For example, a stand-alone plugin could overwrite its product class to control specific behavior of the product in the My Jetpack page after it is active without having to commit changes to the My Jetpack package:
*
* add_filter( 'my_jetpack_products_classes', function( $classes ) {
* $classes['my_plugin'] = 'My_Plugin'; // a class that extends the original one declared in the My Jetpack package.
* return $classes
* } );
*
* @param array $classes An array where the keys are the product slugs and the values are the class names.
*/
$final_classes = apply_filters( 'my_jetpack_products_classes', $classes );
// Check that the classes are still child of the same original classes.
foreach ( (array) $final_classes as $slug => $final_class ) {
if ( $final_class === $classes[ $slug ] ) {
continue;
}
if ( ! class_exists( $final_class ) || ! is_subclass_of( $final_class, $classes[ $slug ] ) ) {
throw new \Exception( 'You can only overwrite a Product class with a child of the original class.' );
}
}
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( $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;
}
/**
* Get a list of products sorted by whether or not the user owns them
* An owned product is defined as a product that is any of the following
* - Active
* - Has historically been active
* - The user has a plan that includes the product
* - The user has the standalone plugin for the product installed
*
* @param string $type The type of ownership to return ('owned' or 'unowned').
*
* @return array
*/
public static function get_products_by_ownership( $type ) {
$owned_active_products = array();
$owned_warning_products = array();
$owned_inactive_products = array();
$unowned_products = array();
foreach ( self::get_products_classes() as $class ) {
$product_slug = $class::$slug;
$status = $class::get_status();
if ( $class::is_owned() ) {
// This sorts the the products in the order of active -> warning -> inactive.
// This enables the frontend to display them in that order.
// This is not needed for unowned products as those will always have a status of 'inactive'.
if ( in_array( $status, self::$active_module_statuses, true ) ) {
array_push( $owned_active_products, $product_slug );
} elseif ( in_array( $status, self::$warning_module_statuses, true ) ) {
array_push( $owned_warning_products, $product_slug );
} else {
array_push( $owned_inactive_products, $product_slug );
}
continue;
}
array_push( $unowned_products, $product_slug );
}
$data = array(
'owned' => array_values(
array_unique(
array_merge(
$owned_active_products,
$owned_warning_products,
$owned_inactive_products
)
)
),
'unowned' => array_values(
array_unique( $unowned_products )
),
);
return $data[ $type ];
}
/**
* Get all plugin filenames associated with the products.
*
* @return array
*/
public static function get_all_plugin_filenames() {
$filenames = array();
foreach ( self::get_products_classes() as $class ) {
if ( ! isset( $class::$plugin_filename ) ) {
continue;
}
if ( is_array( $class::$plugin_filename ) ) {
$filenames = array_merge( $filenames, $class::$plugin_filename );
} else {
$filenames[] = $class::$plugin_filename;
}
}
return $filenames;
}
/**
* Get one product data by its slug
*
* @param string $product_slug The product slug.
*
* @return ?array
*/
public static function get_product( $product_slug ) {
$classes = self::get_products_classes();
if ( isset( $classes[ $product_slug ] ) ) {
return $classes[ $product_slug ]::get_info();
}
}
/**
* Get one product Class name
*
* @param string $product_slug The product slug.
*
* @return ?string
*/
public static function get_product_class( $product_slug ) {
$classes = self::get_products_classes();
if ( isset( $classes[ $product_slug ] ) ) {
return $classes[ $product_slug ];
}
}
/**
* Return product slugs list.
*
* @return array Product slugs array.
*/
public static function get_products_slugs() {
return array_keys( self::get_products_classes() );
}
/**
* Gets the json schema for the product data
*
* @return array
*/
public static function get_product_data_schema() {
return array(
'title' => 'The requested product data',
'type' => 'object',
'properties' => array(
'product' => array(
'description' => __( 'Product slug', 'jetpack-my-jetpack' ),
'type' => 'string',
'enum' => __CLASS__ . '::get_product_slugs',
'required' => false,
'validate_callback' => __CLASS__ . '::check_product_argument',
),
'action' => array(
'description' => __( 'Production action to execute', 'jetpack-my-jetpack' ),
'type' => 'string',
'enum' => array( 'activate', 'deactivate' ),
'required' => false,
'validate_callback' => __CLASS__ . '::check_product_argument',
),
'slug' => array(
'title' => 'The product slug',
'type' => 'string',
),
'name' => array(
'title' => 'The product name',
'type' => 'string',
),
'description' => array(
'title' => 'The product description',
'type' => 'string',
),
'status' => array(
'title' => 'The product status',
'type' => 'string',
'enum' => self::$all_statuses,
),
'class' => array(
'title' => 'The product class handler',
'type' => 'string',
),
),
);
}
/**
* Extend actions links for plugins
* tied to the Products.
*/
public static function extend_plugins_action_links() {
$products = array(
'backup',
'boost',
'crm',
'videopress',
'social',
'protect',
'crm',
'search',
'jetpack-ai',
);
// Add plugin action links for the core Jetpack plugin.
Product::extend_core_plugin_action_links();
// Add plugin action links to standalone products.
foreach ( $products as $product ) {
$class_name = self::get_product_class( $product );
$class_name::extend_plugin_action_links();
}
}
/**
* Get interstitials state for the products
*
* @return array A key-value array of product slugs and their interstitial states. True means the interstitial was seen by the user for that product.
*/
public static function get_interstitials_state() {
return get_option( self::INTERSTITIALS_OPTION_NAME, array() );
}
/**
* Update interstitials state for the products
*
* @param array $new_state A key-value array of product slugs and their interstitial states.
*
* @return bool True if the option was updated successfully, false otherwise.
*/
public static function update_interstitials_state( $new_state ) {
// Merge the existing interstitials state with the new state.
$interstitials_state = array_merge( self::get_interstitials_state(), $new_state );
return update_option( self::INTERSTITIALS_OPTION_NAME, $interstitials_state );
}
}
@@ -0,0 +1,443 @@
<?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 ) {
if ( ! is_array( $param ) ) {
return array();
}
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;
}
// Skip CRM from installation requirements - e.g. don't enforce installation for Complete plan users
if ( $slug === 'crm' ) {
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 ) {
// Backup is not supported on multisite installations.
if ( is_multisite() ) {
return $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 ) {
// Scan is not supported on multisite installations.
if ( is_multisite() ) {
return $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 ) {
// Check if this product is covered by an active bundle plan
$is_covered_by_active_bundle = false;
if ( ! $product::is_bundle_product() ) {
foreach ( $product_classes as $bundle_product ) {
if ( $bundle_product::is_bundle_product() &&
$bundle_product::has_paid_plan_for_product() &&
! $bundle_product::is_paid_plan_expired() &&
! $bundle_product::is_paid_plan_expiring() &&
method_exists( $bundle_product, 'get_supported_products' ) &&
in_array( $key, $bundle_product::get_supported_products(), true ) ) {
$is_covered_by_active_bundle = true;
break;
}
}
}
// Only show expiration alerts if not covered by an active bundle
if ( ! $is_covered_by_active_bundle ) {
$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 )
);
}
}
/**
* Get cached red bubble alerts without triggering expensive computation.
* Returns the cached transient value or false if not cached.
*
* @return array|false Cached alerts or false if cache is empty.
*/
public static function get_cached_alerts() {
return get_transient( self::MY_JETPACK_RED_BUBBLE_TRANSIENT_KEY );
}
/**
* 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 );
}
}
@@ -0,0 +1,465 @@
<?php
/**
* Sets up the Products REST API endpoints.
*
* @package automattic/my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
/**
* Registers the REST routes for Products.
*
* @phan-constructor-used-for-side-effects
*/
class REST_Products {
/**
* Constructor.
*/
public function __construct() {
register_rest_route(
'my-jetpack/v1',
'site/products',
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_products_api_data',
'permission_callback' => __CLASS__ . '::view_products_permissions_callback',
'args' => array(
'products' => array(
'description' => __( 'Comma-separated 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' ),
)
);
$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_products_argument',
);
register_rest_route(
'my-jetpack/v1',
'site/products/install',
array(
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::install_plugins',
'permission_callback' => __CLASS__ . '::edit_permissions_callback',
'args' => array(
'products' => $products_arg,
),
),
)
);
register_rest_route(
'my-jetpack/v1',
'site/products/activate',
array(
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::activate_products',
'permission_callback' => __CLASS__ . '::edit_permissions_callback',
'args' => array(
'products' => $products_arg,
),
),
)
);
register_rest_route(
'my-jetpack/v1',
'site/products/interstitials',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( self::class, 'get_interstitials_state' ),
'permission_callback' => array( self::class, 'edit_permissions_callback' ),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( self::class, 'update_interstitials_state' ),
'permission_callback' => array( self::class, 'edit_permissions_callback' ),
'args' => array(
'products' => array(
'description' => __( 'Key-value pairs of product slugs and their interstitial states.', 'jetpack-my-jetpack' ),
'type' => 'object',
'required' => true,
'properties' => array_fill_keys(
Products::get_products_slugs(),
array(
'type' => 'boolean',
)
),
'additionalProperties' => false,
'minProperties' => 1,
),
),
),
'schema' => array( self::class, 'get_interstitials_schema' ),
)
);
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',
),
)
);
}
/**
* Get the schema for the products endpoint
*
* @return array
*/
public function get_products_schema() {
return array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'products',
'type' => 'object',
'properties' => Products::get_product_data_schema(),
);
}
/**
* Get the schema for the interstitials endpoint
*
* @return array
*/
public static function get_interstitials_schema() {
return array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'Products interstitials',
'type' => 'object',
'properties' => array(
'products' => array(
'type' => 'object',
'description' => __( 'Key-value pairs of product slugs and their interstitial states.', 'jetpack-my-jetpack' ),
'properties' => array_fill_keys(
Products::get_products_slugs(),
array(
'description' => __( 'Interstitial state for the product. True means that the user has seen the interstitial for the product.', 'jetpack-my-jetpack' ),
'type' => 'boolean',
)
),
),
),
);
}
/**
* Check user capability to access the endpoint.
*
* @access public
* @static
*
* @return true|WP_Error
*/
public static function permissions_callback() {
return current_user_can( 'manage_options' );
}
/**
* 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
*
* @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_string( $value ) {
if ( ! is_string( $value ) ) {
return new WP_Error(
'rest_invalid_param',
esc_html__( 'The product argument must be a string.', 'jetpack-my-jetpack' ),
array( 'status' => 400 )
);
}
$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.
*
* @param \WP_REST_Request $request The request object.
* @return WP_Error|\WP_REST_Response
*/
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 API product data endpoint
*
* @param \WP_REST_Request $request The request object.
*
* @return WP_Error|\WP_REST_Response
*/
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 );
}
/**
* Check permission to edit product
*
* @return bool
*/
public static function edit_permissions_callback() {
if ( ! current_user_can( 'activate_plugins' ) ) {
return false;
}
if ( is_multisite() && ! current_user_can( 'manage_network' ) ) {
return false;
}
return true;
}
/**
* Callback for activating products
*
* @param \WP_REST_Request $request The request object.
* @return \WP_REST_Response|\WP_Error
*/
public static function activate_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 )
);
}
$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 products
*
* @param \WP_REST_Request $request The request object.
* @return \WP_REST_Response|\WP_Error
*/
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;
}
}
return rest_ensure_response( Products::get_products( $products_array ) );
}
/**
* Callback for installing (and activating) multiple product plugins.
*
* @param \WP_REST_Request $request The request object.
* @return \WP_REST_Response|\WP_Error
*/
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;
}
}
return rest_ensure_response( Products::get_products( $products_array ) );
}
/**
* Get interstitials state for the products
*
* @return WP_REST_Response
*/
public static function get_interstitials_state() {
return rest_ensure_response(
array(
'products' => Products::get_interstitials_state(),
)
);
}
/**
* Update interstitials state for the products
*
* @param WP_REST_Request $request The request object.
* @return WP_REST_Response|WP_Error
*/
public static function update_interstitials_state( WP_REST_Request $request ) {
$success = Products::update_interstitials_state( $request->get_param( 'products' ) );
if ( ! $success ) {
return new WP_Error(
'my_jetpack_interstitials_update_error',
__( 'Failed to update interstitials state.', 'jetpack-my-jetpack' ),
array( 'status' => 500 )
);
}
return rest_ensure_response(
array(
'products' => Products::get_interstitials_state(),
)
);
}
}
@@ -0,0 +1,79 @@
<?php
/**
* Sets up the Purchases REST API endpoints.
*
* @package automattic/my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use WP_Error;
/**
* Registers the REST routes for Purchases.
*
* @phan-constructor-used-for-side-effects
*/
class REST_Purchases {
/**
* Constructor.
*/
public function __construct() {
register_rest_route(
'my-jetpack/v1',
'/site/purchases',
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_site_current_purchases',
'permission_callback' => __CLASS__ . '::permissions_callback',
)
);
}
/**
* Check user capability to access the endpoint.
*
* @access public
* @static
*
* @return true|WP_Error
*/
public static function permissions_callback() {
$connection = new Connection_Manager();
$is_site_connected = $connection->is_connected();
if ( ! $is_site_connected ) {
return new WP_Error(
'not_connected',
__( 'Your site is not connected to Jetpack.', 'jetpack-my-jetpack' ),
array(
'status' => 400,
)
);
}
return current_user_can( 'edit_posts' );
}
/**
* Site purchases endpoint.
*
* @return array|WP_Error of site purchases.
*/
public static function get_site_current_purchases() {
$site_id = \Jetpack_Options::get_option( 'id' );
$wpcom_endpoint = sprintf( '/upgrades?site=%1$d&locale=%2$s', $site_id, get_user_locale() );
$wpcom_api_version = '1.2';
$response = Client::wpcom_json_api_request_as_blog( $wpcom_endpoint, $wpcom_api_version );
$response_code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ) );
if ( is_wp_error( $response ) || empty( $response['body'] ) || 200 !== $response_code ) {
return new WP_Error( 'site_data_fetch_failed', 'Site data fetch failed', array( 'status' => $response_code ? $response_code : 400 ) );
}
return rest_ensure_response( $body );
}
}
@@ -0,0 +1,152 @@
<?php
/**
* Sets up the Evaluation Recommendations REST API endpoints.
*
* @package automattic/my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use WP_Error;
/**
* Registers the REST routes for Evaluation Recommendations.
*
* @phan-constructor-used-for-side-effects
*/
class REST_Recommendations_Evaluation {
/**
* Constructor.
*/
public function __construct() {
register_rest_route(
'my-jetpack/v1',
'/site/recommendations/evaluation/',
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::evaluate_site_recommendations',
'permission_callback' => __CLASS__ . '::permissions_callback',
),
)
);
register_rest_route(
'my-jetpack/v1',
'/site/recommendations/evaluation/result/',
array(
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::save_evaluation_recommendations',
'permission_callback' => __CLASS__ . '::permissions_callback',
),
)
);
register_rest_route(
'my-jetpack/v1',
'/site/recommendations/evaluation/result/',
array(
array(
'methods' => \WP_REST_Server::DELETABLE,
'callback' => __CLASS__ . '::dismiss_evaluation_recommendations',
'permission_callback' => __CLASS__ . '::permissions_callback',
),
)
);
}
/**
* Check user capability to access the endpoint.
*
* @access public
* @static
*
* @return true|WP_Error
*/
public static function permissions_callback() {
$connection = new Connection_Manager();
$is_site_connected = $connection->is_connected();
if ( ! $is_site_connected ) {
return new WP_Error(
'not_connected',
__( 'Your site is not connected to Jetpack.', 'jetpack-my-jetpack' ),
array(
'status' => 400,
)
);
}
return true; // We require site to be connected.
}
/**
* Recommendations Evaluation endpoint.
*
* @param \WP_REST_Request $request Query request.
*
* @return \WP_REST_Response|WP_Error of 3 product slugs (recommendations).
*/
public static function evaluate_site_recommendations( $request ) {
$goals = $request->get_param( 'goals' );
if ( ! isset( $goals ) ) {
return new WP_Error( 'missing_goals', 'Goals are required', array( 'status' => 400 ) );
}
$site_id = \Jetpack_Options::get_option( 'id' );
$wpcom_endpoint = sprintf( '/sites/%1$d/jetpack-recommendations/evaluation?goals=%2$s', $site_id, implode( ',', $goals ) );
$response = Client::wpcom_json_api_request_as_blog( $wpcom_endpoint, '2', array(), null, 'wpcom' );
$response_code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ) );
if ( is_wp_error( $response ) || empty( $body ) || 200 !== $response_code ) {
return new WP_Error( 'recommendations_evaluation_fetch_failed', 'Evaluation processing failed', array( 'status' => $response_code ? $response_code : 400 ) );
}
return rest_ensure_response( $body );
}
/**
* Endpoint to save recommendations results.
*
* @param \WP_REST_Request $request Query request.
*
* @return \WP_REST_Response|WP_Error success response.
*/
public static function save_evaluation_recommendations( $request ) {
$json = $request->get_json_params();
if ( ! isset( $json['recommendations'] ) ) {
return new WP_Error( 'missing_recommendations', 'Recommendations are required', array( 'status' => 400 ) );
}
\Jetpack_Options::update_option( 'recommendations_evaluation', $json['recommendations'] );
\Jetpack_Options::delete_option( 'dismissed_recommendations' );
return rest_ensure_response( Initializer::get_recommended_modules() );
}
/**
* Endpoint to dismiss the recommendation section
*
* @param \WP_REST_Request $request Query request.
*
* @return \WP_REST_Response|WP_Error success response.
*/
public static function dismiss_evaluation_recommendations( $request ) {
$show_welcome_banner = $request->get_param( 'showWelcomeBanner' );
\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' );
}
return rest_ensure_response( array() );
}
}
@@ -0,0 +1,124 @@
<?php
/**
* Sets up the Zendesk Chat REST API endpoints.
*
* @package automattic/my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Connection\Client;
use WP_Error;
use WP_REST_Response;
/**
* Registers the REST routes for Zendesk Chat.
*
* @phan-constructor-used-for-side-effects
*/
class REST_Zendesk_Chat {
const TRANSIENT_EXPIRY = 1 * MINUTE_IN_SECONDS * 60 * 24 * 7; // 1 week (JWT is actually 2 weeks, but lets be on the safe side)
const ZENDESK_AUTH_TOKEN = 'zendesk_auth_token';
/**
* Constructor.
*/
public function __construct() {
register_rest_route(
'my-jetpack/v1',
'chat/availability',
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_chat_availability',
'permission_callback' => __CLASS__ . '::chat_authentication_permissions_callback',
)
);
register_rest_route(
'my-jetpack/v1',
'chat/authentication',
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_chat_authentication',
'args' => array(
'type' => array(
'required' => false,
'type' => 'string',
),
'test_mode' => array(
'required' => false,
'type' => 'boolean',
),
),
'permission_callback' => __CLASS__ . '::chat_authentication_permissions_callback',
)
);
}
/**
* Ensure user is logged in if making an authentication request
*
* @access public
* @static
*
* @return WP_Error|true
*/
public static function chat_authentication_permissions_callback() {
if ( ! get_current_user_id() ) {
return new WP_Error( 'unauthorized', 'You must be logged in to access this resource.', array( 'status' => 401 ) );
}
return true;
}
/**
* Gets the chat authentication token.
*
* @return WP_Error|WP_REST_Response { token: string }
*/
public static function get_chat_authentication() {
$authentication = get_transient( self::ZENDESK_AUTH_TOKEN );
if ( $authentication ) {
return rest_ensure_response( $authentication );
}
$proxied = function_exists( 'wpcom_is_proxied_request' ) ? wpcom_is_proxied_request() : false;
$wpcom_endpoint = 'help/authenticate/chat';
$wpcom_api_version = '2';
$body = array(
'type' => 'zendesk',
'test_mode' => $proxied ? true : false,
);
$response = Client::wpcom_json_api_request_as_user( $wpcom_endpoint, $wpcom_api_version, array( 'method' => 'POST' ), $body );
$response_code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ) );
if ( is_wp_error( $response ) || empty( $response['body'] ) ) {
return new WP_Error( 'chat_authentication_failed', 'Chat authentication failed', array( 'status' => $response_code ) );
}
set_transient( self::ZENDESK_AUTH_TOKEN, $body, self::TRANSIENT_EXPIRY );
return rest_ensure_response( $body );
}
/**
* Calls `wpcom/v2/presales/chat?group=jp_presales` endpoint.
* This endpoint returns whether or not the Jetpack presales chat group is available
*
* @return WP_Error|WP_REST_Response { is_available: bool }
*/
public static function get_chat_availability() {
$wpcom_endpoint = '/presales/chat?group=jp_presales';
$wpcom_api_version = '2';
$response = Client::wpcom_json_api_request_as_user( $wpcom_endpoint, $wpcom_api_version );
$response_code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ) );
if ( is_wp_error( $response ) || empty( $response['body'] ) ) {
return new WP_Error( 'chat_config_data_fetch_failed', 'Chat config data fetch failed', array( 'status' => $response_code ) );
}
return rest_ensure_response( $body );
}
}
@@ -0,0 +1,405 @@
<?php
/**
* Fetches and store the list of Jetpack products available in WPCOM
*
* @package automattic/my-jetpack
*/
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;
/**
* Stores the list of products available for purchase in WPCOM
*/
class Wpcom_Products {
/**
* The meta name used to store the cache date
*
* @var string
*/
const CACHE_DATE_META_NAME = 'my-jetpack-cache-date';
/**
* The meta name used to store the cache
*
* @var string
*/
const CACHE_META_NAME = 'my-jetpack-cache';
const CACHE_CHECK_HASH_NAME = 'my-jetpack-wpcom-product-check-hash';
const MY_JETPACK_PURCHASES_TRANSIENT_KEY = 'my-jetpack-purchases';
/**
* Store the data on failed WPCOM requests.
*
* @var array
*/
private static $wpcom_request_failures = array();
/**
* Fetches the list of products from WPCOM
*
* @return Object|WP_Error
*/
private static function get_products_from_wpcom() {
$connection = new Connection_Manager();
$blog_id = \Jetpack_Options::get_option( 'id' );
$ip = ( new Visitor() )->get_ip( true );
$headers = array(
'X-Forwarded-For' => $ip,
);
if ( $blog_id ) {
$request_label = 'get_products_from_wpcom_blog_' . $blog_id;
$request_failure = static::get_request_failure( $request_label );
if ( null !== $request_failure ) {
return $request_failure;
}
// If has a blog id, use connected endpoint.
$endpoint = sprintf( '/sites/%d/products/?_locale=%s&type=jetpack', $blog_id, get_user_locale() );
// If available in the user data, set the user's currency as one of the params
if ( $connection->is_user_connected() ) {
$user_details = $connection->get_connected_user_data();
if ( ! empty( $user_details['user_currency'] ) && $user_details['user_currency'] !== 'USD' ) {
$endpoint .= sprintf( '&currency=%s', $user_details['user_currency'] );
}
}
$wpcom_request = Client::wpcom_json_api_request_as_blog(
$endpoint,
'1.1',
array(
'method' => 'GET',
'headers' => $headers,
)
);
} else {
$request_label = 'get_products_from_wpcom';
$request_failure = static::get_request_failure( $request_label );
if ( null !== $request_failure ) {
return $request_failure;
}
$endpoint = 'https://public-api.wordpress.com/rest/v1.1/products?locale=' . get_user_locale() . '&type=jetpack';
$wpcom_request = wp_remote_get(
esc_url_raw( $endpoint ),
array(
'headers' => $headers,
)
);
}
$response_code = wp_remote_retrieve_response_code( $wpcom_request );
if ( 200 === $response_code ) {
return json_decode( wp_remote_retrieve_body( $wpcom_request ) );
} else {
$error = new WP_Error(
'failed_to_fetch_wpcom_products',
esc_html__( 'Unable to fetch the products list from WordPress.com', 'jetpack-my-jetpack' ),
array( 'status' => $response_code )
);
static::set_request_failure( $request_label, $error );
return $error;
}
}
/**
* Super unintelligent hash string that can help us reset the cache after connection changes
* This is important because the currency can change after a user connects depending on what is set in their profile
*
* @return string
*/
private static function build_check_hash() {
static $has_user_data_fetch_error = false;
$hash_string = 'check_hash_';
$connection = new Connection_Manager();
if ( $connection->is_connected() ) {
$hash_string .= 'site_connected_';
}
if ( $connection->is_user_connected() ) {
$hash_string .= 'user_connected';
// Add the user's currency
$user_details = $has_user_data_fetch_error ? false : $connection->get_connected_user_data();
if ( $user_details === false ) {
$has_user_data_fetch_error = true;
} elseif ( ! empty( $user_details['user_currency'] ) ) {
$hash_string .= '_' . $user_details['user_currency'];
}
}
return md5( $hash_string );
}
/**
* Update the cache with new information retrieved from WPCOM
*
* We store one cache for each user, as the information is internationalized based on user preferences
* Also, the currency is based on the user IP address
*
* @param Object $products_list The products list as received from WPCOM.
* @return bool
*/
private static function update_cache( $products_list ) {
update_user_meta( get_current_user_id(), self::CACHE_DATE_META_NAME, time() );
update_user_meta( get_current_user_id(), self::CACHE_CHECK_HASH_NAME, self::build_check_hash() );
return update_user_meta( get_current_user_id(), self::CACHE_META_NAME, $products_list );
}
/**
* Checks if the cache is old, meaning we need to fetch new data from WPCOM
*/
private static function is_cache_old() {
if ( empty( self::get_products_from_cache() ) ) {
return true;
}
// This allows the cache to reset after the site or user connects/ disconnects
$check_hash = get_user_meta( get_current_user_id(), self::CACHE_CHECK_HASH_NAME, true );
if ( $check_hash !== self::build_check_hash() ) {
return true;
}
$cache_date = get_user_meta( get_current_user_id(), self::CACHE_DATE_META_NAME, true );
return time() - (int) $cache_date > DAY_IN_SECONDS;
}
/**
* Gets the product list from the user cache
*/
private static function get_products_from_cache() {
return get_user_meta( get_current_user_id(), self::CACHE_META_NAME, true );
}
/**
* Gets the product list
*
* Attempts to retrieve the products list from the user cache if cache is not too old.
* If cache is old, it will attempt to fetch information from WPCOM. If it fails, we return what we have in cache, if anything, otherwise we return an error.
*
* @param bool $skip_cache If true it will ignore the cache and attempt to fetch fresh information from WPCOM.
*
* @return Object|WP_Error
*/
public static function get_products( $skip_cache = false ) {
// This is only available for logged in users.
if ( ! get_current_user_id() ) {
return null;
}
if ( ! self::is_cache_old() && ! $skip_cache ) {
return self::get_products_from_cache();
}
$products = self::get_products_from_wpcom();
if ( is_wp_error( $products ) ) {
// Let's see if we have it cached.
$cached = self::get_products_from_cache();
if ( ! empty( $cached ) ) {
return $cached;
} else {
return $products;
}
}
self::update_cache( $products );
return $products;
}
/**
* Get one product
*
* @param string $product_slug The product slug.
* @param bool $renew_cache A flag to force the cache to be renewed.
*
* @return ?Object The product details if found
*/
public static function get_product( $product_slug, $renew_cache = false ) {
$products = self::get_products( $renew_cache );
if ( ! empty( $products->$product_slug ) ) {
return $products->$product_slug;
}
}
/**
* Get only the product currency code and price in an array
*
* @param string $product_slug The product slug.
*
* @return array An array with currency_code and full_price. Empty array if product not found.
*/
public static function get_product_pricing( $product_slug ) {
$product = self::get_product( $product_slug );
if ( empty( $product ) ) {
return array();
}
$cost = $product->cost;
$discount_price = $cost;
$is_introductory_offer = false;
$introductory_offer = null;
// Get/compute the discounted price.
if ( isset( $product->introductory_offer->cost_per_interval ) ) {
$discount_price = $product->introductory_offer->cost_per_interval;
$is_introductory_offer = true;
$introductory_offer = $product->introductory_offer;
}
$pricing = array(
'currency_code' => $product->currency_code,
'full_price' => $cost,
'discount_price' => $discount_price,
'is_introductory_offer' => $is_introductory_offer,
'introductory_offer' => $introductory_offer,
'product_term' => $product->product_term,
);
return self::populate_with_discount( $product, $pricing, $discount_price );
}
/**
* Populate the pricing array with the discount information.
*
* @param object $product - The product object.
* @param array $pricing - The pricing array.
* @param float $price - The price to be discounted.
* @return array The pricing array with the discount information.
*/
public static function populate_with_discount( $product, $pricing, $price ) {
// Check whether the product has a coupon.
if ( ! isset( $product->sale_coupon ) ) {
return $pricing;
}
// Check whether it is still valid.
$coupon = $product->sale_coupon;
$coupon_start_date = strtotime( $coupon->start_date );
$coupon_expires = strtotime( $coupon->expires );
if ( $coupon_start_date > time() || $coupon_expires < time() ) {
return $pricing;
}
$coupon_discount = intval( $coupon->discount );
// Populate response with coupon discount.
$pricing['coupon_discount'] = $coupon_discount;
// Apply coupon discount to the price.
$pricing['discount_price'] = $price * ( 100 - $coupon_discount ) / 100;
return $pricing;
}
/**
* Gets the site purchases from WPCOM.
*
* @return Object|WP_Error
*/
public static function get_site_current_purchases() {
static $purchases = null;
if ( $purchases !== null ) {
return $purchases;
}
// Check for a cached value before doing lookup
$stored_purchases = get_transient( self::MY_JETPACK_PURCHASES_TRANSIENT_KEY );
if ( $stored_purchases !== false ) {
return $stored_purchases;
}
$request_failure = static::get_request_failure( 'get_site_current_purchases' );
if ( null !== $request_failure ) {
return $request_failure;
}
$site_id = Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_blog(
sprintf( '/upgrades?site=%d', $site_id ),
'1.2',
array(
'method' => 'GET',
)
);
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
$error = new WP_Error( 'purchases_state_fetch_failed' );
static::set_request_failure( 'get_site_current_purchases', $error );
return $error;
}
$body = wp_remote_retrieve_body( $response );
$purchases = json_decode( $body );
// Set short transient to help with repeated lookups on the same page load
set_transient( self::MY_JETPACK_PURCHASES_TRANSIENT_KEY, $purchases, 5 );
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.
*
* @return void
*/
public static function reset_request_failures() {
static::$wpcom_request_failures = array();
}
/**
* Record the request failure to prevent repeated requests.
*
* @param string $request_label The request label.
* @param WP_Error $error The error.
*
* @return void
*/
private static function set_request_failure( $request_label, WP_Error $error ) {
static::$wpcom_request_failures[ $request_label ] = $error;
}
/**
* Get the pre-saved request failure if exists.
*
* @param string $request_label The request label.
*
* @return null|WP_Error
*/
private static function get_request_failure( $request_label ) {
if ( array_key_exists( $request_label, static::$wpcom_request_failures ) ) {
return static::$wpcom_request_failures[ $request_label ];
}
return null;
}
}
@@ -0,0 +1,198 @@
<?php
/**
* Anti_Spam product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Anti_Spam product
*/
class Anti_Spam extends Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'anti-spam';
/**
* The filename (id) of the plugin associated with this product. If not defined, it will default to the Jetpack plugin
*
* @var string
*/
public static $plugin_filename = 'akismet/akismet.php';
/**
* The slug of the plugin associated with this product. If not defined, it will default to the Jetpack plugin
*
* @var string
*/
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
*
* @var string
*/
public static $requires_user_connection = false;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* Akismet has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Akismet Anti-spam';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Akismet Anti-spam';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Automatically clear spam from comments and forms.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Save time and get better responses by automatically blocking spam from your comments and forms.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Boost features list
*/
public static function get_features() {
return array(
_x( 'Comment and form spam protection', 'Anti-Spam Product Feature', 'jetpack-my-jetpack' ),
_x( 'Block spam without CAPTCHAs', 'Anti-Spam Product Feature', 'jetpack-my-jetpack' ),
_x( 'Advanced stats', 'Anti-Spam Product Feature', 'jetpack-my-jetpack' ),
);
}
/**
* 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_anti_spam',
'jetpack_anti_spam_monthly',
'jetpack_anti_spam_bi_yearly',
);
}
/**
* 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' ) );
if ( ! empty( $akismet_api_key ) ) {
return true;
}
return false;
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => static::get_wpcom_product_slug(),
),
Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() )
);
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_anti_spam';
}
/**
* Return product bundles list
* that supports the product.
*
* @return boolean|array Products bundle list.
*/
public static function is_upgradable_by_bundle() {
return array( 'security', 'complete' );
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return admin_url( 'admin.php?page=akismet-key-config' );
}
}
@@ -0,0 +1,489 @@
<?php
/**
* Backup product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\My_Jetpack\Hybrid_Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use Automattic\Jetpack\Redirect;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* 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
*
* @var string
*/
public static $slug = 'backup';
/**
* The filename (id) of the plugin associated with this product.
*
* @var string
*/
public static $plugin_filename = array(
'jetpack-backup/jetpack-backup.php',
'backup/jetpack-backup.php',
'jetpack-backup-dev/jetpack-backup.php',
);
/**
* The slug of the plugin associated with this product.
*
* @var string
*/
public static $plugin_slug = 'jetpack-backup';
/**
* The category of the product
*
* @var string
*/
public static $category = 'security';
/**
* Backup has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = false;
/**
* Whether this product requires a plan to work at all
*
* @var bool
*/
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
*
* @return string
*/
public static function get_name() {
return 'VaultPress Backup';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack VaultPress Backup';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Real-time backups save every change, and one-click restores get you back online quickly.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Never lose a word, image, page, or time worrying about your site with automated backups & one-click restores.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Backup features list
*/
public static function get_features() {
return array(
_x( 'Real-time cloud backups', 'Backup Product Feature', 'jetpack-my-jetpack' ),
_x( '10GB of backup storage', 'Backup Product Feature', 'jetpack-my-jetpack' ),
_x( '30-day archive & activity log*', 'Backup Product Feature', 'jetpack-my-jetpack' ),
_x( 'One-click restores', 'Backup Product Feature', 'jetpack-my-jetpack' ),
);
}
/**
* Get disclaimers corresponding to a feature
*
* @return array Backup disclaimers list
*/
public static function get_disclaimers() {
return array(
array(
'text' => _x( '* Subject to your usage and storage limit.', 'Backup Product Disclaimer', 'jetpack-my-jetpack' ),
'link_text' => _x( 'Learn more', 'Backup Product Disclaimer', 'jetpack-my-jetpack' ),
'url' => Redirect::get_url( 'jetpack-faq-backup-disclaimer' ),
),
);
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_backup_t1_yearly';
}
/**
* Get the URL where the user should be redirected after checkout
*/
public static function get_post_checkout_url() {
return self::get_manage_url();
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => static::get_wpcom_product_slug(),
),
Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() )
);
}
/**
* 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
*/
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/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' );
return $status;
}
$body = wp_remote_retrieve_body( $response );
$status = json_decode( $body );
return $status;
}
/**
* Hits the wpcom api to retrieve the last 10 backup records.
*
* @return object|WP_Error
*/
public static function get_latest_backups() {
static $backups = null;
if ( $backups !== null ) {
return $backups;
}
$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 ) ) {
// Special case: 'unavailable' with 'site_new' reason is a normal provisioning state for brand new sites.
$is_new_site_provisioning = ( 'unavailable' === $rewind_state->state &&
'site_new' === ( $rewind_state->reason ?? '' ) );
if (
! in_array( $rewind_state->state, array( 'active', 'provisioning', 'awaiting_credentials' ), true ) &&
! $is_new_site_provisioning
) {
$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 ) ) {
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;
}
/**
* Return product bundles list
* that supports the product.
*
* @return boolean|array Products bundle list.
*/
public static function is_upgradable_by_bundle() {
return array( 'security', 'complete' );
}
/**
* Get the URL the user is taken after activating the product
*
* @return ?string
*/
public static function get_post_activation_url() {
return ''; // stay in My Jetpack page or continue the purchase flow if needed.
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
// check standalone first
if ( static::is_standalone_plugin_active() ) {
return admin_url( 'admin.php?page=jetpack-backup' );
// otherwise, check for the main Jetpack plugin
} elseif ( static::is_jetpack_plugin_active() ) {
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',
);
}
/**
* Override the product status to return INACTIVE when backups are deactivated.
*
* @return string
*/
public static function get_status() {
// Get the default status from parent.
$status = parent::get_status();
// Check if backups are deactivated (not an error, just manually turned off).
$needs_attention = static::does_module_need_attention();
if (
is_array( $needs_attention ) &&
isset( $needs_attention['data']['status'] ) &&
'backups-deactivated' === $needs_attention['data']['status']
) {
// Preserve NEEDS_PLAN status - user must purchase before reactivating.
if ( \Automattic\Jetpack\My_Jetpack\Products::STATUS_NEEDS_PLAN === $status ) {
return $status;
}
return \Automattic\Jetpack\My_Jetpack\Products::STATUS_INACTIVE;
}
return $status;
}
}
@@ -0,0 +1,409 @@
<?php
/**
* Boost product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Boost product
*/
class Boost extends Product {
const FREE_TIER_SLUG = 'free';
const UPGRADED_TIER_SLUG = 'upgraded';
const UPGRADED_TIER_PRODUCT_SLUG = 'jetpack_boost_yearly';
/**
* The product slug
*
* @var string
*/
public static $slug = 'boost';
/**
* The filename (id) of the plugin associated with this product.
*
* @var string
*/
public static $plugin_filename = array(
'jetpack-boost/jetpack-boost.php',
'boost/jetpack-boost.php',
'jetpack-boost-dev/jetpack-boost.php',
);
/**
* The slug of the plugin associated with this product.
*
* @var string
*/
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
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* Whether this product requires a user connection
*
* @var string
*/
public static $requires_user_connection = false;
/**
* Whether this product has a free offering
*
* @var bool
*/
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
*
* @return string
*/
public static function get_name() {
return 'Boost';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Boost';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Improves your site speed and performance.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Fast sites get more page visits, more conversions, and better SEO rankings. Boost speeds up your site in seconds.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Boost features list
*/
public static function get_features() {
return array(
__( 'Check your site performance', 'jetpack-my-jetpack' ),
__( 'Enable improvements in one click', 'jetpack-my-jetpack' ),
__( 'Standalone free plugin for those focused on speed', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product's available tiers
*
* @return string[] Slugs of the available tiers
*/
public static function get_tiers() {
return array(
self::UPGRADED_TIER_SLUG,
self::FREE_TIER_SLUG,
);
}
/**
* Get the internationalized comparison of free vs upgraded features
*
* @return array[] Protect features comparison
*/
public static function get_features_by_tier() {
return array(
array(
'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.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array(
'included' => false,
'description' => __( 'Manual', 'jetpack-my-jetpack' ),
'info' => array(
'title' => __( 'Manual Critical CSS regeneration', 'jetpack-my-jetpack' ),
'content' => __(
'<p>To enhance the speed of your site, with this plan you will need to optimize CSS by using the Manual Critical CSS generation feature whenever you:</p>
<ul>
<li>Make theme changes.</li>
<li>Write a new post/page.</li>
<li>Edit a post/page.</li>
<li>Activate, deactivate, or update plugins that impact your site layout or HTML structure.</li>
<li>Change settings of plugins that impact your site layout or HTML structure.</li>
<li>Upgrade your WordPress version if the new release includes core CSS changes.</li>
</ul>',
'jetpack-my-jetpack'
),
),
),
self::UPGRADED_TIER_SLUG => array(
'included' => true,
'description' => __( 'Included', 'jetpack-my-jetpack' ),
'info' => array(
'title' => __( 'Automatic Critical CSS regeneration', 'jetpack-my-jetpack' ),
'content' => __(
'<p>Its essential to regenerate Critical CSS to optimize your site speed whenever your HTML or CSS structure changes. Being on top of this can be tedious and time-consuming.</p>
<p>Boosts cloud service can automatically detect when your site needs the Critical CSS regenerated, and perform this function behind the scenes without requiring you to monitor it manually.</p>',
'jetpack-my-jetpack'
),
),
),
),
),
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(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Image CDN Quality Settings', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Fine-tune image quality settings to your liking.',
'jetpack-my-jetpack'
),
),
'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(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Image guide', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Discover and fix images with a suboptimal resolution, aspect ratio, or file size, improving user experience and page speed.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Defer non-essential JavaScript', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Run non-essential JavaScript after the page has loaded so that styles and images can load more quickly.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Concatenate JS and CSS', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'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' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
);
}
/**
* Get the URL the user is taken after purchasing the product through the checkout
*
* @return ?string
*/
public static function get_post_checkout_url() {
return self::get_manage_url();
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array(
'tiers' => array(
self::FREE_TIER_SLUG => array(
'available' => true,
'is_free' => true,
),
self::UPGRADED_TIER_SLUG => array_merge(
array(
'available' => true,
'wpcom_product_slug' => self::UPGRADED_TIER_PRODUCT_SLUG,
),
Wpcom_Products::get_product_pricing( self::UPGRADED_TIER_PRODUCT_SLUG )
),
),
);
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return admin_url( 'admin.php?page=jetpack-boost' );
}
/**
* Activates the product by installing and activating its plugin
*
* @param bool|WP_Error $current_result Is the result of the top level activation actions. You probably won't do anything if it is an WP_Error.
* @return boolean|WP_Error
*/
public static function do_product_specific_activation( $current_result ) {
$product_activation = parent::do_product_specific_activation( $current_result );
if ( is_wp_error( $product_activation ) && 'module_activation_failed' === $product_activation->get_error_code() ) {
// 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.
$product_activation = true;
}
// We just "got started" in My Jetpack, so skip the in-plugin experience.
update_option( 'jb_get_started', false );
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' );
}
}
@@ -0,0 +1,276 @@
<?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;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* 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',
'jetpack-ai',
);
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return '';
}
}
@@ -0,0 +1,360 @@
<?php
/**
* Creator product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Creator product
*/
class Creator extends Product {
const FREE_TIER_SLUG = 'free';
const UPGRADED_TIER_SLUG = 'upgraded';
const UPGRADED_TIER_PRODUCT_SLUG = 'jetpack_creator_yearly';
/**
* The product slug
*
* @var string
*/
public static $slug = 'creator';
/**
* The slug of the plugin associated with this product - Creator functionalities are part of Jetpack's main plugin
*
* @var string
*/
public static $plugin_slug = self::JETPACK_PLUGIN_SLUG;
/**
* Get the plugin filename - ovewrite it and return Jetpack's
*
* @return ?string
*/
public static function get_plugin_filename() {
return self::JETPACK_PLUGIN_FILENAME;
}
/**
* Whether this product requires a user connection
*
* @var string
*/
public static $requires_user_connection = false;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Creator';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Creator';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Get more subscribers and keep them engaged with our creator tools', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Craft stunning content, boost your subscriber base, and monetize your audience with subscriptions.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Features list
*/
public static function get_features() {
return array(
__( 'Create content that stands out', 'jetpack-my-jetpack' ),
__( 'Grow your subscribers through our creator network and tools', 'jetpack-my-jetpack' ),
__( 'Monetize your online presence and earn from your website', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product's available tiers
*
* @return string[] Slugs of the available tiers
*/
public static function get_tiers() {
return array(
self::UPGRADED_TIER_SLUG,
self::FREE_TIER_SLUG,
);
}
/**
* Get the internationalized comparison of free vs upgraded features
*
* @return array[] Protect features comparison
*/
public static function get_features_by_tier() {
return array(
array(
'name' => __( 'Import subscribers', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Import a CSV file of your existing subscribers to be sent your Newsletter.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array(
'included' => true,
'description' => __( '100 subscribers', 'jetpack-my-jetpack' ),
),
self::UPGRADED_TIER_SLUG => array(
'included' => true,
'description' => __( 'Unlimited subscribers', 'jetpack-my-jetpack' ),
),
),
),
array(
'name' => __( 'Transaction fees', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'<p>Fees are only collected when you accept payments.</p>
<p>Fees are based on the Jetpack plan you have and are calculated as a percentage of your revenue from 10% on the Free plan to 2% on the Creator plan (plus Stripe fees).</p>',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array(
'included' => true,
'description' => __( '10%', 'jetpack-my-jetpack' ),
),
self::UPGRADED_TIER_SLUG => array(
'included' => true,
'description' => __( '2%', 'jetpack-my-jetpack' ),
),
),
),
array(
'name' => __( 'Jetpack Blocks', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Jetpack has over 40 Gutenberg blocks to help you with your content creation, such as displaying your podcasts, showing different content to repeat visitors, creating contact forms and many more.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Paid content gating', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Lock your content behind a paid content block. To access the content, readers will need to pay a one-time fee or a recurring subscription.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Paywall access', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Add a Paywall to your content which lets your visitors read a section of your content before being asked to subscribe to continue reading.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Newsletter', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Start a Newsletter by sending your content as an email newsletter direct to your fans email inboxes.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Pay with PayPal', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Accept payment with PayPal for simple payments like eBooks, courses and more.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'WordAds', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'<p>WordAds adds advertisements to your website. Start earning from your website traffic.</p>
<p>Over 50 internet advertisers — including Google AdSense & Adx, AppNexus, Amazon A9, AOL Marketplace, Yahoo, Criteo, and more — bid to display ads in WordAds spots.</p>',
'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 ),
),
),
);
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array(
'tiers' => array(
self::FREE_TIER_SLUG => array(
'available' => true,
'is_free' => true,
),
self::UPGRADED_TIER_SLUG => array_merge(
array(
'available' => true,
'wpcom_product_slug' => self::UPGRADED_TIER_PRODUCT_SLUG,
),
Wpcom_Products::get_product_pricing( self::UPGRADED_TIER_PRODUCT_SLUG )
),
),
);
}
/**
* 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=creator' );
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_creator_yearly';
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_biyearly_product_slug() {
return 'jetpack_creator_bi_yearly';
}
/**
* Get the WPCOM monthly product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_monthly_product_slug() {
return 'jetpack_creator_monthly';
}
/**
* 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',
);
}
/**
* 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',
);
}
/**
* Checks whether the product can be upgraded - i.e. this shows the /#add-creator interstitial
*
* @return boolean
*/
public static function is_upgradable() {
return ! self::has_paid_plan_for_product();
}
}
@@ -0,0 +1,209 @@
<?php
/**
* Boost product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the CRM product
*/
class Crm extends Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'crm';
/**
* The filename (id) of the plugin associated with this product. If not defined, it will default to the Jetpack plugin
*
* @var string
*/
public static $plugin_filename = array(
'zero-bs-crm/ZeroBSCRM.php',
'crm/ZeroBSCRM.php',
);
/**
* The slug of the plugin associated with this product. If not defined, it will default to the Jetpack plugin
*
* @var string
*/
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
*
* @var string
*/
public static $requires_user_connection = false;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* CRM has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'CRM';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack CRM';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'All of the tools you need to grow your business.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Build better relationships with your customers and grow your business.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array CRM features list
*/
public static function get_features() {
return array(
__( 'Manage unlimited contacts', 'jetpack-my-jetpack' ),
__( 'Manage billing and create invoices', 'jetpack-my-jetpack' ),
__( 'Fully integrated with WordPress & WooCommerce', 'jetpack-my-jetpack' ),
__( 'Infinitely customizable with integrations and extensions', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
// We are hard coding pricing info for CRM because it is not available to us through the CRM API.
return array(
'available' => true,
'is_free' => false,
'full_price' => 132,
'discount_price' => 132,
'is_introductory_offer' => false,
'product_term' => 'year',
'introductory_offer' => null,
// CRM is only sold in USD
'currency_code' => 'USD',
);
}
/**
* Get the URL the user is taken after activating the product
*
* @return ?string
*/
public static function get_post_activation_url() {
return admin_url( 'admin.php?page=zerobscrm-plugin' ); // Welcome page.
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return admin_url( 'admin.php?page=zerobscrm-dash' );
}
/**
* Checks whether the current plan (or purchases) of the site already supports the product
* CRM is available as part of Jetpack Complete
*
* @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;
}
// TODO: check if CRM has a separate plan
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 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' );
}
}
@@ -0,0 +1,144 @@
<?php
/**
* Extras product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Product;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Extras product.
* Extras, so far, could be considered as Jetpack plugin bridge.
*/
class Extras extends Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'extras';
/**
* The slug of the plugin associated with this product.
* Extras, is in short, Jetpack plugin bridge so far.
*
* @var string
*/
public static $plugin_slug = 'jetpack';
/**
* Whether this product requires a user connection
*
* @var string
*/
public static $requires_user_connection = false;
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Extras';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Extras';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Basic tools for a successful site', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( "Secure and speed up your site for free with Jetpack's powerful WordPress tools.", 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Boost features list
*/
public static function get_features() {
return array(
__( 'Speed up your site with optimized images', 'jetpack-my-jetpack' ),
__( 'Protect your site against bot attacks', 'jetpack-my-jetpack' ),
__( 'Get notifications if your site goes offline', 'jetpack-my-jetpack' ),
__( 'Enhance your site with dozens of other features', 'jetpack-my-jetpack' ),
);
}
/**
* 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.
* If Jetpack plugin is active, then Extras will be inactive.
*
* @return boolean
*/
public static function is_active() {
return static::is_jetpack_plugin_active();
}
/**
* Checks whether the plugin is installed
* If Jetpack plugin is installed, then Extras will be inactive.
*
* @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' );
}
/**
* Activates the Jetpack plugin
*
* @return null|WP_Error Null on success, WP_Error on invalid file.
*/
public static function activate_plugin() {
return activate_plugin( static::get_installed_plugin_filename( 'jetpack' ) );
}
}
@@ -0,0 +1,227 @@
<?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;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* 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 '';
}
}
@@ -0,0 +1,184 @@
<?php
/**
* Base product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Modules;
use Automattic\Jetpack\Plugins_Installer;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the hybrid products
*
* Hybrid products are those that may work both as a stand-alone plugin or with the Jetpack plugin.
*/
abstract class Hybrid_Product extends Product {
/**
* All hybrid products have a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* For Hybrid products, we can use either the standalone or Jetpack plugin
*
* @return bool
*/
public static function is_plugin_installed() {
return parent::is_plugin_installed() || parent::is_jetpack_plugin_installed();
}
/**
* Checks whether the Product is active
*
* @return boolean
*/
public static function is_plugin_active() {
return parent::is_plugin_active() || parent::is_jetpack_plugin_active();
}
/**
* Checks whether the standalone plugin for this product is active
*
* @return boolean
*/
public static function is_standalone_plugin_active() {
return parent::is_plugin_active();
}
/**
* Checks whether the Product is active
*
* @return boolean
*/
public static function is_active() {
return parent::is_active() && static::is_module_active();
}
/**
* Activates the plugin
*
* @return null|WP_Error Null on success, WP_Error on invalid file.
*/
public static function activate_plugin() {
/*
* Activate self-installed plugin if it's installed.
*/
if ( parent::is_plugin_installed() ) {
return activate_plugin( static::get_installed_plugin_filename() );
}
/*
* Otherwise, activate Jetpack plugin.
*/
if ( static::is_jetpack_plugin_installed() ) {
return activate_plugin( static::get_installed_plugin_filename( 'jetpack' ) );
}
return new WP_Error( 'plugin_not_found', __( 'Activation failed. Plugin is not installed', 'jetpack-my-jetpack' ) );
}
/**
* Activates the product. If the Hybrid product has declared a jetpack module name, let's try to activate it if Jetpack plugin is active
*
* @param bool|WP_Error $product_activation 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( $product_activation ) {
if ( is_wp_error( $product_activation ) ) {
// If we failed to install the stand-alone plugin because the package was not found, let's try and install Jetpack plugin instead.
// This might happen, for example, while the stand-alone plugin was not released to the WP.org repository yet.
if ( 'no_package' === $product_activation->get_error_code() ) {
$product_activation = Plugins_Installer::install_plugin( self::JETPACK_PLUGIN_SLUG );
if ( ! is_wp_error( $product_activation ) ) {
$product_activation = static::activate_plugin();
}
}
if ( is_wp_error( $product_activation ) ) {
return $product_activation;
}
}
if ( ! empty( static::$module_name ) ) {
// Only activate the module if the plan supports it
// We don't want to throw an error for a missing plan here since we try activation before purchase
if ( static::$requires_plan && ! static::has_any_plan_for_product() ) {
return true;
}
$module_activation = ( new Modules() )->activate( static::$module_name, false, false );
if ( ! $module_activation ) {
return new WP_Error( 'module_activation_failed', __( 'Error activating Jetpack module', 'jetpack-my-jetpack' ) );
}
return $module_activation;
}
return true;
}
/**
* Deactivates the product.
*
* In addition to the parent's plugin deactivation, also deactivates the
* matching Jetpack module when `static::$module_name` is set and the
* module is currently active — otherwise Hybrid products activated via
* the Jetpack-module path stay half-on after "deactivation".
*
* @return bool|WP_Error True on success (matches Product::deactivate()),
* WP_Error if the module deactivation failed.
*/
public static function deactivate() {
$result = parent::deactivate();
$modules = new Modules();
if ( ! empty( static::$module_name ) && $modules->is_active( static::$module_name ) ) {
if ( ! $modules->deactivate( static::$module_name ) ) {
return new WP_Error(
'module_deactivation_failed',
__( 'Error deactivating Jetpack module', 'jetpack-my-jetpack' )
);
}
}
return $result;
}
/**
* Install and activate the standalone plugin in the case it's missing.
*
* @return boolean|WP_Error
*/
public static function install_and_activate_standalone() {
$result = parent::install_and_activate_standalone();
if ( is_wp_error( $result ) ) {
return $result;
}
/**
* Activate the module as well, if the user has a plan
* or the product does not require a plan to work
*/
if ( static::has_any_plan_for_product() && isset( static::$module_name ) ) {
$module_activation = ( new Modules() )->activate( static::$module_name, false, false );
if ( ! $module_activation ) {
return new WP_Error( 'module_activation_failed', __( 'Error activating Jetpack module', 'jetpack-my-jetpack' ) );
}
}
return true;
}
}
@@ -0,0 +1,681 @@
<?php
/**
* AI product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\My_Jetpack\Initializer;
use Automattic\Jetpack\My_Jetpack\Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use WP_Post;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Jetpack AI product
*/
class Jetpack_Ai extends Product {
const CURRENT_TIER_SLUG = 'free';
const UPGRADED_TIER_SLUG = 'upgraded';
/**
* The product slug
*
* @var string
*/
public static $slug = 'jetpack-ai';
/**
* The category of the product
*
* @var string
*/
public static $category = 'create';
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* The feature slug that identifies the paid plan
*
* @var string
*/
public static $feature_identifying_paid_plan = 'ai-assistant';
/**
* Get the plugin slug - ovewrite it and return Jetpack's
*
* @return ?string
*/
public static function get_plugin_slug() {
return self::JETPACK_PLUGIN_SLUG;
}
/**
* Get the plugin filename - ovewrite it and return Jetpack's
*
* @return ?string
*/
public static function get_plugin_filename() {
return self::JETPACK_PLUGIN_FILENAME;
}
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'AI';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack AI';
}
/**
* Get the product's available tiers
*
* @return string[] Slugs of the available tiers
*/
public static function get_tiers() {
if ( ! self::are_tier_plans_enabled() ) {
return parent::get_tiers();
}
return array(
self::UPGRADED_TIER_SLUG,
self::CURRENT_TIER_SLUG,
);
}
/**
* Get the internationalized comparison of free vs upgraded features
*
* @return array[] Protect features comparison
*/
public static function get_features_by_tier() {
if ( ! self::are_tier_plans_enabled() ) {
return parent::get_features_by_tier();
}
$current_tier = self::get_current_usage_tier();
$current_description = 0 === $current_tier
? __( 'Up to 20 requests', 'jetpack-my-jetpack' )
/* translators: number of requests */
: sprintf( __( 'Up to %d requests per month', 'jetpack-my-jetpack' ), $current_tier );
$next_tier = self::get_next_usage_tier();
$next_description = $next_tier === null
? __( 'Let\'s get in touch', 'jetpack-my-jetpack' )
/* translators: number of requests */
: sprintf( __( 'Up to %d requests per month', 'jetpack-my-jetpack' ), $next_tier );
return array(
array(
'name' => __( 'Number of requests', 'jetpack-my-jetpack' ),
'info' => array(
'title' => __( 'Requests', 'jetpack-my-jetpack' ),
'content' => __( 'Increase your monthly request limit. Upgrade now and have the option to further increase your requests with additional upgrades.', 'jetpack-my-jetpack' ),
),
'tiers' => array(
self::CURRENT_TIER_SLUG => array(
'included' => true,
'description' => $current_description,
),
self::UPGRADED_TIER_SLUG => array(
'included' => true,
'description' => $next_description,
),
),
),
array(
'name' => __( 'Generate and edit content', 'jetpack-my-jetpack' ),
'tiers' => array(
self::CURRENT_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Build forms from prompts', 'jetpack-my-jetpack' ),
'tiers' => array(
self::CURRENT_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Get feedback on posts', 'jetpack-my-jetpack' ),
'tiers' => array(
self::CURRENT_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Generate featured images', 'jetpack-my-jetpack' ),
'tiers' => array(
self::CURRENT_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
);
}
/**
* Get the current usage tier
*
* @return int
*/
public static function get_current_usage_tier() {
if ( ! self::is_site_connected() ) {
return 0;
}
$info = self::get_ai_assistant_feature();
// Bail early if it's not possible to fetch the feature data.
if ( is_wp_error( $info ) ) {
return 0;
}
$current_tier = $info['current-tier']['value'] ?? null;
return $current_tier;
}
/**
* Get the next usage tier
*
* @return int
*/
public static function get_next_usage_tier() {
if ( ! self::is_site_connected() || ! self::has_paid_plan_for_product() ) {
// without site connection we can't know if tiers are enabled or not,
// hence we can't know if the next tier is 100 or 1 (unlimited).
return 100;
}
$info = self::get_ai_assistant_feature();
// Bail early if it's not possible to fetch the feature data or if it's included in a plan.
if ( is_wp_error( $info ) || empty( $info ) ) {
return null;
}
// Trust the next tier provided by the feature data.
$next_tier = $info['next-tier']['value'] ?? null;
return $next_tier;
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Turn your ideas into readytopublish content at lightspeed.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized usage tier long description by tier
*
* @param int|null $tier The usage tier.
* @return string
*/
public static function get_long_description_by_usage_tier( $tier ) {
switch ( (int) $tier ) {
case 1:
return __( 'Jetpack AI Assistant brings the power of AI right into your WordPress editor, letting your content creation soar to new heights.', 'jetpack-my-jetpack' );
case 100:
return __( 'The most advanced AI technology Jetpack has to offer.', 'jetpack-my-jetpack' );
default:
return __( 'Upgrade and increase the amount of your available monthly requests to continue using the most advanced AI technology Jetpack has to offer.', 'jetpack-my-jetpack' );
}
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
$next_tier = self::get_next_usage_tier();
return self::get_long_description_by_usage_tier( $next_tier );
}
/**
* Get the internationalized usage tier features by tier
*
* @param int $tier The usage tier.
* @return string
*/
public static function get_features_by_usage_tier( $tier ) {
$is_tier_plan = $tier && intval( $tier ) > 1;
if ( $tier === 100 && ( ! self::is_site_connected() || ! self::has_paid_plan_for_product() ) ) {
// in these cases, get_next_usage_tier() will return 100
// 100 is fine as default when tiered plans are enabled, but not otherwise
$is_tier_plan = false;
}
$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' ),
__( 'Generate images with one-click', 'jetpack-my-jetpack' ),
__( 'Optimize your titles for better performance', 'jetpack-my-jetpack' ),
__( 'Priority support', 'jetpack-my-jetpack' ),
);
$tiered_features = array(
__( 'Prompt based content generation', 'jetpack-my-jetpack' ),
__( 'Generate text, tables, and lists', 'jetpack-my-jetpack' ),
__( 'Adaptive tone adjustment', 'jetpack-my-jetpack' ),
__( 'Superior spelling and grammar correction', 'jetpack-my-jetpack' ),
__( 'Title & summary generation', 'jetpack-my-jetpack' ),
__( 'Priority support', 'jetpack-my-jetpack' ),
/* translators: %d is the number of requests. */
sprintf( __( 'Up to %d requests per month', 'jetpack-my-jetpack' ), $tier ),
);
return $is_tier_plan ? $tiered_features : $features;
}
/**
* Get the internationalized features list
*
* @return array Jetpack AI features list
*/
public static function get_features() {
$next_tier = self::get_next_usage_tier();
return self::get_features_by_usage_tier( $next_tier );
}
/**
* Get the product pricing details by tier
*
* @param int|null $tier The usage tier.
* @return array Pricing details
*/
public static function get_pricing_for_ui_by_usage_tier( $tier ) {
if ( $tier === null ) {
return array();
}
$product = Wpcom_Products::get_product( static::get_wpcom_product_slug() );
if ( empty( $product ) ) {
return array();
}
$tier_plans_enabled = self::are_tier_plans_enabled();
/*
* when tiers are enabled and the price tier list is empty,
* we may need to renew the cache for the product data so
* we get the new price tier list.
*
* if the list is still empty after the fresh data, we will
* default to empty pricing (by returning an empty array).
*/
if ( empty( $product->price_tier_list ) && $tier_plans_enabled ) {
$product = Wpcom_Products::get_product( static::get_wpcom_product_slug(), true );
}
// get the base pricing for the unlimited plan, for compatibility
$base_pricing = Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() );
$price_tier_list = $product->price_tier_list;
$yearly_prices = array();
foreach ( $price_tier_list as $price_tier ) {
if ( isset( $price_tier->maximum_units ) && isset( $price_tier->maximum_price ) ) {
// The prices are in cents
$yearly_prices[ $price_tier->maximum_units ] = $price_tier->maximum_price / 100;
}
}
// add the base pricing to the list
$prices = array( 1 => $base_pricing );
foreach ( $yearly_prices as $units => $price ) {
$prices[ $units ] = array_merge(
$base_pricing,
array(
'full_price' => $price,
'discount_price' => $price,
'is_introductory_offer' => false,
'introductory_offer' => null,
)
);
}
return $prices[ $tier ] ?? array();
}
/**
* Get the product pricing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
// no tiers
if ( ! self::are_tier_plans_enabled() ) {
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => static::get_wpcom_product_slug(),
),
// hardcoding 1 as next tier if tiers are not enabled
self::get_pricing_for_ui_by_usage_tier( 1 )
);
}
$next_tier = self::get_next_usage_tier();
$current_tier = self::get_current_usage_tier();
$current_call_to_action = $current_tier === 0
? __( 'Continue for free', 'jetpack-my-jetpack' )
: __( 'I\'m fine with my plan, thanks', 'jetpack-my-jetpack' );
$next_call_to_action = $next_tier === null
? __( 'Contact Us', 'jetpack-my-jetpack' )
: __( 'Upgrade', 'jetpack-my-jetpack' );
return array(
'tiers' => array(
self::CURRENT_TIER_SLUG => array_merge(
self::get_pricing_for_ui_by_usage_tier( $current_tier ),
array(
'available' => true,
'is_free' => true,
'call_to_action' => $current_call_to_action,
)
),
self::UPGRADED_TIER_SLUG => array_merge(
self::get_pricing_for_ui_by_usage_tier( $next_tier ),
array(
'wpcom_product_slug' => static::get_wpcom_product_slug(),
'quantity' => $next_tier,
'call_to_action' => $next_call_to_action,
)
),
),
);
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_ai_yearly';
}
/**
* Get the WPCOM monthly product slug used to make the purchase
*
* @return string
*/
public static function get_wpcom_monthly_product_slug() {
return 'jetpack_ai_monthly';
}
/**
* Get the WPCOM bi-yearly product slug used to make the purchase
*
* @return string
*/
public static function get_wpcom_bi_yearly_product_slug() {
return 'jetpack_ai_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_ai_yearly',
'jetpack_ai_monthly',
'jetpack_ai_bi_yearly',
);
}
/**
* Checks whether the product can be upgraded to a different product.
*
* @return boolean
*/
public static function is_upgradable() {
$has_ai_feature = static::does_site_have_feature( 'ai-assistant' );
$tier_plans_enabled = self::are_tier_plans_enabled();
$current_tier = self::get_current_usage_tier();
if ( $has_ai_feature && ! $tier_plans_enabled && $current_tier >= 1 ) {
return false;
}
$next_tier = self::get_next_usage_tier();
// The check below is debatable, not having the feature should not flag as not upgradable.
// If user is free (tier = 0), not unlimited (tier = 1) and has a next tier, then it's upgradable.
if ( $current_tier !== null && $current_tier !== 1 && $next_tier ) {
return true;
}
// Mark as not upgradable if user is on unlimited tier or does not have any plan.
if ( ! $has_ai_feature || null === $current_tier || 1 === $current_tier ) {
return false;
}
return true;
}
/**
* Get the URL the user is taken after purchasing the product through the checkout
*
* @return ?string
*/
public static function get_post_checkout_url() {
return self::get_manage_url();
}
/**
* Get the URL the user is taken after activating the product through the checkout
*
* @return ?string
*/
public static function get_post_activation_url() {
return self::get_manage_url();
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return admin_url( 'admin.php?page=my-jetpack#/jetpack-ai' );
}
/**
* Checks whether the plugin is installed
*
* @return boolean
*/
public static function is_plugin_installed() {
return self::is_jetpack_plugin_installed();
}
/**
* Checks whether the plugin is active
*
* @return boolean
*/
public static function is_plugin_active() {
return (bool) static::is_jetpack_plugin_active();
}
/**
* Checks whether the Product is active
*
* Overrides the parent method to respect the jetpack_ai_enabled filter.
*
* @return boolean
*/
public static function is_active() {
/**
* Filter to enable/disable Jetpack AI.
*
* @since 5.28.3
*
* @param boolean $enabled True if Jetpack AI should be enabled, false otherwise. Default true.
*/
$is_enabled = apply_filters( 'jetpack_ai_enabled', true );
return $is_enabled && parent::is_active();
}
/**
* Get data about the AI Assistant feature
*
* @return array
*/
public static function get_ai_assistant_feature() {
// Bail early if the plugin is not active.
if ( ! self::is_jetpack_plugin_installed() ) {
return array();
}
// Check if the global constant is defined.
if ( ! defined( 'JETPACK__PLUGIN_DIR' ) ) {
return array();
}
// Bail early if the site is not connected.
if ( ! self::is_site_connected() ) {
return array();
}
// Check if class exists. If not, try to require it once.
if ( ! class_exists( 'Jetpack_AI_Helper' ) ) {
$class_file_path = JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-ai-helper.php';
// Check whether the file exists
if ( ! file_exists( $class_file_path ) ) {
return array();
}
require_once $class_file_path;
}
return \Jetpack_AI_Helper::get_ai_assistance_feature();
}
/**
* Get the AI Assistant tiered plans status
*
* @return boolean
*/
public static function are_tier_plans_enabled() {
$info = self::get_ai_assistant_feature();
if ( is_wp_error( $info ) ) {
// this is another faulty default value, we'll assume disabled while
// production is enabled
return false;
}
if ( ! empty( $info ) && isset( $info['tier-plans-enabled'] ) ) {
return boolval( $info['tier-plans-enabled'] );
}
return false;
}
/**
* Checks whether the site is connected to WordPress.com.
*
* @return boolean
*/
private static function is_site_connected() {
return ( new Connection_Manager() )->is_connected();
}
/**
* Get the URL where the user manages the product
*
* NOTE: this method is the only thing that resembles an initialization for the product.
*
* @return void
*/
public static function extend_plugin_action_links() {
add_action( 'myjetpack_enqueue_scripts', array( static::class, 'admin_enqueue_scripts' ) );
add_filter( 'default_content', array( static::class, 'add_ai_block' ), 10, 2 );
}
/**
* Enqueue the AI Assistant script
*
* The script is just a global variable used for the nonce, needed for the create post link.
*
* @return void
*/
public static function admin_enqueue_scripts() {
wp_register_script(
'my_jetpack_ai_app',
false,
array(),
Initializer::PACKAGE_VERSION,
array( 'in_footer' => true )
);
wp_localize_script(
'my_jetpack_ai_app',
'jetpackAi',
array(
'nonce' => wp_create_nonce( 'ai-assistant-content-nonce' ),
)
);
wp_enqueue_script( 'my_jetpack_ai_app' );
}
/**
* Add AI block to the post content
*
* Used only from the link on the product page, the filter will insert an AI Assistant block in the post content.
*
* @param string $content The post content.
* @param WP_Post $post The post object.
* @return string
*/
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
) {
return '<!-- wp:jetpack/ai-assistant /-->';
}
return $content;
}
}
@@ -0,0 +1,195 @@
<?php
/**
* Forms product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Module_Product;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Jetpack Forms product.
*
* Forms is a feature available as part of the Jetpack plugin, backed by the
* `contact-form` module.
*/
class Jetpack_Forms extends Module_Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'jetpack-forms';
/**
* The slug of the plugin associated with this product.
* Forms 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 Forms.
*
* @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 = 'contact-form';
/**
* 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 'Forms';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Forms';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Build and share forms to collect leads, feedback, and payments.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Create contact forms, surveys, and registration forms in minutes — then manage every response right from your dashboard, no third-party tools required.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized feature list
*
* @return array Forms features list
*/
public static function get_features() {
return array(
__( 'Spam protection with Akismet', 'jetpack-my-jetpack' ),
__( 'Export your data anytime', 'jetpack-my-jetpack' ),
__( 'Manage all your responses in one place', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product pricing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array(
'available' => true,
'is_free' => true,
);
}
/**
* 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() {
// Defer to the Forms package for the canonical admin URL when it's available
// (it accounts for the responses dashboard variant and the admin URL filter).
$dashboard = 'Automattic\Jetpack\Forms\Dashboard\Dashboard';
if ( method_exists( $dashboard, 'get_forms_admin_url' ) ) {
return $dashboard::get_forms_admin_url();
}
return admin_url( 'admin.php?page=jetpack-forms-admin' );
}
/**
* 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 );
}
}
}
@@ -0,0 +1,168 @@
<?php
/**
* Base Module product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Jetpack;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Module products
*
* Module products are those that are a Jetpack module behind the scenes.
*
* They require Jetpack plugin and will then activate/deactivate a module.
*/
abstract class Module_Product extends Product {
/**
* The Jetpack module name associated with this product
*
* @var string|null
*/
public static $module_name = null;
/**
* Get the plugin slug - ovewrite it ans return Jetpack's
*
* @return ?string
*/
public static function get_plugin_slug() {
return self::JETPACK_PLUGIN_SLUG;
}
/**
* Get the plugin filename - ovewrite it ans return Jetpack's
*
* @return ?string
*/
public static function get_plugin_filename() {
return self::JETPACK_PLUGIN_FILENAME;
}
/**
* Ensure that child classes define $module_name attribute
*
* @throws \Exception If required attribute is not declared in the child class.
* @return void
*/
private static function check_for_module_name() {
if ( empty( static::$module_name ) ) {
throw new \Exception( 'Module Product classes must declare the $module_name attribute.' );
}
}
/**
* Checks whether the Product is active
*
* @return boolean
*/
public static function is_active() {
return static::is_jetpack_plugin_active() && static::is_module_active();
}
/**
* Checks whether the Jetpack module is active
*
* @return bool
*/
public static function is_module_active() {
self::check_for_module_name();
if ( ! class_exists( 'Jetpack' ) ) {
return false;
}
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;
}
return $status;
}
/**
* Activates the product by installing and activating its plugin
*
* @param bool|WP_Error $plugin_activation Is the result of the top level activation actions. You probably won't do anything if it is an WP_Error.
* @return boolean|\WP_Error
*/
public static function do_product_specific_activation( $plugin_activation ) {
self::check_for_module_name();
if ( is_wp_error( $plugin_activation ) ) {
return $plugin_activation;
}
if ( ! class_exists( 'Jetpack' ) ) {
return new WP_Error( 'plugin_activation_failed', __( 'Error activating Jetpack plugin', 'jetpack-my-jetpack' ) );
}
$module_activation = Jetpack::activate_module( static::$module_name, false, false );
if ( ! $module_activation ) {
return new WP_Error( 'module_activation_failed', __( 'Error activating Jetpack module', 'jetpack-my-jetpack' ) );
}
return $module_activation;
}
/**
* Deactivate the module
*
* @return boolean
*/
public static function deactivate() {
self::check_for_module_name();
if ( ! class_exists( 'Jetpack' ) ) {
return true;
}
return Jetpack::deactivate_module( static::$module_name );
}
}
@@ -0,0 +1,190 @@
<?php
/**
* Feature: Newsletter
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Module_Product;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* 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-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 );
}
}
}
@@ -0,0 +1,466 @@
<?php
/**
* Protect product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Hybrid_Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
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;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Protect product
*/
class Protect extends Hybrid_Product {
const FREE_TIER_SLUG = 'free';
const UPGRADED_TIER_SLUG = 'upgraded';
const UPGRADED_TIER_PRODUCT_SLUG = 'jetpack_scan';
const SCAN_FEATURE_SLUG = 'scan';
const FIREWALL_FEATURE_SLUG = 'firewall';
/**
* The product slug
*
* @var string
*/
public static $slug = 'protect';
/**
* The Jetpack module name
*
* @var string
*/
public static $module_name = 'protect';
/**
* The filename (id) of the plugin associated with this product.
*
* @var string
*/
public static $plugin_filename = array(
'jetpack-protect/jetpack-protect.php',
'protect/jetpack-protect.php',
'jetpack-protect-dev/jetpack-protect.php',
);
/**
* The slug of the plugin associated with this product.
*
* @var string
*/
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
*
* @var string
*/
public static $requires_user_connection = false;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* Protect has a standalone plugin
*
* @var bool
*/
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
*
* @return string
*/
public static function get_name() {
return 'Protect';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Protect';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Guard against malware and bad actors 24/7', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Protect your site from bad actors and malware 24/7. Clean up security vulnerabilities with one click.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Protect features list
*/
public static function get_features() {
return array(
__( 'Over 20,000 listed vulnerabilities', 'jetpack-my-jetpack' ),
__( 'Daily automatic scans', 'jetpack-my-jetpack' ),
__( 'Check plugin and theme version status', 'jetpack-my-jetpack' ),
__( 'Easy to navigate and use', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product's available tiers
*
* @return string[] Slugs of the available tiers
*/
public static function get_tiers() {
return array(
self::UPGRADED_TIER_SLUG,
self::FREE_TIER_SLUG,
);
}
/**
* Get the internationalized comparison of free vs upgraded features
*
* @return array[] Protect features comparison
*/
public static function get_features_by_tier() {
return array(
array(
'name' => __( 'Scan for threats and vulnerabilities', 'jetpack-my-jetpack' ),
'tiers' => array(
self::FREE_TIER_SLUG => array(
'included' => true,
'description' => __( 'Check items against database', 'jetpack-my-jetpack' ),
),
self::UPGRADED_TIER_SLUG => array(
'included' => true,
'description' => __( 'Line by line malware scanning', 'jetpack-my-jetpack' ),
),
),
),
array(
'name' => __( 'Daily automated scans', 'jetpack-my-jetpack' ),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array(
'included' => true,
'description' => __( 'Plus on-demand manual scans', 'jetpack-my-jetpack' ),
),
),
),
array(
'name' => __( 'Web Application Firewall', 'jetpack-my-jetpack' ),
'tiers' => array(
self::FREE_TIER_SLUG => array(
'included' => false,
'description' => __( 'Manual rules only', 'jetpack-my-jetpack' ),
),
self::UPGRADED_TIER_SLUG => array(
'included' => true,
'description' => __( 'Automatic protection and rule updates', 'jetpack-my-jetpack' ),
),
),
),
array(
'name' => __( 'Brute force protection', 'jetpack-my-jetpack' ),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Account protection', 'jetpack-my-jetpack' ),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Access to scan on Cloud', 'jetpack-my-jetpack' ),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'One-click auto fixes', 'jetpack-my-jetpack' ),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Notifications', 'jetpack-my-jetpack' ),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Severity labels', 'jetpack-my-jetpack' ),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
);
}
/**
* Get the product pricing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array(
'tiers' => array(
self::FREE_TIER_SLUG => array(
'available' => true,
'is_free' => true,
),
self::UPGRADED_TIER_SLUG => array_merge(
array(
'available' => true,
'wpcom_product_slug' => self::UPGRADED_TIER_PRODUCT_SLUG,
),
Wpcom_Products::get_product_pricing( self::UPGRADED_TIER_PRODUCT_SLUG )
),
),
);
}
/**
* 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() {
$protect_threat_status = false;
$scan_data = Protect_Status::get_status();
// Check if there are scan threats.
$protect_data = $scan_data;
if ( is_wp_error( $protect_data ) ) {
return $protect_threat_status; // false
}
$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 $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',
);
}
/**
* Checks whether the product can be upgraded - i.e. this shows the /#add-protect interstitial
*
* @return boolean
*/
public static function is_upgradable() {
return ! self::has_paid_plan_for_product();
}
/**
* Get the URL the user is taken after purchasing the product through the checkout
*
* @return ?string
*/
public static function get_post_checkout_url() {
return self::get_manage_url();
}
/**
* Get the URL the user is taken after purchasing the product through the checkout for each product feature
*
* @return ?array
*/
public static function get_post_checkout_urls_by_feature() {
return array(
self::SCAN_FEATURE_SLUG => self::get_post_checkout_url(),
self::FIREWALL_FEATURE_SLUG => admin_url( 'admin.php?page=jetpack-protect#/firewall' ),
);
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
if ( static::is_standalone_plugin_active() ) {
// Protect admin dashboard.
return admin_url( 'admin.php?page=jetpack-protect' );
}
if ( static::has_paid_plan_for_product() ) {
// Paid users without standalone plugin go to Jetpack Cloud Scan dashboard.
return Redirect::get_url( 'my-jetpack-manage-scan' );
}
// Free users without standalone plugin go to the Protect details page.
return admin_url( 'admin.php?page=my-jetpack#/protect-details' );
}
/**
* Get the URL where the user manages the product for each product feature
*
* @return ?array
*/
public static function get_manage_urls_by_feature() {
return array(
self::SCAN_FEATURE_SLUG => self::get_manage_url(),
self::FIREWALL_FEATURE_SLUG => admin_url( 'admin.php?page=jetpack-protect#/firewall' ),
);
}
/**
* Return product bundles list
* that supports the product.
*
* @return array Products bundle list.
*/
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 ) )
),
)
);
}
}
@@ -0,0 +1,190 @@
<?php
/**
* Feature: Related Posts
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Module_Product;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* 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 );
}
}
}
@@ -0,0 +1,221 @@
<?php
/**
* Scan product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Module_Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use Automattic\Jetpack\Redirect;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Scan product
*/
class Scan extends Module_Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'scan';
/**
* The Jetpack module name
*
* @var string
*/
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
*
* @return string
*/
public static function get_name() {
return 'Scan';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Scan';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Aroundtheclock web application firewall, and automated malware scanning.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Automatic scanning and one-click fixes keep your site one step ahead of security threats and malware.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Scan features list
*/
public static function get_features() {
return array(
_x( 'Automated daily scanning', 'Scan Product Feature', 'jetpack-my-jetpack' ),
_x( 'One-click fixes for most issues', 'Scan Product Feature', 'jetpack-my-jetpack' ),
_x( 'Instant email notifications', 'Scan Product Feature', 'jetpack-my-jetpack' ),
_x( 'Access to latest Firewall rules', 'Scan Product Feature', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => static::get_wpcom_product_slug(),
),
Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() )
);
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_scan';
}
/**
* Checks whether the Product is active
*
* Scan is not actually 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();
}
/**
* Activates the product by installing and activating its plugin
*
* @param bool|WP_Error $current_result Is the result of the top level activation actions. You probably won't do anything if it is an WP_Error.
* @return boolean|\WP_Error
*/
public static function do_product_specific_activation( $current_result ) {
$product_activation = parent::do_product_specific_activation( $current_result );
if ( is_wp_error( $product_activation ) && 'module_activation_failed' === $product_activation->get_error_code() ) {
// Scan is not a module. There's nothing in the plugin to be activated, so it's ok to fail to activate the module.
$product_activation = true;
}
return $product_activation;
}
/**
* Checks whether the Jetpack module is active
*
* Scan is not a module. Nothing needs to be active. Let's always consider it active.
*
* @return bool
*/
public static function is_module_active() {
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.
*
* @return boolean|array Products bundle list.
*/
public static function is_upgradable_by_bundle() {
return array( 'security', 'complete' );
}
/**
* Get the URL the user is taken after activating the product
*
* @return ?string
*/
public static function get_post_activation_url() {
return ''; // stay in My Jetpack page.
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return Redirect::get_url( 'my-jetpack-manage-scan' );
}
/**
* Get the URL where the user should be redirected after checkout
*/
public static function get_post_checkout_url() {
if ( static::is_jetpack_plugin_active() ) {
return 'admin.php?page=jetpack#/recommendations';
}
// If Jetpack is not active, it means that the user has another standalone plugin active
// and it follows the `Protect` plugin flow instead of `Scan` so for now it would be safe
// to return null and let the user go back to the My Jetpack page.
return null;
}
}
@@ -0,0 +1,192 @@
<?php
/**
* Get search stats for use in the wp-admin dashboard.
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\Connection\Client;
use Jetpack_Options;
/**
* Search stats (e.g. post count, post type breakdown)
*/
class Search_Stats {
const EXCLUDED_POST_TYPES = array(
'elementor_library', // Used by Elementor.
'jp_sitemap', // Used by Jetpack.
'revision',
'vip-legacy-redirect',
'scheduled-action',
'nbcs_video_lookup',
'reply', // bbpress, these get included in the topic
'product_variation', // woocommerce, not really public
'nav_menu_item',
'shop_order', // woocommerce, not really public
'redirect_rule', // Used by the Safe Redirect plugin.
);
const DO_NOT_EXCLUDE_POST_TYPES = array(
'topic', // bbpress
'forum', // bbpress
);
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.
*/
public function get_stats_from_wpcom() {
$blog_id = Jetpack_Options::get_option( 'id' );
if ( ! is_numeric( $blog_id ) ) {
return null;
}
$response = Client::wpcom_json_api_request_as_blog(
'/sites/' . (int) $blog_id . '/jetpack-search/stats',
'2',
array(),
null,
'wpcom'
);
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.
*/
public static function estimate_count() {
return array_sum( static::get_post_type_breakdown() );
}
/**
* Calculate breakdown of post types for the site.
*/
public static function get_post_type_breakdown() {
$indexable_post_types = get_post_types(
array(
'public' => true,
'exclude_from_search' => false,
)
);
$indexable_status_array = get_post_stati(
array(
'public' => true,
'exclude_from_search' => false,
)
);
$raw_posts_counts = static::get_raw_post_type_breakdown();
if ( ! $raw_posts_counts || is_wp_error( $raw_posts_counts ) ) {
return array();
}
$posts_counts = static::get_post_type_breakdown_with( $raw_posts_counts, $indexable_post_types, $indexable_status_array );
return $posts_counts;
}
/**
* Calculate breakdown of post types with passed in indexable post types and statuses.
* The function is going to be used from WPCOM as well for consistency.
*
* @param array $raw_posts_counts Array of post types with counts as value.
* @param array $indexable_post_types Array of indexable post types.
* @param array $indexable_status_array Array of indexable post statuses.
*/
public static function get_post_type_breakdown_with( $raw_posts_counts, $indexable_post_types, $indexable_status_array ) {
$posts_counts = array();
foreach ( $raw_posts_counts as $row ) {
// ignore if status is not public.
if ( ! in_array( $row['post_status'], $indexable_status_array, true ) ) {
continue;
}
// ignore if post type is in excluded post types.
if ( in_array( $row['post_type'], self::EXCLUDED_POST_TYPES, true ) ) {
continue;
}
// ignore if post type is not public and is not explicitly included.
if ( ! in_array( $row['post_type'], $indexable_post_types, true ) &&
! in_array( $row['post_type'], self::DO_NOT_EXCLUDE_POST_TYPES, true )
) {
continue;
}
// add up post type counts of potentially multiple post_status.
if ( ! isset( $posts_counts[ $row['post_type'] ] ) ) {
$posts_counts[ $row['post_type'] ] = 0;
}
$posts_counts[ $row['post_type'] ] += intval( $row['num_posts'] );
}
arsort( $posts_counts, SORT_NUMERIC );
return $posts_counts;
}
/**
* 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;
$results = wp_cache_get( self::POST_TYPE_BREAKDOWN_CACHE_KEY, self::CACHE_GROUP );
if ( false !== $results ) {
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 = ''
GROUP BY post_type, post_status";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery
$results = $wpdb->get_results( $query, ARRAY_A );
wp_cache_set( self::POST_TYPE_BREAKDOWN_CACHE_KEY, $results, self::CACHE_GROUP, self::CACHE_EXPIRY );
return $results;
}
}
@@ -0,0 +1,414 @@
<?php
/**
* Search product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
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 WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Search product
*/
class Search extends Hybrid_Product {
/**
* Fallback starting price (USD, billed yearly) for the entry record tier, used when
* the WPCOM pricing fetch fails so the dashboard still shows a price, not "$0".
*
* @var float
*/
const FALLBACK_STARTING_PRICE_USD = 100;
/**
* Search "new pricing" version identifier (introduced 202208).
*
* Intentionally duplicated from Automattic\Jetpack\Search\Plan::JETPACK_SEARCH_NEW_PRICING_VERSION
* rather than referencing that class. My Jetpack is bundled into standalone plugins (e.g. Jetpack
* Boost) that do NOT ship the jetpack-search package, so referencing Search\Plan here fatals with
* "Class not found" the moment this product builds its pricing data (introduced by PR #48892).
*
* @var string
*/
const SEARCH_NEW_PRICING_VERSION = '202208';
/**
* The product slug
*
* @var string
*/
public static $slug = 'search';
/**
* The Jetpack module name
*
* @var string
*/
public static $module_name = 'search';
/**
* The slug of the plugin associated with this product.
*
* @var string
*/
public static $plugin_slug = 'jetpack-search';
/**
* The category of the product
*
* @var string
*/
public static $category = 'performance';
/**
* Search has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* Whether this product requires a plan to work at all
*
* @var bool
*/
public static $requires_plan = true;
/**
* The filename (id) of the plugin associated with this product.
*
* @var string
*/
public static $plugin_filename = array(
'jetpack-search/jetpack-search.php',
'search/jetpack-search.php',
'jetpack-search-dev/jetpack-search.php',
);
/**
* Search only requires site connection
*
* @var boolean
*/
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
*
* @return string
*/
public static function get_name() {
return 'Search';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Search';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Instantly deliver the most relevant results to your visitors.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Help your site visitors find answers instantly so they keep reading and buying. Great for sites with a lot of content.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Boost features list
*/
public static function get_features() {
return array(
__( 'Instant search and indexing', 'jetpack-my-jetpack' ),
__( 'Powerful filtering', 'jetpack-my-jetpack' ),
__( 'Supports 38 languages', 'jetpack-my-jetpack' ),
__( 'Spelling correction', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
// Basic pricing info.
$pricing = array_merge(
array(
'available' => true,
'trial_available' => static::has_trial_support(),
'wpcom_product_slug' => static::get_wpcom_product_slug(),
'wpcom_free_product_slug' => static::get_wpcom_free_product_slug(),
),
Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() )
);
$record_count = intval( Search_Stats::estimate_count() );
$search_pricing = static::get_pricing_from_wpcom( $record_count );
if ( is_wp_error( $search_pricing ) ) {
// Default to the current pricing experience when the WPCOM fetch fails so the
// dashboard degrades to the production default, not the legacy single-card view.
$pricing['pricing_version'] = self::SEARCH_NEW_PRICING_VERSION;
// If the generic product pricing was also unavailable, fall back to a USD
// starting price so the pricing grid renders a price instead of "$0".
if ( empty( $pricing['full_price'] ) ) {
$pricing['currency_code'] = 'USD';
$pricing['full_price'] = self::FALLBACK_STARTING_PRICE_USD;
$pricing['discount_price'] = self::FALLBACK_STARTING_PRICE_USD;
}
return $pricing;
}
$pricing['estimated_record_count'] = $record_count;
return array_merge( $pricing, $search_pricing );
}
/**
* Get the URL the user is taken after purchasing the product through the checkout
*
* @return ?string
*/
public static function get_post_checkout_url() {
return self::get_manage_url();
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_search';
}
/**
* Get the WPCOM free product slug
*
* @return ?string
*/
public static function get_wpcom_free_product_slug() {
return 'jetpack_search_free';
}
/**
* Returns true if the new_pricing_202208 is set to not empty in URL for testing purpose, or it's active.
*/
public static function is_new_pricing_202208() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
if ( isset( $_GET['new_pricing_202208'] ) && $_GET['new_pricing_202208'] ) {
return true;
}
$record_count = intval( Search_Stats::estimate_count() );
$search_pricing = static::get_pricing_from_wpcom( $record_count );
if ( is_wp_error( $search_pricing ) ) {
// Default to the current pricing experience when the WPCOM fetch fails.
return true;
}
return self::SEARCH_NEW_PRICING_VERSION === $search_pricing['pricing_version'];
}
/**
* Override status to `needs_activation` when status is `needs_plan`.
*/
public static function get_status() {
$status = parent::get_status();
return $status;
}
/**
* Use centralized Search pricing API.
*
* The function is also used by the search package, as a result it could be called before site connection - i.e. blog token might not be available.
*
* @param int $record_count Record count to estimate pricing.
*
* @return array|WP_Error
*/
public static function get_pricing_from_wpcom( $record_count ) {
static $pricings = array();
$connection = new Connection_Manager();
$blog_id = \Jetpack_Options::get_option( 'id' );
if ( isset( $pricings[ $record_count ] ) ) {
return $pricings[ $record_count ];
}
// If the site is connected, request pricing with the blog token
if ( $blog_id ) {
$endpoint = sprintf( '/jetpack-search/pricing?record_count=%1$d&locale=%2$s', $record_count, get_user_locale() );
// If available in the user data, set the user's currency as one of the params
if ( $connection->is_user_connected() ) {
$user_details = $connection->get_connected_user_data();
if ( ! empty( $user_details['user_currency'] ) && $user_details['user_currency'] !== 'USD' ) {
$endpoint .= sprintf( '&currency=%s', $user_details['user_currency'] );
}
}
$response = Client::wpcom_json_api_request_as_blog(
$endpoint,
'2',
array( 'timeout' => 5 ),
null,
'wpcom'
);
} else {
$response = wp_remote_get(
sprintf( Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ) . '/wpcom/v2/jetpack-search/pricing?record_count=%1$d&locale=%2$s', $record_count, get_user_locale() ),
array( 'timeout' => 5 )
);
}
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
// Cache the failure too: get_pricing_for_ui() reaches this twice per request
// (once via has_trial_support(), once directly), and each miss is a 5s timeout.
$pricings[ $record_count ] = new WP_Error( 'search_pricing_fetch_failed' );
return $pricings[ $record_count ];
}
$body = wp_remote_retrieve_body( $response );
$pricings[ $record_count ] = json_decode( $body, true );
return $pricings[ $record_count ];
}
/**
* Checks whether the product supports trial or not
*
* Returns true if it supports. Return false otherwise.
*
* Free products will always return false.
*
* @return boolean
*/
public static function has_trial_support() {
return static::is_new_pricing_202208();
}
/**
* 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_search',
'jetpack_search_monthly',
'jetpack_search_bi_yearly',
);
}
/**
* Checks if the site purchases contain a free search plan
*
* @return bool
*/
public static function has_free_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_search_free' ) ) {
return true;
}
}
}
return false;
}
/**
* Activates the product. Try to enable instant search after the Search module was enabled.
*
* @param bool|WP_Error $product_activation 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( $product_activation ) {
$product_activation = parent::do_product_specific_activation( $product_activation );
if ( is_wp_error( $product_activation ) ) {
return $product_activation;
}
if ( class_exists( 'Automattic\Jetpack\Search\Module_Control' ) ) {
( new Search_Module_Control() )->enable_instant_search();
}
// we don't want to change the success of the activation if we fail to activate instant search. That's not mandatory.
return $product_activation;
}
/**
* Get the URL the user is taken after activating the product
*
* @return ?string
*/
public static function get_post_activation_url() {
return ''; // stay in My Jetpack page or continue the purchase flow if needed.
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
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' );
}
}
@@ -0,0 +1,233 @@
<?php
/**
* Security product
*
* @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;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Security product
*/
class Security extends Module_Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'security';
/**
* The Jetpack module name
*
* @var string
*/
public static $module_name = 'security';
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Security Bundle';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Security';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Comprehensive site security, including VaultPress Backup, Scan, and Akismet Anti-spam.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Comprehensive site security, including VaultPress Backup, Scan, and Akismet Anti-spam.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Security features list
*/
public static function get_features() {
return array(
_x( 'Real-time cloud backups with 10GB storage', 'Security Product Feature', 'jetpack-my-jetpack' ),
_x( 'Automated real-time malware scan', 'Security Product Feature', 'jetpack-my-jetpack' ),
_x( 'One-click fixes for most threats', 'Security Product Feature', 'jetpack-my-jetpack' ),
_x( 'Comment & form spam protection', 'Security 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_security_t1_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 bool|WP_Error $current_result Is the result of the top level activation actions. You probably won't do anything if it is an WP_Error.
* @return boolean|\WP_Error
*/
public static function do_product_specific_activation( $current_result ) {
$product_activation = parent::do_product_specific_activation( $current_result );
if ( is_wp_error( $product_activation ) && 'module_activation_failed' === $product_activation->get_error_code() ) {
// 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.
$product_activation = true;
}
// At this point, Jetpack plugin is installed. Let's activate each individual product.
$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;
}
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();
}
/**
* 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
*
* @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_security' ) ||
str_starts_with( $purchase->product_slug, 'jetpack_complete' )
) {
return true;
}
}
}
return false;
}
/**
* 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( 'backup', 'scan', 'anti-spam' );
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return '';
}
}
@@ -0,0 +1,190 @@
<?php
/**
* Feature: Site Accelerator
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Module_Product;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* 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 );
}
}
}
@@ -0,0 +1,239 @@
<?php
/**
* Jetpack Social product
*
* @package my-jetpack
*/
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;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Social product
*/
class Social extends Hybrid_Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'social';
/**
* The Jetpack module name
*
* @var string
*/
public static $module_name = 'publicize';
/**
* The slug of the plugin associated with this product.
*
* @var string
*/
public static $plugin_slug = 'jetpack-social';
/**
* The category of the product
*
* @var string
*/
public static $category = 'growth';
/**
* Social has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* The filename (id) of the plugin associated with this product.
*
* @var string
*/
public static $plugin_filename = array(
'jetpack-social/jetpack-social.php',
'social/jetpack-social.php',
'jetpack-social-dev/jetpack-social.php',
);
/**
* Whether this product has a free offering
*
* @var bool
*/
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
*
* @return string
*/
public static function get_name() {
return 'Social';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Social';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Autoshare your posts to social networks and track engagement in one place.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Grow your following by sharing your content across social media automatically.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Social features list
*/
public static function get_features() {
return array(
__( 'Post to social networks', 'jetpack-my-jetpack' ),
__( 'Schedule publishing', 'jetpack-my-jetpack' ),
__( 'Supports the major social networks', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product pricing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => static::get_wpcom_product_slug(),
),
Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() )
);
}
/**
* Get the URL the user is taken after purchasing the product through the checkout
*
* @return ?string
*/
public static function get_post_checkout_url() {
return self::get_manage_url();
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return string
*/
public static function get_wpcom_product_slug() {
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() {
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() && static::does_site_have_feature( 'republicize' ) ) {
return true;
}
return false;
}
/**
* Get the URL where the user manages the product.
*
* @return string
*/
public static function get_manage_url() {
return admin_url( 'admin.php?page=jetpack-social' );
}
/**
* 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' );
}
}
@@ -0,0 +1,220 @@
<?php
/**
* Starter 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;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Starter plan
*/
class Starter extends Module_Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'starter';
/**
* The Jetpack module name
*
* @var string
*/
public static $module_name = 'starter';
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Starter';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Starter';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Essential security tools: real-time backups and comment spam protection.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Essential security tools: real-time backups and comment spam protection.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Starter features list
*/
public static function get_features() {
return array(
_x( 'Real-time cloud backups with 1GB storage', 'Starter Product Feature', 'jetpack-my-jetpack' ),
_x( 'One-click fixes for most threats', 'Starter Product Feature', 'jetpack-my-jetpack' ),
_x( 'Comment & form spam protection', 'Starter Product Feature', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => static::get_wpcom_product_slug(),
),
Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() )
);
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_starter_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 bool|WP_Error $current_result Is the result of the top level activation actions. You probably won't do anything if it is an WP_Error.
* @return boolean|\WP_Error
*/
public static function do_product_specific_activation( $current_result ) {
$product_activation = parent::do_product_specific_activation( $current_result );
if ( is_wp_error( $product_activation ) && 'module_activation_failed' === $product_activation->get_error_code() ) {
// 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.
$product_activation = true;
}
// At this point, Jetpack plugin is installed. Let's activate each individual product.
$activation = Anti_Spam::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
$activation = Backup::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
return $activation;
}
/**
* Checks whether the Product is active
*
* Jetpack Starter 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_starter' ) ) {
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_starter_yearly',
'jetpack_starter_monthly',
);
}
/**
* 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( 'backup', 'anti-spam' );
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return '';
}
}
@@ -0,0 +1,338 @@
<?php
/**
* Jetpack Stats product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Initializer;
use Automattic\Jetpack\My_Jetpack\Module_Product;
use Automattic\Jetpack\My_jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use Automattic\Jetpack\Status\Host;
use Jetpack_Options;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Jetpack Stats product
*/
class Stats extends Module_Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'stats';
/**
* The Jetpack module name associated with this product
*
* @var string|null
*/
public static $module_name = 'stats';
/**
* The category of the product
*
* @var string
*/
public static $category = 'growth';
/**
* The Plugin slug associated with stats
*
* @var string|null
*/
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;
/**
* Stats only requires site connection, not user connection
*
* @var bool
*/
public static $requires_user_connection = false;
/**
* Stats does not have a standalone plugin (yet?)
*
* @var bool
*/
public static $has_standalone_plugin = false;
/**
* Whether this product has a free offering
*
* @var bool
*/
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
*
* @return string
*/
public static function get_name() {
return 'Stats';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Stats';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Clear, concise, and actionable analysis of your site performance.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'With Jetpack Stats, you dont need to be a data scientist to see how your site is performing, understand your visitors, and grow your site.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array CRM features list
*/
public static function get_features() {
return array(
__( 'Real-time data on visitors', 'jetpack-my-jetpack' ),
__( 'Traffic stats and trends for post and pages', 'jetpack-my-jetpack' ),
__( 'Detailed statistics about links leading to your site', 'jetpack-my-jetpack' ),
__( 'GDPR compliant', 'jetpack-my-jetpack' ),
__( 'Access to upcoming advanced features', 'jetpack-my-jetpack' ),
__( 'Priority support', 'jetpack-my-jetpack' ),
__( 'Commercial use', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product pricing details
* Only showing the pricing details for the commercial product
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => static::get_wpcom_product_slug(),
),
Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() )
);
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_stats_yearly';
}
/**
* Get the WPCOM Pay Whatever You Want product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_pwyw_product_slug() {
return 'jetpack_stats_pwyw_yearly';
}
/**
* Get the WPCOM free product slug
*
* @return ?string
*/
public static function get_wpcom_free_product_slug() {
return 'jetpack_stats_free_yearly';
}
/**
* Gets the 'status' of the Stats product
*
* @return string
*/
public static function get_status() {
$status = parent::get_status();
if ( Products::STATUS_MODULE_DISABLED === $status && ! Initializer::is_registered() ) {
// If the site has never been connected before, show the "Learn more" CTA,
// that points to the add Stats product interstitial.
$status = Products::STATUS_NEEDS_FIRST_SITE_CONNECTION;
}
return $status;
}
/**
* Checks whether the product can be upgraded to a different product.
* Stats Commercial plan (highest tier) & Jetpack Complete are not upgradable.
* Also we don't push PWYW users to upgrade.
*
* @return boolean
*/
public static function is_upgradable() {
// For now, atomic sites with stats are not in a position to upgrade
if ( ( new Host() )->is_woa_site() ) {
return false;
}
$purchases_data = Wpcom_Products::get_site_current_purchases();
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' ) ||
str_starts_with( $purchase->product_slug, 'jetpack_growth' )
) {
return false;
} elseif (
// Stats commercial purchased with highest tier cannot be upgraded.
in_array(
$purchase->product_slug,
array( 'jetpack_stats_yearly', 'jetpack_stats_monthly', 'jetpack_stats_bi_yearly' ),
true
) && $purchase->current_price_tier_slug === 'more_than_1m_views'
) {
return false;
} elseif (
// If user already has Stats PWYW, we won't push them to upgrade.
$purchase->product_slug === 'jetpack_stats_pwyw_yearly'
) {
return false;
}
}
}
return true;
}
/**
* 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_stats_yearly',
'jetpack_stats_monthly',
'jetpack_stats_bi_yearly',
'jetpack_stats_pwyw_yearly',
);
}
/**
* Returns a productType parameter for an upgrade URL, determining whether
* to show the PWYW upgrade interstitial or commercial upgrade interstitial.
*
* @return string
*/
public static function get_url_product_type() {
$purchases_data = Wpcom_Products::get_site_current_purchases();
$is_commercial_site = Initializer::is_commercial_site();
if ( is_wp_error( $purchases_data ) ) {
return $is_commercial_site ? '&productType=commercial' : '';
}
if ( $is_commercial_site ) {
return '&productType=commercial';
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
if (
str_starts_with( $purchase->product_slug, static::get_wpcom_free_product_slug() )
) {
return '&productType=personal';
} elseif (
in_array(
$purchase->product_slug,
array( 'jetpack_stats_yearly', 'jetpack_stats_monthly', 'jetpack_stats_bi_yearly' ),
true
) &&
! empty( $purchase->current_price_tier_slug )
) {
return '&productType=commercial';
}
}
}
return '';
}
/**
* Checks whether the product supports trial or not.
* Since Jetpack Stats has been widely available as a free product in the past, it "supports" a trial.
*
* @return boolean
*/
public static function has_trial_support() {
return true;
}
/**
* Get the WordPress.com URL for purchasing Jetpack Stats for the current site.
*
* @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',
admin_url( 'admin.php?page=stats' ),
Jetpack_Options::get_option( 'id' ),
static::get_url_product_type(),
rawurlencode( 'admin.php?page=stats' )
);
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
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' );
}
}
@@ -0,0 +1,319 @@
<?php
/**
* VideoPress product
*
* @package my-jetpack
*/
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;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* 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
*
* @var string
*/
public static $slug = 'videopress';
/**
* The Jetpack module name
*
* @var string
*/
public static $module_name = 'videopress';
/**
* The slug of the plugin associated with this product.
*
* @var string
*/
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.
*
* @var string
*/
public static $plugin_filename = array(
'jetpack-videopress/jetpack-videopress.php',
'videopress/jetpack-videopress.php',
'jetpack-videopress-dev/jetpack-videopress.php',
);
/**
* Search only requires site connection
*
* @var boolean
*/
public static $requires_user_connection = true;
/**
* VideoPress has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* Whether this product has a free offering
*
* @var bool
*/
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
*
* @return string
*/
public static function get_name() {
return 'VideoPress';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack VideoPress';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Powerful and flexible video hosting.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Stunning-quality, ad-free video in the WordPress Editor', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Boost features list
*/
public static function get_features() {
return array(
_x( '1TB of storage', 'VideoPress Product Feature', 'jetpack-my-jetpack' ),
_x( 'Built into WordPress editor', 'VideoPress Product Feature', 'jetpack-my-jetpack' ),
_x( 'Ad-free and customizable player', 'VideoPress Product Feature', 'jetpack-my-jetpack' ),
_x( 'Unlimited users', 'VideoPress Product Feature', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => static::get_wpcom_product_slug(),
),
Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() )
);
}
/**
* Get the URL the user is taken after purchasing the product through the checkout
*
* @return ?string
*/
public static function get_post_checkout_url() {
return self::get_manage_url();
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_videopress';
}
/**
* Get the URL the user is taken after activating the product
*
* @return ?string
*/
public static function get_post_activation_url() {
return ''; // stay in My Jetpack page.
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
if ( method_exists( 'Automattic\Jetpack\VideoPress\Initializer', 'should_initialize_admin_ui' ) && \Automattic\Jetpack\VideoPress\Initializer::should_initialize_admin_ui() ) {
return \Automattic\Jetpack\VideoPress\Admin_UI::get_admin_page_url();
} else {
return admin_url( 'admin.php?page=jetpack#/settings?term=videopress' );
}
}
/**
* 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_videopress',
'jetpack_videopress_monthly',
'jetpack_videopress_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' );
}
/**
* 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,
);
}
$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 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 );
}
}