updated plugin Jetpack Protect version 2.2.0

This commit is contained in:
2024-06-27 12:10:57 +00:00
committed by Gitium
parent ec9d8a5834
commit 938cef2946
218 changed files with 7469 additions and 1864 deletions

View File

@ -9,7 +9,6 @@ namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Admin_UI\Admin_Menu;
use Automattic\Jetpack\Assets;
use Automattic\Jetpack\Boost_Speed_Score\Jetpack_Boost_Modules;
use Automattic\Jetpack\Boost_Speed_Score\Speed_Score;
use Automattic\Jetpack\Boost_Speed_Score\Speed_Score_History;
use Automattic\Jetpack\Connection\Client;
@ -26,6 +25,7 @@ use Automattic\Jetpack\Status\Host as Status_Host;
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.
@ -37,7 +37,7 @@ class Initializer {
*
* @var string
*/
const PACKAGE_VERSION = '4.17.0';
const PACKAGE_VERSION = '4.24.1';
/**
* HTML container ID for the IDC screen on My Jetpack page.
@ -62,7 +62,7 @@ class Initializer {
/**
* Holds info/data about the site (from the /sites/%d endpoint)
*
* @var stdClass Object
* @var object
*/
public static $site_info;
@ -87,8 +87,7 @@ class Initializer {
}
// Initialize Boost Speed Score
$boost_modules = Jetpack_Boost_Modules::init();
new Speed_Score( $boost_modules, 'jetpack-my-jetpack' );
new Speed_Score( array(), 'jetpack-my-jetpack' );
// Add custom WP REST API endoints.
add_action( 'rest_api_init', array( __CLASS__, 'register_rest_endpoints' ) );
@ -200,7 +199,14 @@ class Initializer {
);
$modules = new Modules();
$connection = new Connection_Manager();
$speed_score_history = new Speed_Score_History( wp_parse_url( get_site_url(), PHP_URL_HOST ) );
$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();
wp_localize_script(
'my_jetpack_main_app',
'myJetpackInitialState',
@ -216,6 +222,7 @@ class Initializer {
'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(),
@ -237,14 +244,11 @@ class Initializer {
'isUserFromKnownHost' => self::is_user_from_known_host(),
'isCommercial' => self::is_commercial_site(),
'isAtomic' => ( new Status_Host() )->is_woa_site(),
'welcomeBanner' => array(
'hasBeenDismissed' => \Jetpack_Options::get_option( 'dismissed_welcome_banner', false ),
),
'jetpackManage' => array(
'isEnabled' => Jetpack_Manage::could_use_jp_manage(),
'isAgencyAccount' => Jetpack_Manage::is_agency_account(),
),
'latestBoostSpeedScores' => $speed_score_history->latest(),
'latestBoostSpeedScores' => $latest_score,
)
);
@ -496,7 +500,7 @@ class Initializer {
$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 new WP_Error( 'site_data_fetch_failed', 'Site data fetch failed', array( 'status' => $response_code ) );
}
return rest_ensure_response( $body, 200 );
@ -505,7 +509,7 @@ class Initializer {
/**
* Populates the self::$site_info var with site data from the /sites/%d endpoint
*
* @return Object|WP_Error
* @return object|WP_Error
*/
public static function get_site_info() {
static $site_info = null;
@ -620,7 +624,7 @@ class Initializer {
public static function maybe_show_red_bubble() {
global $menu;
// filters for the items in this file
add_filter( 'my_jetpack_red_bubble_notification_slugs', array( __CLASS__, 'alert_if_missing_site_connection' ) );
add_filter( 'my_jetpack_red_bubble_notification_slugs', array( __CLASS__, 'add_red_bubble_alerts' ) );
$red_bubble_alerts = self::get_red_bubble_alerts();
// The Jetpack menu item should be on index 3
@ -653,6 +657,22 @@ class Initializer {
return $red_bubble_alerts;
}
/**
* 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 ) {
$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;
return $red_bubble_slugs;
} else {
return self::alert_if_missing_site_connection( $red_bubble_slugs );
}
}
/**
* Add an alert slug if the site is missing a site connection
*
@ -661,7 +681,7 @@ class Initializer {
*/
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;
$red_bubble_slugs[ self::MISSING_SITE_CONNECTION_NOTIFICATION_KEY ] = null;
}
return $red_bubble_slugs;

View File

@ -36,6 +36,7 @@ class Products {
'protect' => Products\Protect::class,
'videopress' => Products\Videopress::class,
'stats' => Products\Stats::class,
'ai' => Products\Jetpack_Ai::class,
);
/**
@ -155,7 +156,7 @@ class Products {
'status' => array(
'title' => 'The product status',
'type' => 'string',
'enum' => array( 'active', 'inactive', 'plugin_absent', 'needs_purchase', 'needs_purchase_or_free', 'error' ),
'enum' => array( 'active', 'inactive', 'plugin_absent', 'needs_purchase', 'needs_purchase_or_free', 'needs_first_site_connection', 'user_connection_error', 'site_connection_error' ),
),
'class' => array(
'title' => 'The product class handler',
@ -179,6 +180,7 @@ class Products {
'protect',
'crm',
'search',
'ai',
);
// Add plugin action links for the core Jetpack plugin.

View File

@ -190,6 +190,7 @@ class REST_Products {
$activate_product_result->add_data( array( 'status' => 400 ) );
return $activate_product_result;
}
set_transient( 'my_jetpack_product_activated', $product_slug, 10 );
return rest_ensure_response( Products::get_product( $product_slug ), 200 );
}

View File

@ -9,6 +9,7 @@ namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use WP_Error;
/**
* Registers the REST routes for Purchases.
@ -42,7 +43,7 @@ class REST_Purchases {
$is_site_connected = $connection->is_connected();
if ( ! $is_site_connected ) {
return new \WP_Error(
return new WP_Error(
'not_connected',
__( 'Your site is not connected to Jetpack.', 'jetpack-my-jetpack' ),
array(
@ -68,7 +69,7 @@ class REST_Purchases {
$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 new WP_Error( 'site_data_fetch_failed', 'Site data fetch failed', array( 'status' => $response_code ? $response_code : 400 ) );
}
return rest_ensure_response( $body, 200 );

View File

@ -8,6 +8,8 @@
namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Connection\Client;
use WP_Error;
use WP_REST_Response;
/**
* Registers the REST routes for Zendesk Chat.
@ -56,11 +58,11 @@ class REST_Zendesk_Chat {
* @access public
* @static
*
* @return \WP_Error|true
* @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 new WP_Error( 'unauthorized', 'You must be logged in to access this resource.', array( 'status' => 401 ) );
}
return true;
@ -69,7 +71,7 @@ class REST_Zendesk_Chat {
/**
* Gets the chat authentication token.
*
* @return \WP_Error|object Object: { token: string }
* @return WP_Error|WP_REST_Response { token: string }
*/
public static function get_chat_authentication() {
$authentication = get_transient( self::ZENDESK_AUTH_TOKEN );
@ -91,7 +93,7 @@ class REST_Zendesk_Chat {
$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 ) );
return new WP_Error( 'chat_authentication_failed', 'Chat authentication failed', array( 'status' => $response_code ) );
}
set_transient( self::ZENDESK_AUTH_TOKEN, $body, self::TRANSIENT_EXPIRY );
@ -102,7 +104,7 @@ class REST_Zendesk_Chat {
* 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/object Object: { is_available: bool }
* @return WP_Error|WP_REST_Response { is_available: bool }
*/
public static function get_chat_availability() {
$wpcom_endpoint = '/presales/chat?group=jp_presales';
@ -112,7 +114,7 @@ class REST_Zendesk_Chat {
$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 new WP_Error( 'chat_config_data_fetch_failed', 'Chat config data fetch failed', array( 'status' => $response_code ) );
}
return rest_ensure_response( $body, 200 );

View File

@ -202,10 +202,10 @@ class Wpcom_Products {
/**
* Populate the pricing array with the discount information.
*
* @param {object} $product - The product object.
* @param {object} $pricing - The pricing array.
* @param {float} $price - The price to be discounted.
* @return {object} 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.

View File

@ -43,6 +43,13 @@ class Anti_Spam extends Product {
*/
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
*
@ -94,10 +101,11 @@ class Anti_Spam extends Product {
/**
* Determine if the site has an Akismet plan by checking for an API key
* Note that some Akismet Plans are free - we're just checking for an API key and don't have the perspective of the plan attached to it here
*
* @return bool - whether an API key was found
*/
public static function has_required_plan() {
public static function has_paid_plan_for_product() {
// 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 );

View File

@ -51,6 +51,20 @@ class Backup extends Hybrid_Product {
*/
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;
/**
* Get the product name
*
@ -183,7 +197,7 @@ class Backup extends Hybrid_Product {
*
* @return boolean
*/
public static function has_required_plan() {
public static function has_paid_plan_for_product() {
$rewind_data = static::get_state_from_wpcom();
if ( is_wp_error( $rewind_data ) ) {
return false;
@ -224,13 +238,4 @@ class Backup extends Hybrid_Product {
return Redirect::get_url( 'my-jetpack-manage-backup' );
}
}
/**
* Checks whether the Product is active
*
* @return boolean
*/
public static function is_active() {
return parent::is_active() && static::has_required_plan();
}
}

View File

@ -9,6 +9,7 @@ namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use WP_Error;
/**
* Class responsible for handling the Boost product
@ -50,6 +51,13 @@ class Boost extends Product {
*/
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
*
@ -303,7 +311,7 @@ class Boost extends Product {
* 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
* @return boolean|WP_Error
*/
public static function do_product_specific_activation( $current_result ) {

View File

@ -49,6 +49,13 @@ class Creator extends Product {
*/
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
*
@ -156,20 +163,6 @@ class Creator extends Product {
),
),
),
array(
'name' => __( 'Creator network', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'<p>The creator network is the network of websites either hosted with WordPress.com or self-hosted and connected with Jetpack.</p>
<p>Sites that are part of the creator network can gain exposure to new readers. Sites on the Creator plan have enhanced distribution to more areas of the Reader.</p>',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Jetpack Blocks', 'jetpack-my-jetpack' ),
'info' => array(
@ -330,7 +323,7 @@ class Creator extends Product {
*
* @return boolean
*/
public static function has_required_plan() {
public static function has_paid_plan_for_product() {
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
@ -352,7 +345,6 @@ class Creator extends Product {
* @return boolean
*/
public static function is_upgradable() {
$has_required_plan = self::has_required_plan();
return ! $has_required_plan;
return ! self::has_paid_plan_for_product();
}
}

View File

@ -46,6 +46,13 @@ class Crm extends Product {
*/
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
*
@ -140,12 +147,14 @@ class Crm extends Product {
*
* @return boolean
*/
public static function has_required_plan() {
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' ) ) {

View File

@ -8,6 +8,7 @@
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Product;
use WP_Error;
/**
* Class responsible for handling the Extras product.

View File

@ -17,7 +17,6 @@ use WP_Error;
* 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
*
@ -52,18 +51,6 @@ abstract class Hybrid_Product extends Product {
return parent::is_plugin_active();
}
/**
* Checks whether the Jetpack module is active only if a module_name is defined
*
* @return bool
*/
public static function is_module_active() {
if ( ! empty( static::$module_name ) ) {
return ( new Modules() )->is_active( static::$module_name );
}
return true;
}
/**
* Checks whether the Product is active
*
@ -118,9 +105,13 @@ abstract class Hybrid_Product extends Product {
}
}
// 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::has_required_plan() && ! empty( static::$module_name ) ) {
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 ) {
@ -149,7 +140,7 @@ abstract class Hybrid_Product extends Product {
* Activate the module as well, if the user has a plan
* or the product does not require a plan to work
*/
if ( static::has_required_plan() && isset( static::$module_name ) ) {
if ( static::has_any_plan_for_product() && isset( static::$module_name ) ) {
$module_activation = ( new Modules() )->activate( static::$module_name, false, false );
if ( ! $module_activation ) {

View File

@ -8,14 +8,19 @@
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;
/**
* 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
*
@ -23,6 +28,13 @@ class Jetpack_Ai extends Product {
*/
public static $slug = 'jetpack-ai';
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* Get the Product info for the API
*
@ -75,6 +87,84 @@ class Jetpack_Ai extends Product {
return 'Jetpack AI';
}
/**
* 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::CURRENT_TIER_SLUG,
);
}
/**
* Get the internationalized comparison of free vs upgraded features
*
* @return array[] Protect features comparison
*/
public static function 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
*
@ -89,7 +179,7 @@ class Jetpack_Ai extends Product {
// Bail early if it's not possible to fetch the feature data.
if ( is_wp_error( $info ) ) {
return null;
return 0;
}
$current_tier = isset( $info['current-tier']['value'] ) ? $info['current-tier']['value'] : null;
@ -103,7 +193,7 @@ class Jetpack_Ai extends Product {
* @return int
*/
public static function get_next_usage_tier() {
if ( ! self::is_site_connected() || ! self::has_required_plan() ) {
if ( ! self::is_site_connected() || ! self::has_paid_plan_for_product() ) {
return 100;
}
@ -126,7 +216,7 @@ class Jetpack_Ai extends Product {
* @return string
*/
public static function get_description() {
return __( 'Experimental tool to add AI to your editor', 'jetpack-my-jetpack' );
return __( 'The most powerful AI tool for WordPress', 'jetpack-my-jetpack' );
}
/**
@ -201,10 +291,14 @@ class Jetpack_Ai extends Product {
/**
* Get the product pricing details by tier
*
* @param int $tier The usage 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 ) ) {
@ -266,14 +360,34 @@ class Jetpack_Ai extends Product {
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
$next_tier = self::get_next_usage_tier();
$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_merge(
array(
'available' => true,
'wpcom_product_slug' => static::get_wpcom_product_slug(),
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,
)
),
),
self::get_pricing_for_ui_by_usage_tier( $next_tier )
);
}
@ -305,12 +419,23 @@ class Jetpack_Ai extends Product {
}
/**
* Checks whether the current plan (or purchases) of the site already supports the product
* Checks whether the site has a paid plan for this product
*
* @return boolean
*/
public static function has_required_plan() {
return static::does_site_have_feature( 'ai-assistant' );
public static function has_paid_plan_for_product() {
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
if ( str_contains( $purchase->product_slug, 'jetpack_ai' ) ) {
return true;
}
}
}
return false;
}
/**
@ -319,24 +444,60 @@ class Jetpack_Ai extends Product {
* @return boolean
*/
public static function is_upgradable() {
$has_required_plan = self::has_required_plan();
$current_tier = self::get_current_usage_tier();
$has_ai_feature = static::does_site_have_feature( 'ai-assistant' );
$current_tier = self::get_current_usage_tier();
// Mark as not upgradable if user is on unlimited tier or does not have any plan.
if ( ! $has_required_plan || null === $current_tier || 1 === $current_tier ) {
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 '/wp-admin/admin.php?page=my-jetpack#/jetpack-ai';
}
/**
* Get the URL the user is taken after activating the product through the checkout
*
* @return ?string
*/
public static function get_post_activation_url() {
return '/wp-admin/admin.php?page=my-jetpack#/jetpack-ai';
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return '';
return '/wp-admin/admin.php?page=my-jetpack#/add-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();
}
/**
@ -383,4 +544,61 @@ class Jetpack_Ai extends Product {
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( 'admin_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, WP_Post $post ) {
if ( isset( $_GET['use_ai_block'] ) && isset( $_GET['_wpnonce'] )
&& wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'ai-assistant-content-nonce' )
&& current_user_can( 'edit_post', $post->ID )
&& '' === $content
) {
return '<!-- wp:jetpack/ai-assistant /-->';
}
return $content;
}
}

View File

@ -9,6 +9,7 @@ namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Modules;
use Automattic\Jetpack\Plugins_Installer;
use Jetpack_Options;
use WP_Error;
@ -25,6 +26,13 @@ abstract class Product {
*/
public static $slug = null;
/**
* The Jetpack module name, if any.
*
* @var ?string
*/
public static $module_name = null;
/**
* The filename (id) of the plugin associated with this product. Can be a string with a single value or a list of possible values
*
@ -77,6 +85,21 @@ abstract class Product {
*/
public static $has_standalone_plugin = false;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = false;
/**
* Whether the product requires a plan to run
* The plan could be paid or free
*
* @var bool
*/
public static $requires_plan = false;
/**
* Get the plugin slug
*
@ -141,13 +164,15 @@ abstract class Product {
'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_required_plan' => static::has_required_plan(),
'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_required_tier' => static::has_required_tier(),
'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(),
@ -332,19 +357,6 @@ abstract class Product {
);
}
/**
* Checks whether the current plan (or purchases) of the site already supports the product
*
* Returns true if it supports. Return false if a purchase is still required.
*
* Free products will always return true.
*
* @return boolean
*/
public static function has_required_plan() {
return true;
}
/**
* Checks whether the site has a paid plan for the product
* This ignores free products, it only checks if there is a purchase that supports the product
@ -352,19 +364,26 @@ abstract class Product {
* @return boolean
*/
public static function has_paid_plan_for_product() {
// TODO: this is not always the same.
// There should be checks on each individual product class for paid plans if the product has a free offering
// For products with no free offering, checking has_required_plan works fine
return static::has_required_plan();
return false;
}
/**
* Checks whether the current plan (or purchases) of the site already supports the tiers
* Checks whether the site has a free plan for the product
* Note, this should not return true if a product does not have a WPCOM plan (ex: search free, Akismet Free, stats free)
*
* @return array Key/value pairs of tier slugs and whether they are supported or not.
* @return false
*/
public static function has_required_tier() {
return array();
public static function has_free_plan_for_product() {
return false;
}
/**
* Checks whether the site has any WPCOM plan for a product (paid or free)
*
* @return bool
*/
public static function has_any_plan_for_product() {
return static::has_paid_plan_for_product() || static::has_free_plan_for_product();
}
/**
@ -434,25 +453,38 @@ abstract class Product {
$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() ) {
$status = 'error';
} elseif ( static::$requires_user_connection && ! ( new Connection_Manager() )->has_connected_owner() ) {
$status = 'error';
} elseif ( static::is_upgradable() ) {
// Upgradable plans should ignore whether or not they have the required plan.
$status = 'can_upgrade';
} elseif ( ! static::has_required_plan() ) { // We need needs_purchase here as well because some products we consider active without the required plan.
if ( static::has_trial_support() ) {
$status = 'needs_purchase_or_free';
// Site has never been connected before
if ( ! \Jetpack_Options::get_option( 'id' ) ) {
$status = 'needs_first_site_connection';
} else {
$status = 'needs_purchase';
$status = 'site_connection_error';
}
} elseif ( static::$requires_user_connection && ! ( new Connection_Manager() )->has_connected_owner() ) {
$status = 'user_connection_error';
} elseif ( static::is_upgradable() ) {
$status = 'can_upgrade';
}
} elseif ( ! static::has_required_plan() ) {
if ( static::has_trial_support() ) {
$status = 'needs_purchase_or_free';
} else {
$status = 'needs_purchase';
// Check specifically for inactive modules, which will prevent a product from being active
} elseif ( static::$module_name && ! static::is_module_active() ) {
$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';
} 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';
} else {
$status = 'site_connection_error';
}
} elseif ( static::$requires_user_connection && ! ( new Connection_Manager() )->has_connected_owner() ) {
$status = 'user_connection_error';
}
} elseif ( ! static::has_any_plan_for_product() ) {
$status = static::$has_free_offering ? 'needs_purchase_or_free' : 'needs_purchase';
} else {
$status = 'inactive';
}
@ -465,7 +497,7 @@ abstract class Product {
* @return boolean
*/
public static function is_active() {
return static::is_plugin_active() && static::has_required_plan();
return static::is_plugin_active() && ( static::has_any_plan_for_product() || ( ! static::$requires_plan && static::$has_free_offering ) );
}
/**
@ -504,6 +536,18 @@ abstract class Product {
return Plugins_Installer::is_plugin_active( static::get_installed_plugin_filename( 'jetpack' ) );
}
/**
* Checks whether the Jetpack module is active only if a module_name is defined
*
* @return bool
*/
public static function is_module_active() {
if ( static::$module_name ) {
return ( new Modules() )->is_active( static::$module_name );
}
return true;
}
/**
* Activates the plugin
*

View File

@ -54,6 +54,13 @@ class Protect extends Product {
*/
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
*

View File

@ -47,6 +47,20 @@ class Search extends Hybrid_Product {
*/
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.
*
@ -63,7 +77,7 @@ class Search extends Hybrid_Product {
*
* @var boolean
*/
public static $requires_user_connection = false;
public static $requires_user_connection = true;
/**
* Get the product name
@ -286,17 +300,46 @@ class Search extends Hybrid_Product {
}
/**
* Checks whether the current plan of the site already supports the product
* Checks if the site purchases contain a paid search plan
*
* Returns true if it supports. Return false if a purchase is still required.
*
* Free products will always return true.
*
* @return boolean
* @return bool
*/
public static function has_required_plan() {
$search_state = static::get_state_from_wpcom();
return ! empty( $search_state->supports_search ) || ! empty( $search_state->supports_instant_search );
public static function has_paid_plan_for_product() {
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
// Search is available as standalone product and as part of the Complete plan.
if (
( str_contains( $purchase->product_slug, 'jetpack_search' ) && ! str_contains( $purchase->product_slug, 'jetpack_search_free' ) ) ||
str_starts_with( $purchase->product_slug, 'jetpack_complete' ) ) {
return true;
}
}
}
return false;
}
/**
* 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;
}
/**

View File

@ -55,6 +55,13 @@ class Social extends Hybrid_Product {
'jetpack-social-dev/jetpack-social.php',
);
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* Get the product name
*
@ -142,7 +149,7 @@ class Social extends Hybrid_Product {
*
* @return boolean
*/
public static function has_required_plan() {
public static function has_paid_plan_for_product() {
// 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() ) {

View File

@ -59,6 +59,13 @@ class Stats extends Module_Product {
*/
public static $has_standalone_plugin = false;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* Get the product name
*

View File

@ -61,6 +61,13 @@ class Videopress extends Hybrid_Product {
*/
public static $has_standalone_plugin = true;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* Get the product name
*
@ -167,11 +174,22 @@ class Videopress extends Hybrid_Product {
}
/**
* Checks whether the current plan (or purchases) of the site already supports the product
* Checks whether the site has a paid plan for this product
*
* @return boolean
*/
public static function has_required_plan() {
return static::does_site_have_feature( 'videopress' );
public static function has_paid_plan_for_product() {
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
if ( str_contains( $purchase->product_slug, 'jetpack_videopress' ) || str_starts_with( $purchase->product_slug, 'jetpack_complete' ) ) {
return true;
}
}
}
return false;
}
}