updated plugin Jetpack Protect version 2.0.0

This commit is contained in:
2024-02-08 12:31:43 +00:00
committed by Gitium
parent ce653dd56c
commit 8d5e7cc070
192 changed files with 5244 additions and 2003 deletions

View File

@ -0,0 +1,57 @@
<?php
/**
* Manage the display of an "Activity Log" menu item.
*
* @package automattic/my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Admin_UI\Admin_Menu;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Redirect;
/**
* Activity Log features in My Jetpack.
*/
class Activitylog {
/**
* Initialize the class and hooks needed.
*/
public static function init() {
add_action( 'admin_menu', array( self::class, 'add_submenu_jetpack' ) );
}
/**
* The page to be added to submenu
*
* @return void|null|string The resulting page's hook_suffix
*/
public static function add_submenu_jetpack() {
// Only proceed if the user is connected to WordPress.com.
if ( ! ( new Connection_Manager() )->is_user_connected() ) {
return;
}
// Do not display the menu on Multisite.
if ( is_multisite() ) {
return;
}
$args = array();
$blog_id = Connection_Manager::get_site_id( true );
if ( $blog_id ) {
$args = array( 'site' => $blog_id );
}
return Admin_Menu::add_menu(
__( 'Activity Log', 'jetpack-my-jetpack' ),
_x( 'Activity Log', 'product name shown in menu', 'jetpack-my-jetpack' ) . ' <span class="dashicons dashicons-external"></span>',
'manage_options',
esc_url( Redirect::get_url( 'cloud-activity-log-wp-menu', $args ) ),
null,
1
);
}
}

View File

@ -19,8 +19,10 @@ use Automattic\Jetpack\Licensing;
use Automattic\Jetpack\Modules;
use Automattic\Jetpack\Plugins_Installer;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Status\Host as Status_Host;
use Automattic\Jetpack\Terms_Of_Service;
use Automattic\Jetpack\Tracking;
use Jetpack;
/**
* The main Initializer class that registers the admin menu and eneuque the assets.
@ -32,13 +34,24 @@ class Initializer {
*
* @var string
*/
const PACKAGE_VERSION = '3.9.1';
const PACKAGE_VERSION = '4.6.0';
/**
* HTML container ID for the IDC screen on My Jetpack page.
*/
const IDC_CONTAINER_ID = 'my-jetpack-identity-crisis-container';
const JETPACK_PLUGIN_SLUGS = array(
'jetpack-backup',
'jetpack-boost',
'zerobscrm',
'jetpack',
'jetpack-protect',
'jetpack-social',
'jetpack-videopress',
'jetpack-search',
);
/**
* Initialize My Jetpack
*
@ -76,6 +89,12 @@ class Initializer {
// Sets up JITMS.
JITM::configure();
// Add "Activity Log" menu item.
Activitylog::init();
// Add "Jetpack Manage" menu item.
Jetpack_Manage::init();
/**
* Fires after the My Jetpack package is initialized
*
@ -171,8 +190,10 @@ class Initializer {
),
'plugins' => Plugins_Installer::get_plugins(),
'myJetpackUrl' => admin_url( 'admin.php?page=my-jetpack' ),
'myJetpackCheckoutUri' => 'admin.php?page=my-jetpack',
'topJetpackMenuItemUrl' => Admin_Menu::get_top_level_menu_item_url(),
'siteSuffix' => ( new Status() )->get_site_suffix(),
'blogID' => Connection_Manager::get_site_id( true ),
'myJetpackVersion' => self::PACKAGE_VERSION,
'myJetpackFlags' => self::get_my_jetpack_flags(),
'fileSystemWriteAccess' => self::has_file_system_write_access(),
@ -180,7 +201,16 @@ class Initializer {
'adminUrl' => esc_url( admin_url() ),
'IDCContainerID' => static::get_idc_container_id(),
'userIsAdmin' => current_user_can( 'manage_options' ),
'userIsNewToJetpack' => self::is_jetpack_user_new(),
'isStatsModuleActive' => $modules->is_active( 'stats' ),
'isUserFromKnownHost' => self::is_user_from_known_host(),
'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(),
),
)
);
@ -202,6 +232,72 @@ class Initializer {
}
}
/**
* Determine if the current user is "new" to Jetpack
* This is used to vary some messaging in My Jetpack
*
* On the front-end, purchases are also taken into account
*
* @return bool
*/
public static function is_jetpack_user_new() {
// is the user connected?
$connection = new Connection_Manager();
if ( $connection->is_user_connected() ) {
return false;
}
// TODO: add a data point for the last known connection/ disconnection time
// are any modules active?
$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() );
}
if ( ! empty( $active_modules ) ) {
return false;
}
// check for other Jetpack plugins that are installed on the site (active or not)
// If there's more than one Jetpack plugin active, this user is not "new"
$plugin_slugs = array_keys( Plugins_Installer::get_plugins() );
$plugin_slugs = array_map(
static function ( $slug ) {
$parts = explode( '/', $slug );
if ( empty( $parts ) ) {
return '';
}
// Return the last segment of the filepath without the PHP extension
return str_replace( '.php', '', $parts[ count( $parts ) - 1 ] );
},
$plugin_slugs
);
$installed_jetpack_plugins = array_intersect( self::JETPACK_PLUGIN_SLUGS, $plugin_slugs );
if ( is_countable( $installed_jetpack_plugins ) && count( $installed_jetpack_plugins ) >= 2 ) {
return false;
}
// Does the site have any purchases?
$purchases = Wpcom_Products::get_site_current_purchases();
if ( ! empty( $purchases ) && ! is_wp_error( $purchases ) ) {
return false;
}
return true;
}
/**
* Determines whether the user has come from a host we can recognize.
*
* @return string
*/
public static function is_user_from_known_host() {
// Known (external) host is the one that has been determined and is not dotcom.
return ! in_array( ( new Status_Host() )->get_known_host_guess(), array( 'unknown', 'wpcom' ), true );
}
/**
* Build flags for My Jetpack UI
*
@ -246,6 +342,16 @@ class Initializer {
'permission_callback' => __CLASS__ . '::permissions_callback',
)
);
register_rest_route(
'my-jetpack/v1',
'site/dismiss-welcome-banner',
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::dismiss_welcome_banner',
'permission_callback' => __CLASS__ . '::permissions_callback',
)
);
}
/**
@ -300,6 +406,16 @@ class Initializer {
return rest_ensure_response( $body, 200 );
}
/**
* Dismiss the welcome banner.
*
* @return \WP_REST_Response
*/
public static function dismiss_welcome_banner() {
\Jetpack_Options::update_option( 'dismissed_welcome_banner', true );
return rest_ensure_response( array( 'success' => true ), 200 );
}
/**
* Returns true if the site has file write access to the plugins folder, false otherwise.
*

View File

@ -0,0 +1,124 @@
<?php
/**
* Tools to manage things related to "Jetpack Manage"
* - Add Jetpack Manage menu item.
* - Check if user is an agency (used by the Jetpack Manage banner)
*
* @package automattic/my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Admin_UI\Admin_Menu;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Redirect;
/**
* Jetpack Manage features in My Jetpack.
*/
class Jetpack_Manage {
/**
* Initialize the class and hooks needed.
*/
public static function init() {
add_action( 'admin_menu', array( self::class, 'add_submenu_jetpack' ) );
}
/**
* The page to be added to submenu
*
* @return void|null|string The resulting page's hook_suffix
*/
public static function add_submenu_jetpack() {
// Do not display the menu if the user has < 2 sites.
if ( ! self::could_use_jp_manage( 2 ) ) {
return;
}
$args = array();
$blog_id = Connection_Manager::get_site_id( true );
if ( $blog_id ) {
$args = array( 'site' => $blog_id );
}
return Admin_Menu::add_menu(
__( 'Jetpack Manage', 'jetpack-my-jetpack' ),
_x( 'Jetpack Manage', 'product name shown in menu', 'jetpack-my-jetpack' ) . ' <span class="dashicons dashicons-external"></span>',
'manage_options',
esc_url( Redirect::get_url( 'cloud-manage-dashboard-wp-menu', $args ) ),
null,
100
);
}
/**
* Check if the user has enough sites to be able to use Jetpack Manage.
*
* @param int $min_sites Minimum number of sites to be able to use Jetpack Manage.
*
* @return bool Return true if the user has enough sites to be able to use Jetpack Manage.
*/
public static function could_use_jp_manage( $min_sites = 2 ) {
// Only proceed if the user is connected to WordPress.com.
if ( ! ( new Connection_Manager() )->is_user_connected() ) {
return false;
}
// Do not display the menu if Jetpack plugin is not installed.
if ( ! class_exists( 'Jetpack' ) ) {
return false;
}
// Do not display the menu on Multisite.
if ( is_multisite() ) {
return false;
}
// Check if the user has the minimum number of sites.
$user_data = ( new Connection_Manager() )->get_connected_user_data( get_current_user_id() );
if ( ! isset( $user_data['site_count'] ) || $user_data['site_count'] < $min_sites ) {
return false;
}
return true;
}
/**
* Check if the user is a partner/agency.
*
* @return bool Return true if the user is a partner/agency, otherwise false.
*/
public static function is_agency_account() {
// Only proceed if the user is connected to WordPress.com.
if ( ! ( new Connection_Manager() )->is_user_connected() ) {
return false;
}
// Get the cached partner data.
$partner = get_transient( 'jetpack_partner_data' );
if ( $partner === false ) {
$wpcom_response = Client::wpcom_json_api_request_as_user( '/jetpack-partners' );
if ( 200 !== wp_remote_retrieve_response_code( $wpcom_response ) || is_wp_error( $wpcom_response ) ) {
return false;
}
$partner_data = json_decode( wp_remote_retrieve_body( $wpcom_response ) );
// The jetpack-partners endpoint will return only one partner data into an array, it uses Jetpack_Partner::find_by_owner.
if ( ! is_array( $partner_data ) || count( $partner_data ) !== 1 || ! is_object( $partner_data[0] ) ) {
return false;
}
$partner = $partner_data[0];
// Cache the partner data for 1 hour.
set_transient( 'jetpack_partner_data', $partner, HOUR_IN_SECONDS );
}
return $partner->partner_type === 'agency';
}
}

View File

@ -26,6 +26,7 @@ class Products {
'backup' => Products\Backup::class,
'boost' => Products\Boost::class,
'crm' => Products\Crm::class,
'creator' => Products\Creator::class,
'extras' => Products\Extras::class,
'jetpack-ai' => Products\Jetpack_Ai::class,
'scan' => Products\Scan::class,

View File

@ -18,12 +18,23 @@ class REST_Product_Data {
* Constructor.
*/
public function __construct() {
// Get backup undo event
register_rest_route(
'my-jetpack/v1',
'site/product-data',
'/site/backup/undo-event',
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_all_product_data',
'callback' => __CLASS__ . '::get_site_backup_undo_event',
'permission_callback' => __CLASS__ . '::permissions_callback',
)
);
register_rest_route(
'my-jetpack/v1',
'/site/backup/count-items',
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::count_things_that_can_be_backed_up',
'permission_callback' => __CLASS__ . '::permissions_callback',
)
);
@ -37,22 +48,100 @@ class REST_Product_Data {
}
/**
* Gets the product data for all products
* This will fetch the last rewindable event from the Activity Log and
* the last rewind_id prior to that.
*
* @return array|WP_Error
* @return array|WP_Error|null
*/
public static function get_all_product_data() {
$site_id = \Jetpack_Options::get_option( 'id' );
$wpcom_endpoint = sprintf( 'sites/%d/jetpack-product-data?locale=%2$s&force=wpcom', $site_id, get_user_locale() );
$api_version = '2';
$response = Client::wpcom_json_api_request_as_blog( $wpcom_endpoint, $api_version, array(), null, 'wpcom' );
$response_code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ) );
public static function get_site_backup_undo_event() {
$blog_id = \Jetpack_Options::get_option( 'id' );
if ( is_wp_error( $response ) || empty( $response['body'] ) || 200 !== $response_code ) {
return new WP_Error( 'site_products_data_fetch_failed', 'Site products data fetch failed', array( 'status' => $response_code ? $response_code : 400 ) );
$response = Client::wpcom_json_api_request_as_user(
'/sites/' . $blog_id . '/activity/rewindable?force=wpcom',
'v2',
array(),
null,
'wpcom'
);
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return null;
}
return rest_ensure_response( $body, 200 );
$body = json_decode( $response['body'], true );
if ( ! isset( $body['current'] ) ) {
return null;
}
// Preparing the response structure
$undo_event = array(
'last_rewindable_event' => null,
'undo_backup_id' => null,
);
// List of events that will not be considered to be undo.
// Basically we should not `undo` a full backup event, but we could
// use them to undo any other action like plugin updates.
$last_event_exceptions = array(
'rewind__backup_only_complete_full',
'rewind__backup_only_complete_initial',
'rewind__backup_only_complete',
'rewind__backup_complete_full',
'rewind__backup_complete_initial',
'rewind__backup_complete',
);
// Looping through the events to find the last rewindable event and the last backup_id.
// The idea is to find the last rewindable event and then the last rewind_id before that.
$found_last_event = false;
foreach ( $body['current']['orderedItems'] as $event ) {
if ( $event['is_rewindable'] ) {
if ( ! $found_last_event && ! in_array( $event['name'], $last_event_exceptions, true ) ) {
$undo_event['last_rewindable_event'] = $event;
$found_last_event = true;
} elseif ( $found_last_event ) {
$undo_event['undo_backup_id'] = $event['rewind_id'];
break;
}
}
}
return rest_ensure_response( $undo_event, 200 );
}
/**
* 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
*/
public static function count_things_that_can_be_backed_up() {
$image_mime_type = 'image';
$video_mime_type = 'video';
$audio_mime_type = 'audio';
$data = array();
// Add all post types together to get the total post count
$data['total_post_count'] = array_sum( (array) wp_count_posts( 'post' ) );
// Add all page types together to get the total page count
$data['total_page_count'] = array_sum( (array) wp_count_posts( 'page' ) );
// Add all comments together to get the total comment count
$comments = (array) wp_count_comments();
$data['total_comment_count'] = $comments ? $comments['total_comments'] : 0;
// Add all image attachments together to get the total image count
$data['total_image_count'] = array_sum( (array) wp_count_attachments( $image_mime_type ) );
// Add all video attachments together to get the total video count
$data['total_video_count'] = array_sum( (array) wp_count_attachments( $video_mime_type ) );
// Add all audio attachments together to get the total audio count
$data['total_audio_count'] = array_sum( (array) wp_count_attachments( $audio_mime_type ) );
return rest_ensure_response( $data, 200 );
}
}

View File

@ -30,6 +30,8 @@ class Wpcom_Products {
*/
const CACHE_META_NAME = 'my-jetpack-cache';
const MY_JETPACK_PURCHASES_TRANSIENT_KEY = 'my-jetpack-purchases';
/**
* Fetches the list of products from WPCOM
*
@ -149,11 +151,12 @@ class Wpcom_Products {
* Get one product
*
* @param string $product_slug The product slug.
* @param bool $renew_cache A flag to force the cache to be renewed.
*
* @return ?Object The product details if found
*/
public static function get_product( $product_slug ) {
$products = self::get_products();
public static function get_product( $product_slug, $renew_cache = false ) {
$products = self::get_products( $renew_cache );
if ( ! empty( $products->$product_slug ) ) {
return $products->$product_slug;
}
@ -232,18 +235,21 @@ class Wpcom_Products {
/**
* Gets the site purchases from WPCOM.
*
* @todo Maybe add caching.
*
* @return Object|WP_Error
*/
public static function get_site_current_purchases() {
// TODO: Add a short-lived cache (less than a minute) to accommodate repeated invocation of this function.
static $purchases = null;
if ( $purchases !== null ) {
return $purchases;
}
// Check for a cached value before doing lookup
$stored_purchases = get_transient( self::MY_JETPACK_PURCHASES_TRANSIENT_KEY );
if ( $stored_purchases !== false ) {
return $stored_purchases;
}
$site_id = Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_blog(
@ -259,6 +265,9 @@ class Wpcom_Products {
$body = wp_remote_retrieve_body( $response );
$purchases = json_decode( $body );
// Set short transient to help with repeated lookups on the same page load
set_transient( self::MY_JETPACK_PURCHASES_TRANSIENT_KEY, $purchases, 5 );
return $purchases;
}
}

View File

@ -92,6 +92,38 @@ class Anti_Spam extends Product {
);
}
/**
* Determine if the site has an Akismet plan by checking for an API key
*
* @return bool - whether an API key was found
*/
public static function has_required_plan() {
// 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 );
// Check for existing plans
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return $fallback;
}
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;
}
}
}
return $fallback;
}
/**
* Get the product princing details
*

View File

@ -75,7 +75,11 @@ class Backup extends Hybrid_Product {
* @return string
*/
public static function get_description() {
return __( 'Save every change', 'jetpack-my-jetpack' );
if ( static::is_active() ) {
return __( 'Save every change', 'jetpack-my-jetpack' );
}
return __( 'Your site is not backed up', 'jetpack-my-jetpack' );
}
/**
@ -226,9 +230,9 @@ class Backup extends Hybrid_Product {
*/
public static function get_post_checkout_url() {
if ( static::is_jetpack_plugin_active() ) {
return admin_url( 'admin.php?page=jetpack#/recommendations' );
return 'admin.php?page=jetpack#/recommendations';
} elseif ( static::is_plugin_active() ) {
return admin_url( 'admin.php?page=jetpack-backup' );
return 'admin.php?page=jetpack-backup';
}
}
}

View File

@ -0,0 +1,358 @@
<?php
/**
* Creator product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
/**
* Class responsible for handling the Creator product
*/
class Creator extends Product {
const FREE_TIER_SLUG = 'free';
const UPGRADED_TIER_SLUG = 'upgraded';
const UPGRADED_TIER_PRODUCT_SLUG = 'jetpack_creator_yearly';
/**
* The product slug
*
* @var string
*/
public static $slug = 'creator';
/**
* The slug of the plugin associated with this product - Creator functionalities are part of Jetpack's main plugin
*
* @var string
*/
public static $plugin_slug = self::JETPACK_PLUGIN_SLUG;
/**
* Get the plugin filename - ovewrite it and return Jetpack's
*
* @return ?string
*/
public static function get_plugin_filename() {
return self::JETPACK_PLUGIN_FILENAME;
}
/**
* Whether this product requires a user connection
*
* @var string
*/
public static $requires_user_connection = false;
/**
* Get the internationalized product name
*
* @return string
*/
public static function get_name() {
return __( 'Creator', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product title
*
* @return string
*/
public static function get_title() {
return __( 'Jetpack Creator', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Create, grow, and monetize your audience', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Create, grow, and monetize your audience with powerful tools for creators.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Features list
*/
public static function get_features() {
return array(
__( 'Create content that stands out', 'jetpack-my-jetpack' ),
__( 'Grow your subscribers through our creator network and tools', 'jetpack-my-jetpack' ),
__( 'Monetize your online presence and earn from your website', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product's available tiers
*
* @return string[] Slugs of the available tiers
*/
public static function get_tiers() {
return array(
self::UPGRADED_TIER_SLUG,
self::FREE_TIER_SLUG,
);
}
/**
* Get the internationalized comparison of free vs upgraded features
*
* @return array[] Protect features comparison
*/
public static function get_features_by_tier() {
return array(
array(
'name' => __( 'Import subscribers', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Import a CSV file of your existing subscribers to be sent your Newsletter.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array(
'included' => true,
'description' => __( '100 subscribers', 'jetpack-my-jetpack' ),
),
self::UPGRADED_TIER_SLUG => array(
'included' => true,
'description' => __( 'Unlimited subscribers', 'jetpack-my-jetpack' ),
),
),
),
array(
'name' => __( 'Transaction fees', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'<p>Fees are only collected when you accept payments.</p>
<p>Fees are based on the Jetpack plan you have and are calculated as a percentage of your revenue from 10% on the Free plan to 2% on the Creator plan (plus Stripe fees).</p>',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array(
'included' => true,
'description' => __( '10%', 'jetpack-my-jetpack' ),
),
self::UPGRADED_TIER_SLUG => array(
'included' => true,
'description' => __( '2%', 'jetpack-my-jetpack' ),
),
),
),
array(
'name' => __( '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(
'content' => __(
'Jetpack has over 40 Gutenberg blocks to help you with your content creation, such as displaying your podcasts, showing different content to repeat visitors, creating contact forms and many more.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Paid content gating', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Lock your content behind a paid content block. To access the content, readers will need to pay a one-time fee or a recurring subscription.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Paywall access', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Add a Paywall to your content which lets your visitors read a section of your content before being asked to subscribe to continue reading.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Newsletter', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Start a Newsletter by sending your content as an email newsletter direct to your fans email inboxes.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Pay with PayPal', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Accept payment with PayPal for simple payments like eBooks, courses and more.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'WordAds', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'<p>WordAds adds advertisements to your website. Start earning from your website traffic.</p>
<p>Over 50 internet advertisers — including Google AdSense & Adx, AppNexus, Amazon A9, AOL Marketplace, Yahoo, Criteo, and more — bid to display ads in WordAds spots.</p>',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Dedicated email support', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'<p>Paid customers get dedicated email support from our world-class Happiness Engineers to help with any issue.</p>
<p>All other questions are handled by our team as quickly as we are able to go through the WordPress support forum.</p>',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
);
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array(
'tiers' => array(
self::FREE_TIER_SLUG => array(
'available' => true,
'is_free' => true,
),
self::UPGRADED_TIER_SLUG => array_merge(
array(
'available' => true,
'wpcom_product_slug' => self::UPGRADED_TIER_PRODUCT_SLUG,
),
Wpcom_Products::get_product_pricing( self::UPGRADED_TIER_PRODUCT_SLUG )
),
),
);
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return admin_url( 'admin.php?page=jetpack#/settings?term=creator' );
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_creator_yearly';
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_biyearly_product_slug() {
return 'jetpack_creator_bi_yearly';
}
/**
* Get the WPCOM monthly product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_monthly_product_slug() {
return 'jetpack_creator_monthly';
}
/**
* Checks whether the current plan (or purchases) of the site already supports the product
*
* @return boolean
*/
public static function has_required_plan() {
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
// Creator is available as standalone bundle and as part of the Complete plan.
if ( strpos( $purchase->product_slug, 'jetpack_creator' ) !== false || str_starts_with( $purchase->product_slug, 'jetpack_complete' ) ) {
return true;
}
}
}
return false;
}
/**
* Checks whether the product can be upgraded - i.e. this shows the /#add-creator interstitial
*
* @return boolean
*/
public static function is_upgradable() {
$has_required_plan = self::has_required_plan();
return ! $has_required_plan;
}
}

View File

@ -7,6 +7,7 @@
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\My_Jetpack\Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
@ -74,6 +75,51 @@ class Jetpack_Ai extends Product {
return __( 'Jetpack AI', 'jetpack-my-jetpack' );
}
/**
* Get the current usage tier
*
* @return int
*/
public static function get_current_usage_tier() {
if ( ! self::is_site_connected() ) {
return 0;
}
$info = self::get_ai_assistant_feature();
// Bail early if it's not possible to fetch the feature data.
if ( is_wp_error( $info ) ) {
return null;
}
$current_tier = isset( $info['current-tier']['value'] ) ? $info['current-tier']['value'] : null;
return $current_tier;
}
/**
* Get the next usage tier
*
* @return int
*/
public static function get_next_usage_tier() {
if ( ! self::is_site_connected() ) {
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 ) ) {
return null;
}
// Trust the next tier provided by the feature data.
$next_tier = isset( $info['next-tier']['value'] ) ? $info['next-tier']['value'] : null;
return $next_tier;
}
/**
* Get the internationalized product description
*
@ -83,49 +129,164 @@ class Jetpack_Ai extends Product {
return __( 'Experimental tool to add AI to your editor', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized usage tier long description by tier
*
* @param int $tier The usage tier.
* @return string
*/
public static function get_long_description_by_usage_tier( $tier ) {
$long_descriptions = array(
1 => __( 'Jetpack AI Assistant brings the power of AI right into your WordPress editor, letting your content creation soar to new heights.', 'jetpack-my-jetpack' ),
100 => __( 'The most advanced AI technology Jetpack has to offer.', 'jetpack-my-jetpack' ),
);
$tiered_description = __( 'Upgrade and increase the amount of your available monthly requests to continue using the most advanced AI technology Jetpack has to offer.', 'jetpack-my-jetpack' );
return isset( $long_descriptions[ $tier ] ) ? $long_descriptions[ $tier ] : $tiered_description;
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Jetpack AI Assistant brings the power of AI right into your WordPress editor, letting your content creation soar to new heights.', 'jetpack-my-jetpack' );
$next_tier = self::get_next_usage_tier();
return self::get_long_description_by_usage_tier( $next_tier );
}
/**
* Get the internationalized usage tier features by tier
*
* @param int $tier The usage tier.
* @return string
*/
public static function get_features_by_usage_tier( $tier ) {
$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' ),
),
);
$tiered_features = array(
__( 'Prompt based content generation', 'jetpack-my-jetpack' ),
__( 'Generate text, tables, and lists', 'jetpack-my-jetpack' ),
__( 'Adaptive tone adjustment', 'jetpack-my-jetpack' ),
__( 'Superior spelling and grammar correction', 'jetpack-my-jetpack' ),
__( 'Title & summary generation', 'jetpack-my-jetpack' ),
__( 'Priority support', 'jetpack-my-jetpack' ),
/* translators: %d is the number of requests. */
sprintf( __( 'Up to %d requests per month', 'jetpack-my-jetpack' ), $tier ),
);
return isset( $features[ $tier ] ) ? $features[ $tier ] : $tiered_features;
}
/**
* Get the internationalized features list
*
* @return array CRM features list
* @return array Jetpack AI features list
*/
public static function get_features() {
return 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' ),
);
$next_tier = self::get_next_usage_tier();
return self::get_features_by_usage_tier( $next_tier );
}
/**
* Get the product princing details
* Get the product pricing details by tier
*
* @param int $tier The usage tier.
* @return array Pricing details
*/
public static function get_pricing_for_ui_by_usage_tier( $tier ) {
// Bail early if the site is not connected.
if ( ! self::is_site_connected() ) {
return array();
}
$product = Wpcom_Products::get_product( static::get_wpcom_product_slug() );
if ( empty( $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;
/*
* when tiers are enabled and the price tier list is empty,
* we may need to renew the cache for the product data so
* we get the new price tier list.
*
* if the list is still empty after the fresh data, we will
* default to empty pricing (by returning an empty array).
*/
if ( empty( $product->price_tier_list ) && $tier_plans_enabled ) {
$product = Wpcom_Products::get_product( static::get_wpcom_product_slug(), true );
}
// get the base pricing for the unlimited plan, for compatibility
$base_pricing = Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() );
$price_tier_list = $product->price_tier_list;
$yearly_prices = array();
foreach ( $price_tier_list as $price_tier ) {
if ( isset( $price_tier->maximum_units ) && isset( $price_tier->maximum_price ) ) {
// The prices are in cents
$yearly_prices[ $price_tier->maximum_units ] = $price_tier->maximum_price / 100;
}
}
// add the base pricing to the list
$prices = array( 1 => $base_pricing );
foreach ( $yearly_prices as $units => $price ) {
$prices[ $units ] = array_merge(
$base_pricing,
array(
'full_price' => $price,
'discount_price' => $price,
'is_introductory_offer' => false,
'introductory_offer' => null,
)
);
}
return isset( $prices[ $tier ] ) ? $prices[ $tier ] : array();
}
/**
* Get the product pricing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
$next_tier = self::get_next_usage_tier();
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => static::get_wpcom_product_slug(),
),
Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() )
self::get_pricing_for_ui_by_usage_tier( $next_tier )
);
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return ?string
* @return string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_ai_yearly';
@ -134,12 +295,21 @@ class Jetpack_Ai extends Product {
/**
* Get the WPCOM monthly product slug used to make the purchase
*
* @return ?string
* @return string
*/
public static function get_wpcom_monthly_product_slug() {
return 'jetpack_ai_monthly';
}
/**
* Get the WPCOM bi-yearly product slug used to make the purchase
*
* @return string
*/
public static function get_wpcom_bi_yearly_product_slug() {
return 'jetpack_ai_bi_yearly';
}
/**
* Checks whether the current plan (or purchases) of the site already supports the product
*
@ -152,10 +322,13 @@ class Jetpack_Ai extends Product {
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
if ( 0 === strpos( $purchase->product_slug, static::get_wpcom_product_slug() ) ) {
if ( str_starts_with( $purchase->product_slug, static::get_wpcom_product_slug() ) ) {
return true;
}
if ( 0 === strpos( $purchase->product_slug, static::get_wpcom_monthly_product_slug() ) ) {
if ( str_starts_with( $purchase->product_slug, static::get_wpcom_monthly_product_slug() ) ) {
return true;
}
if ( str_starts_with( $purchase->product_slug, static::get_wpcom_bi_yearly_product_slug() ) ) {
return true;
}
}
@ -163,6 +336,23 @@ class Jetpack_Ai extends Product {
return false;
}
/**
* Checks whether the product can be upgraded to a different product.
*
* @return boolean
*/
public static function is_upgradable() {
$has_required_plan = self::has_required_plan();
$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 ) {
return false;
}
return true;
}
/**
* Get the URL where the user manages the product
*
@ -188,6 +378,11 @@ class Jetpack_Ai extends Product {
return array();
}
// Bail early if the site is not connected.
if ( ! self::is_site_connected() ) {
return array();
}
// Check if class exists. If not, try to require it once.
if ( ! class_exists( 'Jetpack_AI_Helper' ) ) {
$class_file_path = JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-ai-helper.php';
@ -202,4 +397,13 @@ class Jetpack_Ai extends Product {
return \Jetpack_AI_Helper::get_ai_assistance_feature();
}
/**
* Checks whether the site is connected to WordPress.com.
*
* @return boolean
*/
private static function is_site_connected() {
return ( new Connection_Manager() )->is_connected();
}
}

View File

@ -220,7 +220,7 @@ class Scan extends Module_Product {
*/
public static function get_post_checkout_url() {
if ( static::is_jetpack_plugin_active() ) {
return admin_url( 'admin.php?page=jetpack#/recommendations' );
return 'admin.php?page=jetpack#/recommendations';
}
// If Jetpack is not active, it means that the user has another standalone plugin active

View File

@ -173,8 +173,8 @@ class Security extends Module_Product {
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
if (
0 === strpos( $purchase->product_slug, 'jetpack_security' ) ||
0 === strpos( $purchase->product_slug, 'jetpack_complete' )
str_starts_with( $purchase->product_slug, 'jetpack_security' ) ||
str_starts_with( $purchase->product_slug, 'jetpack_complete' )
) {
return true;
}

View File

@ -124,7 +124,28 @@ class Social extends Hybrid_Product {
* @return string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_social';
return 'jetpack_social_basic_yearly';
}
/**
* Checks whether the current plan (or purchases) of the site already supports the product
*
* @return boolean
*/
public static function has_required_plan() {
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
// 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;
}
}
}
return false;
}
/**

View File

@ -166,7 +166,7 @@ class Starter extends Module_Product {
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
if ( 0 === strpos( $purchase->product_slug, 'jetpack_starter' ) ) {
if ( str_starts_with( $purchase->product_slug, 'jetpack_starter' ) ) {
return true;
}
}

View File

@ -29,6 +29,20 @@ class Stats extends Module_Product {
*/
public static $module_name = 'stats';
/**
* The Plugin slug associated with stats
*
* @var string|null
*/
public static $plugin_slug = self::JETPACK_PLUGIN_SLUG;
/**
* The Plugin file associated with stats
*
* @var string|null
*/
public static $plugin_filename = self::JETPACK_PLUGIN_FILENAME;
/**
* Get the internationalized product name
*
@ -72,32 +86,29 @@ class Stats extends Module_Product {
*/
public static function get_features() {
return array(
__( 'Instant access to upcoming features', 'jetpack-my-jetpack' ),
__( 'Real-time data on visitors', 'jetpack-my-jetpack' ),
__( 'Traffic stats and trends for post and pages', 'jetpack-my-jetpack' ),
__( 'Detailed statistics about links leading to your site', 'jetpack-my-jetpack' ),
__( 'GDPR compliant', 'jetpack-my-jetpack' ),
__( 'Access to upcoming advanced features', 'jetpack-my-jetpack' ),
__( 'Priority support', 'jetpack-my-jetpack' ),
__( 'Commercial use', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product princing details
* Get the product pricing details
* Only showing the pricing details for the commercial product
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => static::get_wpcom_product_slug(),
'wpcom_free_product_slug' => static::get_wpcom_free_product_slug(),
'wpcom_pwyw_product_slug' => static::get_wpcom_pwyw_product_slug(),
'available' => true,
'wpcom_product_slug' => static::get_wpcom_product_slug(),
),
// TODO: replace with `Wpcom_Products::get_product_pricing` once available.
// This is not yet used anywhere, so it's fine to leave it as is for now.
array(
'currency_code' => 'USD',
'full_price' => 10,
'discount_price' => 10,
'product_term' => 'month',
)
Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() )
);
}
@ -107,7 +118,7 @@ class Stats extends Module_Product {
* @return ?string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_stats_monthly';
return 'jetpack_stats_yearly';
}
/**
@ -140,10 +151,10 @@ class Stats extends Module_Product {
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
if ( 0 === strpos( $purchase->product_slug, 'jetpack_stats' ) ) {
if ( str_starts_with( $purchase->product_slug, 'jetpack_stats' ) ) {
return true;
}
if ( 0 === strpos( $purchase->product_slug, 'jetpack_complete' ) ) {
if ( str_starts_with( $purchase->product_slug, 'jetpack_complete' ) ) {
return true;
}
}
@ -162,23 +173,36 @@ class Stats extends Module_Product {
if ( is_wp_error( $purchases_data ) ) {
return false;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
if (
(
// Purchase is Jetpack Stats...
0 === strpos( $purchase->product_slug, 'jetpack_stats' ) &&
// but not Jetpack Stats Free...
false === strpos( $purchase->product_slug, 'free' )
) || 0 === strpos( $purchase->product_slug, 'jetpack_complete' )
) {
// Only Jetpack Stats paid plans should be eligible for this conditional.
// Sample product slugs: jetpack_stats_monthly
// For now, only the free and commercial tiered subs show as upgradable
$upgradeable_stats_purchases = array_filter(
$purchases_data,
static function ( $purchase ) {
// Free plan is upgradeable
if ( $purchase->product_slug === 'jetpack_stats_free_yearly' ) {
return true;
// Commercial plans are upgradeable if they have a tier
} elseif (
in_array(
$purchase->product_slug,
array( 'jetpack_stats_yearly', 'jetpack_stats_monthly', 'jetpack_stats_bi_yearly' ),
true
) &&
! empty( $purchase->current_price_tier_slug )
) {
return true;
}
return false;
}
}
);
return ! empty( $upgradeable_stats_purchases );
}
return true;
// If there are no plans found, don't consider the product as upgradeable
return false;
}
/**
@ -195,9 +219,18 @@ class Stats extends Module_Product {
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
if (
0 === strpos( $purchase->product_slug, static::get_wpcom_free_product_slug() )
str_starts_with( $purchase->product_slug, static::get_wpcom_free_product_slug() )
) {
return '&productType=personal';
} elseif (
in_array(
$purchase->product_slug,
array( 'jetpack_stats_yearly', 'jetpack_stats_monthly', 'jetpack_stats_bi_yearly' ),
true
) &&
! empty( $purchase->current_price_tier_slug )
) {
return '&productType=commercial';
}
}
}

View File

@ -7,6 +7,7 @@
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\Current_Plan;
use Automattic\Jetpack\My_Jetpack\Hybrid_Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
@ -163,7 +164,7 @@ class Videopress extends Hybrid_Product {
* @return boolean
*/
public static function has_required_plan() {
// TODO: import and perform a proper check with Current_Plan. See #33410.
return true;
// using second argument `true` to force fetching from wpcom
return Current_Plan::supports( 'videopress-1tb-storage', true );
}
}