Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
45.91% |
73 / 159 |
|
50.00% |
7 / 14 |
CRAP | |
0.00% |
0 / 1 |
| Products | |
45.91% |
73 / 159 |
|
50.00% |
7 / 14 |
253.62 | |
0.00% |
0 / 1 |
| get_products_classes | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
5 | |||
| register_product_endpoints | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| get_not_shown_products | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| get_products | |
72.73% |
8 / 11 |
|
0.00% |
0 / 1 |
5.51 | |||
| get_products_api_data | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
5 | |||
| get_products_by_ownership | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
30 | |||
| get_all_plugin_filenames | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
| get_product | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| get_product_class | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| get_products_slugs | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| get_product_data_schema | |
0.00% |
0 / 41 |
|
0.00% |
0 / 1 |
2 | |||
| extend_plugins_action_links | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
2 | |||
| get_interstitials_state | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| update_interstitials_state | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Class for manipulating products |
| 4 | * |
| 5 | * @package automattic/my-jetpack |
| 6 | */ |
| 7 | |
| 8 | namespace Automattic\Jetpack\My_Jetpack; |
| 9 | |
| 10 | /** |
| 11 | * A class for everything related to product handling in My Jetpack |
| 12 | */ |
| 13 | class Products { |
| 14 | /** |
| 15 | * Constants for the status of a product on a site |
| 16 | * |
| 17 | * @var string |
| 18 | */ |
| 19 | public const STATUS_SITE_CONNECTION_ERROR = 'site_connection_error'; |
| 20 | public const STATUS_USER_CONNECTION_ERROR = 'user_connection_error'; |
| 21 | public const STATUS_ACTIVE = 'active'; |
| 22 | public const STATUS_CAN_UPGRADE = 'can_upgrade'; |
| 23 | public const STATUS_EXPIRING_SOON = 'expiring'; |
| 24 | public const STATUS_EXPIRED = 'expired'; |
| 25 | public const STATUS_INACTIVE = 'inactive'; |
| 26 | public const STATUS_MODULE_DISABLED = 'module_disabled'; |
| 27 | public const STATUS_PLUGIN_ABSENT = 'plugin_absent'; |
| 28 | public const STATUS_PLUGIN_ABSENT_WITH_PLAN = 'plugin_absent_with_plan'; |
| 29 | public const STATUS_NEEDS_PLAN = 'needs_plan'; |
| 30 | public const STATUS_NEEDS_ACTIVATION = 'needs_activation'; |
| 31 | public const STATUS_NEEDS_FIRST_SITE_CONNECTION = 'needs_first_site_connection'; |
| 32 | public const STATUS_NEEDS_ATTENTION__WARNING = 'needs_attention_warning'; |
| 33 | public const STATUS_NEEDS_ATTENTION__ERROR = 'needs_attention_error'; |
| 34 | |
| 35 | public const INTERSTITIALS_OPTION_NAME = 'my_jetpack_products_interstitials_state'; |
| 36 | |
| 37 | /** |
| 38 | * List of statuses that display the module as disabled |
| 39 | * This is defined as the statuses in which the user willingly has the module disabled whether it be by |
| 40 | * default, uninstalling the plugin, disabling the module, or not renewing their plan. |
| 41 | * |
| 42 | * @var array |
| 43 | */ |
| 44 | public static $disabled_module_statuses = array( |
| 45 | self::STATUS_INACTIVE, |
| 46 | self::STATUS_MODULE_DISABLED, |
| 47 | self::STATUS_PLUGIN_ABSENT, |
| 48 | self::STATUS_PLUGIN_ABSENT_WITH_PLAN, |
| 49 | self::STATUS_NEEDS_ACTIVATION, |
| 50 | self::STATUS_NEEDS_FIRST_SITE_CONNECTION, |
| 51 | ); |
| 52 | |
| 53 | /** |
| 54 | * List of statuses that display the module as broken |
| 55 | * |
| 56 | * @var array |
| 57 | */ |
| 58 | public static $broken_module_statuses = array( |
| 59 | self::STATUS_SITE_CONNECTION_ERROR, |
| 60 | self::STATUS_USER_CONNECTION_ERROR, |
| 61 | ); |
| 62 | |
| 63 | /** |
| 64 | * List of statuses that display the module as needing attention with a warning |
| 65 | * |
| 66 | * @var array |
| 67 | */ |
| 68 | public static $warning_module_statuses = array( |
| 69 | self::STATUS_SITE_CONNECTION_ERROR, |
| 70 | self::STATUS_USER_CONNECTION_ERROR, |
| 71 | self::STATUS_PLUGIN_ABSENT_WITH_PLAN, |
| 72 | self::STATUS_NEEDS_PLAN, |
| 73 | self::STATUS_NEEDS_ATTENTION__ERROR, |
| 74 | self::STATUS_NEEDS_ATTENTION__WARNING, |
| 75 | ); |
| 76 | |
| 77 | /** |
| 78 | * List of statuses that display the module as active |
| 79 | * |
| 80 | * @var array |
| 81 | */ |
| 82 | public static $active_module_statuses = array( |
| 83 | self::STATUS_ACTIVE, |
| 84 | self::STATUS_CAN_UPGRADE, |
| 85 | ); |
| 86 | |
| 87 | /** |
| 88 | * List of statuses that display the module as active |
| 89 | * |
| 90 | * @var array |
| 91 | */ |
| 92 | public static $expiring_or_expired_module_statuses = array( |
| 93 | self::STATUS_EXPIRING_SOON, |
| 94 | self::STATUS_EXPIRED, |
| 95 | ); |
| 96 | |
| 97 | /** |
| 98 | * List of all statuses that a product can have |
| 99 | * |
| 100 | * @var array |
| 101 | */ |
| 102 | public static $all_statuses = array( |
| 103 | self::STATUS_SITE_CONNECTION_ERROR, |
| 104 | self::STATUS_USER_CONNECTION_ERROR, |
| 105 | self::STATUS_ACTIVE, |
| 106 | self::STATUS_CAN_UPGRADE, |
| 107 | self::STATUS_EXPIRING_SOON, |
| 108 | self::STATUS_EXPIRED, |
| 109 | self::STATUS_INACTIVE, |
| 110 | self::STATUS_MODULE_DISABLED, |
| 111 | self::STATUS_PLUGIN_ABSENT, |
| 112 | self::STATUS_PLUGIN_ABSENT_WITH_PLAN, |
| 113 | self::STATUS_NEEDS_PLAN, |
| 114 | self::STATUS_NEEDS_ACTIVATION, |
| 115 | self::STATUS_NEEDS_FIRST_SITE_CONNECTION, |
| 116 | self::STATUS_NEEDS_ATTENTION__WARNING, |
| 117 | self::STATUS_NEEDS_ATTENTION__ERROR, |
| 118 | ); |
| 119 | |
| 120 | /** |
| 121 | * Get the list of Products classes |
| 122 | * |
| 123 | * Here's where all the existing Products are registered |
| 124 | * |
| 125 | * @throws \Exception If the result of a filter has invalid classes. |
| 126 | * @return array List of class names |
| 127 | */ |
| 128 | public static function get_products_classes() { |
| 129 | $classes = array( |
| 130 | 'anti-spam' => Products\Anti_Spam::class, |
| 131 | 'backup' => Products\Backup::class, |
| 132 | 'boost' => Products\Boost::class, |
| 133 | 'crm' => Products\Crm::class, |
| 134 | 'creator' => Products\Creator::class, |
| 135 | 'extras' => Products\Extras::class, |
| 136 | 'jetpack-ai' => Products\Jetpack_Ai::class, |
| 137 | // TODO: Remove this duplicate class ('ai')? See: https://github.com/Automattic/jetpack/pull/35910#pullrequestreview-2456462227. |
| 138 | 'ai' => Products\Jetpack_Ai::class, |
| 139 | 'scan' => Products\Scan::class, |
| 140 | 'search' => Products\Search::class, |
| 141 | 'social' => Products\Social::class, |
| 142 | 'security' => Products\Security::class, |
| 143 | 'protect' => Products\Protect::class, |
| 144 | 'videopress' => Products\Videopress::class, |
| 145 | 'stats' => Products\Stats::class, |
| 146 | 'growth' => Products\Growth::class, |
| 147 | 'complete' => Products\Complete::class, |
| 148 | // Features. |
| 149 | 'newsletter' => Products\Newsletter::class, |
| 150 | 'site-accelerator' => Products\Site_Accelerator::class, |
| 151 | 'related-posts' => Products\Related_Posts::class, |
| 152 | ); |
| 153 | |
| 154 | /** |
| 155 | * This filter allows plugin to override the Product class of a given product. The new class must be a child class of the default one declared in My Jetpack |
| 156 | * |
| 157 | * For example, a stand-alone plugin could overwrite its product class to control specific behavior of the product in the My Jetpack page after it is active without having to commit changes to the My Jetpack package: |
| 158 | * |
| 159 | * add_filter( 'my_jetpack_products_classes', function( $classes ) { |
| 160 | * $classes['my_plugin'] = 'My_Plugin'; // a class that extends the original one declared in the My Jetpack package. |
| 161 | * return $classes |
| 162 | * } ); |
| 163 | * |
| 164 | * @param array $classes An array where the keys are the product slugs and the values are the class names. |
| 165 | */ |
| 166 | $final_classes = apply_filters( 'my_jetpack_products_classes', $classes ); |
| 167 | |
| 168 | // Check that the classes are still child of the same original classes. |
| 169 | foreach ( (array) $final_classes as $slug => $final_class ) { |
| 170 | if ( $final_class === $classes[ $slug ] ) { |
| 171 | continue; |
| 172 | } |
| 173 | if ( ! class_exists( $final_class ) || ! is_subclass_of( $final_class, $classes[ $slug ] ) ) { |
| 174 | throw new \Exception( 'You can only overwrite a Product class with a child of the original class.' ); |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | return $final_classes; |
| 179 | } |
| 180 | |
| 181 | /** |
| 182 | * Register endpoints related to product classes |
| 183 | * |
| 184 | * @return void |
| 185 | */ |
| 186 | public static function register_product_endpoints() { |
| 187 | $classes = self::get_products_classes(); |
| 188 | |
| 189 | foreach ( $classes as $class ) { |
| 190 | $class::register_endpoints(); |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | /** |
| 195 | * List of product slugs that are displayed on the main My Jetpack page |
| 196 | * |
| 197 | * @var array |
| 198 | */ |
| 199 | public static $shown_products = array( |
| 200 | 'anti-spam', |
| 201 | 'backup', |
| 202 | 'boost', |
| 203 | 'crm', |
| 204 | 'jetpack-ai', |
| 205 | 'search', |
| 206 | 'social', |
| 207 | 'protect', |
| 208 | 'videopress', |
| 209 | 'stats', |
| 210 | ); |
| 211 | |
| 212 | /** |
| 213 | * Gets the list of product slugs that are Not displayed on the main My Jetpack page |
| 214 | * |
| 215 | * @return array |
| 216 | */ |
| 217 | public static function get_not_shown_products() { |
| 218 | return array_diff( array_keys( static::get_products_classes() ), self::$shown_products ); |
| 219 | } |
| 220 | |
| 221 | /** |
| 222 | * Product data |
| 223 | * |
| 224 | * @param array $product_slugs (optional) An array of specified product slugs. |
| 225 | * @return array Jetpack products on the site and their availability. |
| 226 | */ |
| 227 | public static function get_products( $product_slugs = array() ) { |
| 228 | $all_classes = self::get_products_classes(); |
| 229 | $products = array(); |
| 230 | // If an array of $product_slugs are passed, return only the products specified in $product_slugs array. |
| 231 | if ( $product_slugs ) { |
| 232 | foreach ( $product_slugs as $product_slug ) { |
| 233 | if ( isset( $all_classes[ $product_slug ] ) ) { |
| 234 | $class = $all_classes[ $product_slug ]; |
| 235 | $products[ $product_slug ] = $class::get_info(); |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | return $products; |
| 240 | } |
| 241 | // Otherwise return All products. |
| 242 | foreach ( $all_classes as $slug => $class ) { |
| 243 | $products[ $slug ] = $class::get_info(); |
| 244 | } |
| 245 | |
| 246 | return $products; |
| 247 | } |
| 248 | |
| 249 | /** |
| 250 | * Get products data related to the wpcom api |
| 251 | * |
| 252 | * @param array $product_slugs - (optional) An array of specified product slugs. |
| 253 | * @return array |
| 254 | */ |
| 255 | public static function get_products_api_data( $product_slugs = array() ) { |
| 256 | $all_classes = self::get_products_classes(); |
| 257 | $products = array(); |
| 258 | // If an array of $product_slugs are passed, return only the products specified in $product_slugs array. |
| 259 | if ( $product_slugs ) { |
| 260 | foreach ( $product_slugs as $product_slug ) { |
| 261 | if ( isset( $all_classes[ $product_slug ] ) ) { |
| 262 | $class = $all_classes[ $product_slug ]; |
| 263 | $products[ $product_slug ] = $class::get_wpcom_info(); |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | return $products; |
| 268 | } |
| 269 | // Otherwise return All products. |
| 270 | foreach ( $all_classes as $slug => $class ) { |
| 271 | $products[ $slug ] = $class::get_wpcom_info(); |
| 272 | } |
| 273 | |
| 274 | return $products; |
| 275 | } |
| 276 | |
| 277 | /** |
| 278 | * Get a list of products sorted by whether or not the user owns them |
| 279 | * An owned product is defined as a product that is any of the following |
| 280 | * - Active |
| 281 | * - Has historically been active |
| 282 | * - The user has a plan that includes the product |
| 283 | * - The user has the standalone plugin for the product installed |
| 284 | * |
| 285 | * @param string $type The type of ownership to return ('owned' or 'unowned'). |
| 286 | * |
| 287 | * @return array |
| 288 | */ |
| 289 | public static function get_products_by_ownership( $type ) { |
| 290 | $owned_active_products = array(); |
| 291 | $owned_warning_products = array(); |
| 292 | $owned_inactive_products = array(); |
| 293 | $unowned_products = array(); |
| 294 | |
| 295 | foreach ( self::get_products_classes() as $class ) { |
| 296 | $product_slug = $class::$slug; |
| 297 | $status = $class::get_status(); |
| 298 | |
| 299 | if ( $class::is_owned() ) { |
| 300 | // This sorts the the products in the order of active -> warning -> inactive. |
| 301 | // This enables the frontend to display them in that order. |
| 302 | // This is not needed for unowned products as those will always have a status of 'inactive'. |
| 303 | if ( in_array( $status, self::$active_module_statuses, true ) ) { |
| 304 | array_push( $owned_active_products, $product_slug ); |
| 305 | } elseif ( in_array( $status, self::$warning_module_statuses, true ) ) { |
| 306 | array_push( $owned_warning_products, $product_slug ); |
| 307 | } else { |
| 308 | array_push( $owned_inactive_products, $product_slug ); |
| 309 | } |
| 310 | continue; |
| 311 | } |
| 312 | |
| 313 | array_push( $unowned_products, $product_slug ); |
| 314 | } |
| 315 | |
| 316 | $data = array( |
| 317 | 'owned' => array_values( |
| 318 | array_unique( |
| 319 | array_merge( |
| 320 | $owned_active_products, |
| 321 | $owned_warning_products, |
| 322 | $owned_inactive_products |
| 323 | ) |
| 324 | ) |
| 325 | ), |
| 326 | 'unowned' => array_values( |
| 327 | array_unique( $unowned_products ) |
| 328 | ), |
| 329 | ); |
| 330 | |
| 331 | return $data[ $type ]; |
| 332 | } |
| 333 | |
| 334 | /** |
| 335 | * Get all plugin filenames associated with the products. |
| 336 | * |
| 337 | * @return array |
| 338 | */ |
| 339 | public static function get_all_plugin_filenames() { |
| 340 | $filenames = array(); |
| 341 | foreach ( self::get_products_classes() as $class ) { |
| 342 | if ( ! isset( $class::$plugin_filename ) ) { |
| 343 | continue; |
| 344 | } |
| 345 | |
| 346 | if ( is_array( $class::$plugin_filename ) ) { |
| 347 | $filenames = array_merge( $filenames, $class::$plugin_filename ); |
| 348 | } else { |
| 349 | $filenames[] = $class::$plugin_filename; |
| 350 | } |
| 351 | } |
| 352 | return $filenames; |
| 353 | } |
| 354 | |
| 355 | /** |
| 356 | * Get one product data by its slug |
| 357 | * |
| 358 | * @param string $product_slug The product slug. |
| 359 | * |
| 360 | * @return ?array |
| 361 | */ |
| 362 | public static function get_product( $product_slug ) { |
| 363 | $classes = self::get_products_classes(); |
| 364 | if ( isset( $classes[ $product_slug ] ) ) { |
| 365 | return $classes[ $product_slug ]::get_info(); |
| 366 | } |
| 367 | } |
| 368 | |
| 369 | /** |
| 370 | * Get one product Class name |
| 371 | * |
| 372 | * @param string $product_slug The product slug. |
| 373 | * |
| 374 | * @return ?string |
| 375 | */ |
| 376 | public static function get_product_class( $product_slug ) { |
| 377 | $classes = self::get_products_classes(); |
| 378 | if ( isset( $classes[ $product_slug ] ) ) { |
| 379 | return $classes[ $product_slug ]; |
| 380 | } |
| 381 | } |
| 382 | |
| 383 | /** |
| 384 | * Return product slugs list. |
| 385 | * |
| 386 | * @return array Product slugs array. |
| 387 | */ |
| 388 | public static function get_products_slugs() { |
| 389 | return array_keys( self::get_products_classes() ); |
| 390 | } |
| 391 | |
| 392 | /** |
| 393 | * Gets the json schema for the product data |
| 394 | * |
| 395 | * @return array |
| 396 | */ |
| 397 | public static function get_product_data_schema() { |
| 398 | return array( |
| 399 | 'title' => 'The requested product data', |
| 400 | 'type' => 'object', |
| 401 | 'properties' => array( |
| 402 | 'product' => array( |
| 403 | 'description' => __( 'Product slug', 'jetpack-my-jetpack' ), |
| 404 | 'type' => 'string', |
| 405 | 'enum' => __CLASS__ . '::get_product_slugs', |
| 406 | 'required' => false, |
| 407 | 'validate_callback' => __CLASS__ . '::check_product_argument', |
| 408 | ), |
| 409 | 'action' => array( |
| 410 | 'description' => __( 'Production action to execute', 'jetpack-my-jetpack' ), |
| 411 | 'type' => 'string', |
| 412 | 'enum' => array( 'activate', 'deactivate' ), |
| 413 | 'required' => false, |
| 414 | 'validate_callback' => __CLASS__ . '::check_product_argument', |
| 415 | ), |
| 416 | 'slug' => array( |
| 417 | 'title' => 'The product slug', |
| 418 | 'type' => 'string', |
| 419 | ), |
| 420 | 'name' => array( |
| 421 | 'title' => 'The product name', |
| 422 | 'type' => 'string', |
| 423 | ), |
| 424 | 'description' => array( |
| 425 | 'title' => 'The product description', |
| 426 | 'type' => 'string', |
| 427 | ), |
| 428 | 'status' => array( |
| 429 | 'title' => 'The product status', |
| 430 | 'type' => 'string', |
| 431 | 'enum' => self::$all_statuses, |
| 432 | ), |
| 433 | 'class' => array( |
| 434 | 'title' => 'The product class handler', |
| 435 | 'type' => 'string', |
| 436 | ), |
| 437 | ), |
| 438 | ); |
| 439 | } |
| 440 | |
| 441 | /** |
| 442 | * Extend actions links for plugins |
| 443 | * tied to the Products. |
| 444 | */ |
| 445 | public static function extend_plugins_action_links() { |
| 446 | $products = array( |
| 447 | 'backup', |
| 448 | 'boost', |
| 449 | 'crm', |
| 450 | 'videopress', |
| 451 | 'social', |
| 452 | 'protect', |
| 453 | 'crm', |
| 454 | 'search', |
| 455 | 'jetpack-ai', |
| 456 | ); |
| 457 | |
| 458 | // Add plugin action links for the core Jetpack plugin. |
| 459 | Product::extend_core_plugin_action_links(); |
| 460 | |
| 461 | // Add plugin action links to standalone products. |
| 462 | foreach ( $products as $product ) { |
| 463 | $class_name = self::get_product_class( $product ); |
| 464 | $class_name::extend_plugin_action_links(); |
| 465 | } |
| 466 | } |
| 467 | |
| 468 | /** |
| 469 | * Get interstitials state for the products |
| 470 | * |
| 471 | * @return array A key-value array of product slugs and their interstitial states. True means the interstitial was seen by the user for that product. |
| 472 | */ |
| 473 | public static function get_interstitials_state() { |
| 474 | return get_option( self::INTERSTITIALS_OPTION_NAME, array() ); |
| 475 | } |
| 476 | |
| 477 | /** |
| 478 | * Update interstitials state for the products |
| 479 | * |
| 480 | * @param array $new_state A key-value array of product slugs and their interstitial states. |
| 481 | * |
| 482 | * @return bool True if the option was updated successfully, false otherwise. |
| 483 | */ |
| 484 | public static function update_interstitials_state( $new_state ) { |
| 485 | |
| 486 | // Merge the existing interstitials state with the new state. |
| 487 | $interstitials_state = array_merge( self::get_interstitials_state(), $new_state ); |
| 488 | |
| 489 | return update_option( self::INTERSTITIALS_OPTION_NAME, $interstitials_state ); |
| 490 | } |
| 491 | } |