updated plugin Jetpack Protect version 3.0.2

This commit is contained in:
2024-10-09 12:44:31 +00:00
committed by Gitium
parent a35dc419bc
commit f970470c59
283 changed files with 6970 additions and 2338 deletions

View File

@ -51,7 +51,7 @@ class Activitylog {
'manage_options',
esc_url( Redirect::get_url( 'cloud-activity-log-wp-menu', $args ) ),
null,
1
8
);
}
}

View File

@ -16,14 +16,19 @@ 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\Protect_Status\Status as Protect_Status;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Status\Host as Status_Host;
use Automattic\Jetpack\Sync\Functions as Sync_Functions;
use Automattic\Jetpack\Terms_Of_Service;
use Automattic\Jetpack\Tracking;
use Automattic\Jetpack\VideoPress\Stats as VideoPress_Stats;
use Automattic\Jetpack\Waf\Waf_Runner;
use Jetpack;
use WP_Error;
@ -37,7 +42,7 @@ class Initializer {
*
* @var string
*/
const PACKAGE_VERSION = '4.24.1';
const PACKAGE_VERSION = '4.35.3';
/**
* HTML container ID for the IDC screen on My Jetpack page.
@ -55,9 +60,11 @@ class Initializer {
'jetpack-search',
);
const MY_JETPACK_SITE_INFO_TRANSIENT_KEY = 'my-jetpack-site-info';
const MISSING_SITE_CONNECTION_NOTIFICATION_KEY = 'missing-site-connection';
const MY_JETPACK_SITE_INFO_TRANSIENT_KEY = 'my-jetpack-site-info';
const UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY = 'update-historically-active-jetpack-modules';
const MISSING_CONNECTION_NOTIFICATION_KEY = 'missing-connection';
const VIDEOPRESS_STATS_KEY = 'my-jetpack-videopress-stats';
const VIDEOPRESS_PERIOD_KEY = 'my-jetpack-videopress-period';
/**
* Holds info/data about the site (from the /sites/%d endpoint)
@ -102,9 +109,13 @@ class Initializer {
);
add_action( 'load-' . $page_suffix, array( __CLASS__, 'admin_init' ) );
add_action( 'admin_init', array( __CLASS__, 'setup_historically_active_jetpack_modules_sync' ) );
// This is later than the admin-ui package, which runs on 1000
add_action( 'admin_init', array( __CLASS__, 'maybe_show_red_bubble' ), 1001 );
//  Set up the ExPlat package endpoints
ExPlat::init();
// Sets up JITMS.
JITM::configure();
@ -206,6 +217,13 @@ class Initializer {
$previous_score = $speed_score_history->latest( 1 );
}
$latest_score['previousScores'] = $previous_score['scores'] ?? array();
$scan_data = Protect_Status::get_status();
self::update_historically_active_jetpack_modules();
$waf_config = array();
if ( class_exists( 'Automattic\Jetpack\Waf\Waf_Runner' ) ) {
$waf_config = Waf_Runner::get_config();
}
wp_localize_script(
'my_jetpack_main_app',
@ -218,6 +236,7 @@ class Initializer {
'items' => array(),
),
'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(),
@ -233,13 +252,21 @@ class Initializer {
'userIsAdmin' => current_user_can( 'manage_options' ),
'userIsNewToJetpack' => self::is_jetpack_user_new(),
'lifecycleStats' => array(
'jetpackPlugins' => self::get_installed_jetpack_plugins(),
'isSiteConnected' => $connection->is_connected(),
'isUserConnected' => $connection->is_user_connected(),
'purchases' => self::get_purchases(),
'modules' => self::get_active_modules(),
'jetpackPlugins' => self::get_installed_jetpack_plugins(),
'historicallyActiveModules' => \Jetpack_Options::get_option( 'historically_active_modules', array() ),
'ownedProducts' => Products::get_products_by_ownership( 'owned' ),
'unownedProducts' => Products::get_products_by_ownership( 'unowned' ),
'brokenModules' => self::check_for_broken_modules(),
'isSiteConnected' => $connection->is_connected(),
'isUserConnected' => $connection->is_user_connected(),
'purchases' => self::get_purchases(),
'modules' => self::get_active_modules(),
),
'redBubbleAlerts' => self::get_red_bubble_alerts(),
'recommendedModules' => array(
'modules' => self::get_recommended_modules(),
'dismissed' => \Jetpack_Options::get_option( 'dismissed_recommendations', false ),
),
'isStatsModuleActive' => $modules->is_active( 'stats' ),
'isUserFromKnownHost' => self::is_user_from_known_host(),
'isCommercial' => self::is_commercial_site(),
@ -249,6 +276,14 @@ class Initializer {
'isAgencyAccount' => Jetpack_Manage::is_agency_account(),
),
'latestBoostSpeedScores' => $latest_score,
'protect' => array(
'scanData' => $scan_data,
'wafConfig' => array_merge(
$waf_config,
array( 'blocked_logins' => (int) get_site_option( 'jetpack_protect_blocked_attempts', 0 ) )
),
),
'videopress' => self::get_videopress_stats(),
)
);
@ -270,6 +305,67 @@ class Initializer {
}
}
/**
* Get stats for VideoPress
*
* @return array|WP_Error
*/
public static function get_videopress_stats() {
$video_count = array_sum( (array) wp_count_attachments( 'video' ) );
if ( ! class_exists( 'Automattic\Jetpack\VideoPress\Stats' ) ) {
return array(
'videoCount' => $video_count,
);
}
$featured_stats = get_transient( self::VIDEOPRESS_STATS_KEY );
if ( $featured_stats ) {
return array(
'featuredStats' => $featured_stats,
'videoCount' => $video_count,
);
}
$stats_period = get_transient( self::VIDEOPRESS_PERIOD_KEY );
$videopress_stats = new VideoPress_Stats();
// If the stats period exists, retrieve that information without checking the view count.
// If it does not, check the view count of monthly stats and determine if we want to show yearly or monthly stats.
if ( $stats_period ) {
if ( $stats_period === 'day' ) {
$featured_stats = $videopress_stats->get_featured_stats( 60, 'day' );
} else {
$featured_stats = $videopress_stats->get_featured_stats( 2, 'year' );
}
} else {
$featured_stats = $videopress_stats->get_featured_stats( 60, 'day' );
if (
! is_wp_error( $featured_stats ) &&
$featured_stats &&
( $featured_stats['data']['views']['current'] < 500 || $featured_stats['data']['views']['previous'] < 500 )
) {
$featured_stats = $videopress_stats->get_featured_stats( 2, 'year' );
}
}
if ( is_wp_error( $featured_stats ) || ! $featured_stats ) {
return array(
'videoCount' => $video_count,
);
}
set_transient( self::VIDEOPRESS_PERIOD_KEY, $featured_stats['period'], WEEK_IN_SECONDS );
set_transient( self::VIDEOPRESS_STATS_KEY, $featured_stats, DAY_IN_SECONDS );
return array(
'featuredStats' => $featured_stats,
'videoCount' => $video_count,
);
}
/**
* Get product slugs of the active purchases
*
@ -285,7 +381,7 @@ class Initializer {
function ( $purchase ) {
return $purchase->product_slug;
},
$purchases
(array) $purchases
);
}
@ -324,7 +420,7 @@ class Initializer {
if ( class_exists( 'Jetpack' ) && ! empty( $active_modules ) ) {
$active_modules = array_diff( $active_modules, Jetpack::get_default_modules() );
}
return $active_modules;
return array_values( $active_modules );
}
/**
@ -345,12 +441,7 @@ class Initializer {
// TODO: add a data point for the last known connection/ disconnection time
// are any modules active?
$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() );
}
$active_modules = self::get_active_modules();
if ( ! empty( $active_modules ) ) {
return false;
}
@ -427,6 +518,7 @@ class Initializer {
new REST_Zendesk_Chat();
new REST_Product_Data();
new REST_AI();
new REST_Recommendations_Evaluation();
register_rest_route(
'my-jetpack/v1',
@ -486,6 +578,85 @@ class Initializer {
return apply_filters( 'jetpack_my_jetpack_should_initialize', $should );
}
/**
* Set transient to queue an update to the historically active Jetpack modules on the next wp-admin load
*
* @param string $plugin The plugin that triggered the update. This will be present if the function was queued by a plugin activation.
*
* @return void
*/
public static function queue_historically_active_jetpack_modules_update( $plugin = null ) {
$plugin_filenames = Products::get_all_plugin_filenames();
if ( ! $plugin || in_array( $plugin, $plugin_filenames, true ) ) {
set_transient( self::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY, true );
}
}
/**
* Hook into several connection-based actions to update the historically active Jetpack modules
* If the transient that indicates the list needs to be synced, update it and delete the transient
*
* @return void
*/
public static function setup_historically_active_jetpack_modules_sync() {
if ( get_transient( self::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY ) && ! wp_doing_ajax() ) {
self::update_historically_active_jetpack_modules();
delete_transient( self::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY );
}
$actions = array(
'jetpack_site_registered',
'jetpack_user_authorized',
'activated_plugin',
);
foreach ( $actions as $action ) {
add_action( $action, array( __CLASS__, 'queue_historically_active_jetpack_modules_update' ), 5 );
}
// Modules are often updated async, so we need to update them right away as there will sometimes be no page reload.
add_action( 'jetpack_activate_module', array( __CLASS__, 'update_historically_active_jetpack_modules' ), 5 );
}
/**
* Update historically active Jetpack plugins
* Historically active is defined as the Jetpack plugins that are installed and active with the required connections
* This array will consist of any plugins that were active at one point in time and are still enabled on the site
*
* @return void
*/
public static function update_historically_active_jetpack_modules() {
$historically_active_modules = \Jetpack_Options::get_option( 'historically_active_modules', array() );
$products = Products::get_products();
foreach ( $products as $product ) {
$status = $product['status'];
$product_slug = $product['slug'];
// We want to leave modules in the array if they've been active in the past
// and were not manually disabled by the user.
if ( in_array( $status, Products::$broken_module_statuses, true ) ) {
continue;
}
// If the module is active and not already in the array, add it
if (
in_array( $status, Products::$active_module_statuses, true ) &&
! in_array( $product_slug, $historically_active_modules, true )
) {
$historically_active_modules[] = $product_slug;
}
// If the module has been disabled due to a manual user action,
// or because of a missing plan error, remove it from the array
if ( in_array( $status, Products::$disabled_module_statuses, true ) ) {
$historically_active_modules = array_values( array_diff( $historically_active_modules, array( $product_slug ) ) );
}
}
\Jetpack_Options::update_option( 'historically_active_modules', array_unique( $historically_active_modules ) );
}
/**
* Site full-data endpoint.
*
@ -503,7 +674,7 @@ class Initializer {
return new WP_Error( 'site_data_fetch_failed', 'Site data fetch failed', array( 'status' => $response_code ) );
}
return rest_ensure_response( $body, 200 );
return rest_ensure_response( $body );
}
/**
@ -537,7 +708,7 @@ class Initializer {
/**
* Returns whether a site has been determined "commercial" or not.
*
* @return bool
* @return bool|null
*/
public static function is_commercial_site() {
if ( is_wp_error( self::$site_info ) ) {
@ -563,13 +734,13 @@ class Initializer {
*/
public static function dismiss_welcome_banner() {
\Jetpack_Options::update_option( 'dismissed_welcome_banner', true );
return rest_ensure_response( array( 'success' => true ), 200 );
return rest_ensure_response( array( 'success' => true ) );
}
/**
* Returns true if the site has file write access to the plugins folder, false otherwise.
*
* @return bool
* @return string
**/
public static function has_file_system_write_access() {
@ -625,7 +796,13 @@ class Initializer {
global $menu;
// filters for the items in this file
add_filter( 'my_jetpack_red_bubble_notification_slugs', array( __CLASS__, 'add_red_bubble_alerts' ) );
$red_bubble_alerts = self::get_red_bubble_alerts();
$red_bubble_alerts = array_filter(
self::get_red_bubble_alerts(),
function ( $alert ) {
// We don't want to show silent alerts
return empty( $alert['is_silent'] );
}
);
// The Jetpack menu item should be on index 3
if (
@ -657,6 +834,63 @@ class Initializer {
return $red_bubble_alerts;
}
/**
* 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
}
/**
* Check for features broken by a disconnected user or site
*
* @return array
*/
public static function check_for_broken_modules() {
$connection = new Connection_Manager();
$is_user_connected = $connection->is_user_connected() || $connection->has_connected_owner();
$is_site_connected = $connection->is_connected();
$broken_modules = array(
'needs_site_connection' => array(),
'needs_user_connection' => array(),
);
if ( $is_user_connected && $is_site_connected ) {
return $broken_modules;
}
$products = Products::get_products_classes();
$historically_active_modules = \Jetpack_Options::get_option( 'historically_active_modules', array() );
foreach ( $products as $product ) {
if ( ! in_array( $product::$slug, $historically_active_modules, true ) ) {
continue;
}
if ( $product::$requires_user_connection && ! $is_user_connected ) {
if ( ! in_array( $product::$slug, $broken_modules['needs_user_connection'], true ) ) {
$broken_modules['needs_user_connection'][] = $product::$slug;
}
} elseif ( ! $is_site_connected ) {
if ( ! in_array( $product::$slug, $broken_modules['needs_site_connection'], true ) ) {
$broken_modules['needs_site_connection'][] = $product::$slug;
}
}
}
return $broken_modules;
}
/**
* Add relevant red bubble notifications
*
@ -664,12 +898,18 @@ class Initializer {
* @return array
*/
public static function add_red_bubble_alerts( array $red_bubble_slugs ) {
if ( wp_doing_ajax() ) {
return array();
}
$connection = new Connection_Manager();
$welcome_banner_dismissed = \Jetpack_Options::get_option( 'dismissed_welcome_banner', false );
if ( self::is_jetpack_user_new() && ! $welcome_banner_dismissed ) {
$red_bubble_slugs['welcome-banner-active'] = null;
$red_bubble_slugs['welcome-banner-active'] = array(
'is_silent' => $connection->is_connected(), // we don't display the red bubble if the user is connected
);
return $red_bubble_slugs;
} else {
return self::alert_if_missing_site_connection( $red_bubble_slugs );
return self::alert_if_missing_connection( $red_bubble_slugs );
}
}
@ -679,9 +919,40 @@ class Initializer {
* @param array $red_bubble_slugs - slugs that describe the reasons the red bubble is showing.
* @return array
*/
public static function alert_if_missing_site_connection( array $red_bubble_slugs ) {
if ( ! ( new Connection_Manager() )->is_connected() ) {
$red_bubble_slugs[ self::MISSING_SITE_CONNECTION_NOTIFICATION_KEY ] = null;
public static function alert_if_missing_connection( array $red_bubble_slugs ) {
$broken_modules = self::check_for_broken_modules();
$connection = new Connection_Manager();
if ( ! empty( $broken_modules['needs_user_connection'] ) ) {
$red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
'type' => 'user',
'is_error' => true,
);
return $red_bubble_slugs;
}
if ( ! empty( $broken_modules['needs_site_connection'] ) ) {
$red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
'type' => 'site',
'is_error' => true,
);
return $red_bubble_slugs;
}
if ( ! $connection->is_user_connected() && ! $connection->has_connected_owner() ) {
$red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
'type' => 'user',
'is_error' => false,
);
return $red_bubble_slugs;
}
if ( ! $connection->is_connected() ) {
$red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
'type' => 'site',
'is_error' => false,
);
return $red_bubble_slugs;
}
return $red_bubble_slugs;

View File

@ -49,7 +49,7 @@ class Jetpack_Manage {
'manage_options',
esc_url( Redirect::get_url( 'cloud-manage-dashboard-wp-menu', $args ) ),
null,
100
15
);
}

View File

@ -11,6 +11,89 @@ 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
*/
const STATUS_SITE_CONNECTION_ERROR = 'site_connection_error';
const STATUS_USER_CONNECTION_ERROR = 'user_connection_error';
const STATUS_ACTIVE = 'active';
const STATUS_CAN_UPGRADE = 'can_upgrade';
const STATUS_INACTIVE = 'inactive';
const STATUS_MODULE_DISABLED = 'module_disabled';
const STATUS_PLUGIN_ABSENT = 'plugin_absent';
const STATUS_PLUGIN_ABSENT_WITH_PLAN = 'plugin_absent_with_plan';
const STATUS_NEEDS_PLAN = 'needs_plan';
const STATUS_NEEDS_ACTIVATION = 'needs_activation';
const STATUS_NEEDS_FIRST_SITE_CONNECTION = 'needs_first_site_connection';
/**
* 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,
);
/**
* 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 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_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,
);
/**
* Get the list of Products classes
@ -80,6 +163,84 @@ class Products {
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
*
@ -156,7 +317,7 @@ class Products {
'status' => array(
'title' => 'The product status',
'type' => 'string',
'enum' => array( 'active', 'inactive', 'plugin_absent', 'needs_purchase', 'needs_purchase_or_free', 'needs_first_site_connection', 'user_connection_error', 'site_connection_error' ),
'enum' => self::$all_statuses,
),
'class' => array(
'title' => 'The product class handler',

View File

@ -107,14 +107,14 @@ class REST_Product_Data {
}
}
return rest_ensure_response( $undo_event, 200 );
return rest_ensure_response( $undo_event );
}
/**
* This will collect a count of all the items that could be backed up
* This is used to show what backup could be doing if it is not enabled
*
* @return array
* @return WP_Error|\WP_REST_Response
*/
public static function count_things_that_can_be_backed_up() {
$image_mime_type = 'image';
@ -142,6 +142,6 @@ class REST_Product_Data {
// Add all audio attachments together to get the total audio count
$data['total_audio_count'] = array_sum( (array) wp_count_attachments( $audio_mime_type ) );
return rest_ensure_response( $data, 200 );
return rest_ensure_response( $data );
}
}

View File

@ -0,0 +1,149 @@
<?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.
*/
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::delete_option( 'dismissed_welcome_banner' );
}
return rest_ensure_response( array() );
}
}

View File

@ -8,6 +8,7 @@
namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Status\Visitor;
use Jetpack_Options;
use WP_Error;
@ -30,25 +31,48 @@ class Wpcom_Products {
*/
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() {
$blog_id = \Jetpack_Options::get_option( 'id' );
$ip = ( new Visitor() )->get_ip( true );
$headers = array(
$connection = new Connection_Manager();
$blog_id = \Jetpack_Options::get_option( 'id' );
$ip = ( new Visitor() )->get_ip( true );
$headers = array(
'X-Forwarded-For' => $ip,
);
// If has a blog id, use connected endpoint.
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',
@ -58,6 +82,12 @@ class Wpcom_Products {
)
);
} 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(
@ -73,14 +103,47 @@ class Wpcom_Products {
if ( 200 === $response_code ) {
return json_decode( wp_remote_retrieve_body( $wpcom_request ) );
} else {
return new WP_Error(
$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
*
@ -92,6 +155,7 @@ class Wpcom_Products {
*/
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 );
}
@ -102,8 +166,15 @@ class Wpcom_Products {
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 > ( 7 * DAY_IN_SECONDS );
return time() - (int) $cache_date > DAY_IN_SECONDS;
}
/**
@ -250,6 +321,11 @@ class Wpcom_Products {
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(
@ -260,7 +336,9 @@ class Wpcom_Products {
)
);
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return new WP_Error( 'purchases_state_fetch_failed' );
$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 );
@ -270,4 +348,40 @@ class Wpcom_Products {
return $purchases;
}
/**
* 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;
}
}

View File

@ -50,6 +50,13 @@ class Anti_Spam extends Product {
*/
public static $has_free_offering = true;
/**
* Akismet has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* Get the product name
*
@ -74,7 +81,7 @@ class Anti_Spam extends Product {
* @return string
*/
public static function get_description() {
return __( 'Stop comment and form spam', 'jetpack-my-jetpack' );
return __( 'Keep your site free from spam and bots', 'jetpack-my-jetpack' );
}
/**
@ -106,6 +113,14 @@ class Anti_Spam extends Product {
* @return bool - whether an API key was found
*/
public static function has_paid_plan_for_product() {
$products_with_anti_spam = array(
'jetpack_anti_spam',
'jetpack_complete',
'jetpack_security',
'jetpack_personal',
'jetpack_premium',
'jetpack_business',
);
// Check if the site has an API key for Akismet
$akismet_api_key = apply_filters( 'akismet_get_api_key', defined( 'WPCOM_API_KEY' ) ? constant( 'WPCOM_API_KEY' ) : get_option( 'wordpress_api_key' ) );
$fallback = ! empty( $akismet_api_key );
@ -118,13 +133,10 @@ class Anti_Spam extends Product {
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
// Anti-spam is available as standalone bundle and as part of the Security and Complete plans.
if (
strpos( $purchase->product_slug, 'jetpack_anti_spam' ) !== false ||
str_starts_with( $purchase->product_slug, 'jetpack_complete' ) ||
str_starts_with( $purchase->product_slug, 'jetpack_security' )
) {
return true;
foreach ( $products_with_anti_spam as $product ) {
if ( strpos( $purchase->product_slug, $product ) !== false ) {
return true;
}
}
}
}

View File

@ -93,7 +93,7 @@ class Backup extends Hybrid_Product {
return __( 'Save every change', 'jetpack-my-jetpack' );
}
return __( 'Your site is not backed up', 'jetpack-my-jetpack' );
return __( 'Secure your site with automatic backups and one-click restores', 'jetpack-my-jetpack' );
}
/**
@ -184,7 +184,8 @@ class Backup extends Hybrid_Product {
$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 ) ) {
return new WP_Error( 'rewind_state_fetch_failed' );
$status = new WP_Error( 'rewind_state_fetch_failed' );
return $status;
}
$body = wp_remote_retrieve_body( $response );

View File

@ -44,6 +44,13 @@ class Boost extends Product {
*/
public static $plugin_slug = 'jetpack-boost';
/**
* Boost has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* Whether this product requires a user connection
*
@ -82,7 +89,7 @@ class Boost extends Product {
* @return string
*/
public static function get_description() {
return __( 'Speed up your site in seconds', 'jetpack-my-jetpack' );
return __( 'Speed up your site and improve SEO in seconds', 'jetpack-my-jetpack' );
}
/**
@ -91,7 +98,7 @@ class Boost extends Product {
* @return string
*/
public static function get_long_description() {
return __( 'Jetpack Boost gives your site the same performance advantages as the worlds leading websites, no developer required.', 'jetpack-my-jetpack' );
return __( 'Fast sites get more page visits, more conversions, and better SEO rankings. Boost speeds up your site in seconds.', 'jetpack-my-jetpack' );
}
/**

View File

@ -80,7 +80,7 @@ class Creator extends Product {
* @return string
*/
public static function get_description() {
return __( 'Create, grow, and monetize your audience', 'jetpack-my-jetpack' );
return __( 'Get more subscribers and keep them engaged with our creator tools', 'jetpack-my-jetpack' );
}
/**
@ -89,7 +89,7 @@ class Creator extends Product {
* @return string
*/
public static function get_long_description() {
return __( 'Create, grow, and monetize your audience with powerful tools for creators.', 'jetpack-my-jetpack' );
return __( 'Craft stunning content, boost your subscriber base, and monetize your audience with subscriptions.', 'jetpack-my-jetpack' );
}
/**

View File

@ -53,6 +53,13 @@ class Crm extends Product {
*/
public static $has_free_offering = true;
/**
* CRM has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* Get the product name
*
@ -77,7 +84,7 @@ class Crm extends Product {
* @return string
*/
public static function get_description() {
return __( 'Nurture your contacts to grow your business', 'jetpack-my-jetpack' );
return __( 'Strengthen customer relationships and grow your business', 'jetpack-my-jetpack' );
}
/**
@ -86,7 +93,7 @@ class Crm extends Product {
* @return string
*/
public static function get_long_description() {
return __( 'All of your contacts in one place. Build better relationships with your customers and clients.', 'jetpack-my-jetpack' );
return __( 'Build better relationships with your customers and grow your business.', 'jetpack-my-jetpack' );
}
/**

View File

@ -81,7 +81,6 @@ class Extras extends Product {
*/
public static function get_features() {
return array(
__( 'Measure your impact with beautiful stats', 'jetpack-my-jetpack' ),
__( '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' ),

View File

@ -93,6 +93,10 @@ class Jetpack_Ai extends Product {
* @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,
@ -105,6 +109,10 @@ class Jetpack_Ai extends Product {
* @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' )
@ -194,13 +202,15 @@ class Jetpack_Ai extends Product {
*/
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.
if ( is_wp_error( $info ) ) {
// 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;
}
@ -216,7 +226,7 @@ class Jetpack_Ai extends Product {
* @return string
*/
public static function get_description() {
return __( 'The most powerful AI tool for WordPress', 'jetpack-my-jetpack' );
return __( 'Enhance your writing and productivity with our AI suite', 'jetpack-my-jetpack' );
}
/**
@ -253,14 +263,21 @@ class Jetpack_Ai extends Product {
* @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(
1 => array(
__( 'Artificial intelligence chatbot', 'jetpack-my-jetpack' ),
__( 'Generate text, tables, lists, and forms', 'jetpack-my-jetpack' ),
__( 'Refine the tone and content to your liking', 'jetpack-my-jetpack' ),
__( 'Get feedback about your post', 'jetpack-my-jetpack' ),
__( 'Seamless WordPress editor integration', '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(
@ -274,7 +291,7 @@ class Jetpack_Ai extends Product {
sprintf( __( 'Up to %d requests per month', 'jetpack-my-jetpack' ), $tier ),
);
return isset( $features[ $tier ] ) ? $features[ $tier ] : $tiered_features;
return $is_tier_plan ? $tiered_features : $features;
}
/**
@ -305,11 +322,7 @@ class Jetpack_Ai extends Product {
return array();
}
// get info about the feature.
$info = self::get_ai_assistant_feature();
// flag to indicate if the tiers are enabled, case the info is available.
$tier_plans_enabled = ( ! is_wp_error( $info ) && isset( $info['tier-plans-enabled'] ) ) ? boolval( $info['tier-plans-enabled'] ) : false;
$tier_plans_enabled = self::are_tier_plans_enabled();
/*
* when tiers are enabled and the price tier list is empty,
@ -360,6 +373,18 @@ class Jetpack_Ai extends Product {
* @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
@ -444,8 +469,21 @@ class Jetpack_Ai extends Product {
* @return boolean
*/
public static function is_upgradable() {
$has_ai_feature = static::does_site_have_feature( 'ai-assistant' );
$current_tier = self::get_current_usage_tier();
$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 ) {
@ -461,7 +499,7 @@ class Jetpack_Ai extends Product {
* @return ?string
*/
public static function get_post_checkout_url() {
return '/wp-admin/admin.php?page=my-jetpack#/jetpack-ai';
return 'admin.php?page=my-jetpack#/jetpack-ai';
}
/**
@ -479,7 +517,7 @@ class Jetpack_Ai extends Product {
* @return ?string
*/
public static function get_manage_url() {
return '/wp-admin/admin.php?page=my-jetpack#/add-jetpack-ai';
return '/wp-admin/admin.php?page=my-jetpack#/jetpack-ai';
}
/**
@ -536,6 +574,25 @@ class Jetpack_Ai extends Product {
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.
*

View File

@ -86,8 +86,8 @@ abstract class Module_Product extends Product {
*/
public static function get_status() {
$status = parent::get_status();
if ( 'inactive' === $status && ! static::is_module_active() ) {
$status = 'module_disabled';
if ( Products::STATUS_INACTIVE === $status && ! static::is_module_active() ) {
$status = Products::STATUS_MODULE_DISABLED;
}
return $status;
}

View File

@ -38,7 +38,7 @@ abstract class Product {
*
* @var string|string[]
*/
protected static $plugin_filename = null;
public static $plugin_filename = null;
/**
* The slug of the plugin associated with this product. If not defined, it will default to the Jetpack plugin
@ -150,35 +150,37 @@ abstract class Product {
throw new \Exception( 'Product classes must declare the $slug attribute.' );
}
return array(
'slug' => static::$slug,
'plugin_slug' => static::$plugin_slug,
'name' => static::get_name(),
'title' => static::get_title(),
'description' => static::get_description(),
'long_description' => static::get_long_description(),
'tiers' => static::get_tiers(),
'features' => static::get_features(),
'features_by_tier' => static::get_features_by_tier(),
'disclaimers' => static::get_disclaimers(),
'status' => static::get_status(),
'pricing_for_ui' => static::get_pricing_for_ui(),
'is_bundle' => static::is_bundle_product(),
'is_plugin_active' => static::is_plugin_active(),
'is_upgradable' => static::is_upgradable(),
'is_upgradable_by_bundle' => static::is_upgradable_by_bundle(),
'supported_products' => static::get_supported_products(),
'wpcom_product_slug' => static::get_wpcom_product_slug(),
'requires_user_connection' => static::$requires_user_connection,
'has_any_plan_for_product' => static::has_any_plan_for_product(),
'has_free_plan_for_product' => static::has_free_plan_for_product(),
'has_paid_plan_for_product' => static::has_paid_plan_for_product(),
'has_free_offering' => static::$has_free_offering,
'manage_url' => static::get_manage_url(),
'purchase_url' => static::get_purchase_url(),
'post_activation_url' => static::get_post_activation_url(),
'standalone_plugin_info' => static::get_standalone_info(),
'class' => static::class,
'post_checkout_url' => static::get_post_checkout_url(),
'slug' => static::$slug,
'plugin_slug' => static::$plugin_slug,
'name' => static::get_name(),
'title' => static::get_title(),
'description' => static::get_description(),
'long_description' => static::get_long_description(),
'tiers' => static::get_tiers(),
'features' => static::get_features(),
'features_by_tier' => static::get_features_by_tier(),
'disclaimers' => static::get_disclaimers(),
'status' => static::get_status(),
'pricing_for_ui' => static::get_pricing_for_ui(),
'is_bundle' => static::is_bundle_product(),
'is_plugin_active' => static::is_plugin_active(),
'is_upgradable' => static::is_upgradable(),
'is_upgradable_by_bundle' => static::is_upgradable_by_bundle(),
'supported_products' => static::get_supported_products(),
'wpcom_product_slug' => static::get_wpcom_product_slug(),
'requires_user_connection' => static::$requires_user_connection,
'has_any_plan_for_product' => static::has_any_plan_for_product(),
'has_free_plan_for_product' => static::has_free_plan_for_product(),
'has_paid_plan_for_product' => static::has_paid_plan_for_product(),
'has_free_offering' => static::$has_free_offering,
'manage_url' => static::get_manage_url(),
'purchase_url' => static::get_purchase_url(),
'post_activation_url' => static::get_post_activation_url(),
'post_activation_urls_by_feature' => static::get_manage_urls_by_feature(),
'standalone_plugin_info' => static::get_standalone_info(),
'class' => static::class,
'post_checkout_url' => static::get_post_checkout_url(),
'post_checkout_urls_by_feature' => static::get_post_checkout_urls_by_feature(),
);
}
@ -198,7 +200,8 @@ abstract class Product {
$response = Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d/features', $site_id ), '1.1' );
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return new WP_Error( 'site_features_fetch_failed' );
$features = new WP_Error( 'site_features_fetch_failed' );
return $features;
}
$body = wp_remote_retrieve_body( $response );
@ -305,6 +308,15 @@ abstract class Product {
*/
abstract public static function get_manage_url();
/**
* Get the URL where the user manages the product for each product feature
*
* @return ?array
*/
public static function get_manage_urls_by_feature() {
return null;
}
/**
* Get the URL the user is taken after activating the product
*
@ -323,6 +335,15 @@ abstract class Product {
return null;
}
/**
* 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 null;
}
/**
* Get the WPCOM product slug used to make the purchase
*
@ -432,12 +453,36 @@ abstract class Product {
* return all the products it contains.
* Empty array by default.
*
* @return Array Product slugs
* @return array Product slugs
*/
public static function get_supported_products() {
return array();
}
/**
* Determine if the product is owned or not
* 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
*
* @return boolean
*/
public static function is_owned() {
$historically_active_modules = Jetpack_Options::get_option( 'historically_active_modules', array() );
$standalone_info = static::get_standalone_info();
if ( ( static::is_active() && Jetpack_Options::get_option( 'id' ) ) ||
$standalone_info['is_standalone_installed'] ||
in_array( static::$slug, $historically_active_modules, true ) ||
static::has_any_plan_for_product()
) {
return true;
}
return false;
}
/**
* Undocumented function
*
@ -445,48 +490,48 @@ abstract class Product {
*/
public static function get_status() {
if ( ! static::is_plugin_installed() ) {
$status = 'plugin_absent';
$status = Products::STATUS_PLUGIN_ABSENT;
if ( static::has_paid_plan_for_product() ) {
$status = 'plugin_absent_with_plan';
$status = Products::STATUS_PLUGIN_ABSENT_WITH_PLAN;
}
} elseif ( static::is_active() ) {
$status = 'active';
$status = Products::STATUS_ACTIVE;
// We only consider missing site & user connection an error when the Product is active.
if ( static::$requires_site_connection && ! ( new Connection_Manager() )->is_connected() ) {
// Site has never been connected before
if ( ! \Jetpack_Options::get_option( 'id' ) ) {
$status = 'needs_first_site_connection';
if ( ! Jetpack_Options::get_option( 'id' ) && ! static::is_owned() ) {
$status = Products::STATUS_NEEDS_FIRST_SITE_CONNECTION;
} else {
$status = 'site_connection_error';
$status = Products::STATUS_SITE_CONNECTION_ERROR;
}
} elseif ( static::$requires_user_connection && ! ( new Connection_Manager() )->has_connected_owner() ) {
$status = 'user_connection_error';
$status = Products::STATUS_USER_CONNECTION_ERROR;
} elseif ( static::is_upgradable() ) {
$status = 'can_upgrade';
$status = Products::STATUS_CAN_UPGRADE;
}
// Check specifically for inactive modules, which will prevent a product from being active
} elseif ( static::$module_name && ! static::is_module_active() ) {
$status = 'module_disabled';
$status = Products::STATUS_MODULE_DISABLED;
// If there is not a plan associated with the disabled module, encourage a plan first
// Getting a plan set up should help resolve any connection issues
// However if the standalone plugin for this product is active, then we will defer to showing errors that prevent the module from being active
// This is because if a standalone plugin is installed, we expect the product to not show as "inactive" on My Jetpack
if ( static::$requires_plan || ( ! static::has_any_plan_for_product() && static::$has_standalone_plugin && ! self::is_plugin_active() ) ) {
$status = static::$has_free_offering ? 'needs_purchase_or_free' : 'needs_purchase';
$status = static::is_owned() && static::$has_free_offering && ! static::$requires_plan ? Products::STATUS_NEEDS_ACTIVATION : Products::STATUS_NEEDS_PLAN;
} elseif ( static::$requires_site_connection && ! ( new Connection_Manager() )->is_connected() ) {
// Site has never been connected before
if ( ! \Jetpack_Options::get_option( 'id' ) ) {
$status = 'needs_first_site_connection';
// Site has never been connected before and product is not owned
if ( ! Jetpack_Options::get_option( 'id' ) && ! static::is_owned() ) {
$status = Products::STATUS_NEEDS_FIRST_SITE_CONNECTION;
} else {
$status = 'site_connection_error';
$status = Products::STATUS_SITE_CONNECTION_ERROR;
}
} elseif ( static::$requires_user_connection && ! ( new Connection_Manager() )->has_connected_owner() ) {
$status = 'user_connection_error';
$status = Products::STATUS_USER_CONNECTION_ERROR;
}
} elseif ( ! static::has_any_plan_for_product() ) {
$status = static::$has_free_offering ? 'needs_purchase_or_free' : 'needs_purchase';
$status = static::is_owned() && static::$has_free_offering && ! static::$requires_plan ? Products::STATUS_NEEDS_ACTIVATION : Products::STATUS_NEEDS_PLAN;
} else {
$status = 'inactive';
$status = Products::STATUS_INACTIVE;
}
return $status;
}

View File

@ -22,6 +22,9 @@ class Protect extends Product {
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
*
@ -61,6 +64,13 @@ class Protect extends Product {
*/
public static $has_free_offering = true;
/**
* Protect has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* Get the product name
*
@ -85,7 +95,7 @@ class Protect extends Product {
* @return string
*/
public static function get_description() {
return __( 'Powerful, automated site security', 'jetpack-my-jetpack' );
return __( 'Guard against malware and bad actors 24/7', 'jetpack-my-jetpack' );
}
/**
@ -94,7 +104,7 @@ class Protect extends Product {
* @return string
*/
public static function get_long_description() {
return __( 'Protect your site and scan for security vulnerabilities listed in our database.', 'jetpack-my-jetpack' );
return __( 'Protect your site from bad actors and malware 24/7. Clean up security vulnerabilities with one click.', 'jetpack-my-jetpack' );
}
/**
@ -255,16 +265,42 @@ class Protect extends Product {
}
/**
* Checks if the site has a paid plan for the product
* Checks whether the current plan (or purchases) of the site already supports the product
*
* @return bool
* @return boolean
*/
public static function has_paid_plan_for_product() {
$scan_data = static::get_state_from_wpcom();
if ( is_wp_error( $scan_data ) ) {
$plans_with_scan = array(
'jetpack_scan',
'jetpack_security',
'jetpack_complete',
'jetpack_premium',
'jetpack_business',
);
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
return is_object( $scan_data ) && isset( $scan_data->state ) && 'unavailable' !== $scan_data->state;
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
foreach ( $plans_with_scan as $plan ) {
if ( strpos( $purchase->product_slug, $plan ) !== false ) {
return true;
}
}
}
}
return false;
}
/**
* 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();
}
/**
@ -276,6 +312,18 @@ class Protect extends Product {
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
*
@ -285,6 +333,18 @@ class Protect extends Product {
return admin_url( 'admin.php?page=jetpack-protect' );
}
/**
* 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.
@ -292,6 +352,6 @@ class Protect extends Product {
* @return array Products bundle list.
*/
public static function is_upgradable_by_bundle() {
return array( 'security' );
return array( 'security', 'complete' );
}
}

View File

@ -126,7 +126,8 @@ class Scan extends Module_Product {
$response = Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d/scan', $site_id ) . '?force=wpcom', '2', array( 'timeout' => 2 ), null, 'wpcom' );
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return new WP_Error( 'scan_state_fetch_failed' );
$status = new WP_Error( 'scan_state_fetch_failed' );
return $status;
}
$body = wp_remote_retrieve_body( $response );

View File

@ -8,6 +8,7 @@
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;
@ -103,7 +104,7 @@ class Search extends Hybrid_Product {
* @return string
*/
public static function get_description() {
return __( 'Custom instant site search', 'jetpack-my-jetpack' );
return __( 'Help your visitors find what they are looking for with instant search results', 'jetpack-my-jetpack' );
}
/**
@ -204,7 +205,7 @@ class Search extends Hybrid_Product {
}
/**
* Override status to `needs_purchase_or_free` when status is `needs_purchase`.
* Override status to `needs_activation` when status is `needs_plan`.
*/
public static function get_status() {
$status = parent::get_status();
@ -222,22 +223,33 @@ class Search extends Hybrid_Product {
*/
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 ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
// For simple sites fetch the response directly.
// 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(
sprintf( '/jetpack-search/pricing?record_count=%1$d&locale=%2$s', $record_count, get_user_locale() ),
$endpoint,
'2',
array( 'timeout' => 5 ),
null,
'wpcom'
);
} else {
// For non-simple sites we have to use the wp_remote_get, as connection might not be available.
$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 )

View File

@ -1,6 +1,6 @@
<?php
/**
* Search product
* Jetpack Social product
*
* @package my-jetpack
*/
@ -86,7 +86,7 @@ class Social extends Hybrid_Product {
* @return string
*/
public static function get_description() {
return __( 'Auto-publish to social media', 'jetpack-my-jetpack' );
return __( 'Effortlessly share content across social media. Right from within WordPress', 'jetpack-my-jetpack' );
}
/**
@ -95,7 +95,7 @@ class Social extends Hybrid_Product {
* @return string
*/
public static function get_long_description() {
return __( 'Promote your content on social media by automatically publishing when you publish on your site.', 'jetpack-my-jetpack' );
return __( 'Grow your following by sharing your content across social media automatically.', 'jetpack-my-jetpack' );
}
/**
@ -141,7 +141,7 @@ class Social extends Hybrid_Product {
* @return string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_social_basic_yearly';
return 'jetpack_social_v1_yearly';
}
/**
@ -150,6 +150,13 @@ class Social extends Hybrid_Product {
* @return boolean
*/
public static function has_paid_plan_for_product() {
$plans_with_social = array(
'jetpack_social',
'jetpack_complete',
'jetpack_business',
'jetpack_premium',
'jetpack_personal',
);
// 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() ) {
@ -160,11 +167,13 @@ class Social extends Hybrid_Product {
if ( is_wp_error( $purchases_data ) ) {
return false;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
// Social is available as standalone bundle and as part of the Complete plan.
if ( strpos( $purchase->product_slug, 'jetpack_social' ) !== false || str_starts_with( $purchase->product_slug, 'jetpack_complete' ) ) {
return true;
foreach ( $plans_with_social as $plan ) {
if ( strpos( $purchase->product_slug, $plan ) !== false ) {
return true;
}
}
}
}

View File

@ -9,6 +9,7 @@ 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;
@ -90,7 +91,7 @@ class Stats extends Module_Product {
* @return string
*/
public static function get_description() {
return __( 'Simple, yet powerful analytics', 'jetpack-my-jetpack' );
return __( 'The simplest way to track visitor insights and unlock your sites growth', 'jetpack-my-jetpack' );
}
/**
@ -99,7 +100,7 @@ class Stats extends Module_Product {
* @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.', 'jetpack-my-jetpack' );
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' );
}
/**
@ -169,10 +170,10 @@ class Stats extends Module_Product {
*/
public static function get_status() {
$status = parent::get_status();
if ( 'module_disabled' === $status && ! Initializer::is_registered() ) {
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 = 'needs_purchase_or_free';
$status = Products::STATUS_NEEDS_FIRST_SITE_CONNECTION;
}
return $status;
}

View File

@ -92,7 +92,7 @@ class Videopress extends Hybrid_Product {
* @return string
*/
public static function get_description() {
return __( 'High quality, ad-free video', 'jetpack-my-jetpack' );
return __( 'Stunning-quality, ad-free video in the WordPress Editor', 'jetpack-my-jetpack' );
}
/**
@ -101,7 +101,7 @@ class Videopress extends Hybrid_Product {
* @return string
*/
public static function get_long_description() {
return __( 'High-quality, ad-free video built specifically for WordPress.', 'jetpack-my-jetpack' );
return __( 'Stunning-quality, ad-free video in the WordPress Editor', 'jetpack-my-jetpack' );
}
/**
@ -179,14 +179,22 @@ class Videopress extends Hybrid_Product {
* @return boolean
*/
public static function has_paid_plan_for_product() {
$purchases_data = Wpcom_Products::get_site_current_purchases();
$plans_with_videopress = array(
'jetpack_videopress',
'jetpack_complete',
'jetpack_business',
'jetpack_premium',
);
$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_videopress' ) || str_starts_with( $purchase->product_slug, 'jetpack_complete' ) ) {
return true;
foreach ( $plans_with_videopress as $plan ) {
if ( strpos( $purchase->product_slug, $plan ) !== false ) {
return true;
}
}
}
}