Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
17.88% |
32 / 179 |
|
0.00% |
0 / 11 |
CRAP | n/a |
0 / 0 |
|
| _wpcom_get_current_blog_id | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
5.93 | |||
| wpcom_site_has_feature | |
64.71% |
11 / 17 |
|
0.00% |
0 / 1 |
24.89 | |||
| wpcom_site_can_upload_videos | |
60.00% |
6 / 10 |
|
0.00% |
0 / 1 |
8.30 | |||
| wpcom_get_site_purchases | |
64.29% |
9 / 14 |
|
0.00% |
0 / 1 |
7.64 | |||
| _wpcom_features_get_simple_site_purchases | |
0.00% |
0 / 75 |
|
0.00% |
0 / 1 |
240 | |||
| wpcom_datetime_to_iso8601 | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
| wpcom_product_has_feature | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
| wpcom_purchase_has_feature | |
20.00% |
2 / 10 |
|
0.00% |
0 / 1 |
4.05 | |||
| wpcom_get_product_features | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
182 | |||
| _convert_product_to_purchase | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
72 | |||
| wpcom_feature_exists | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * THIS FILE EXISTS VERBATIM IN WPCOM AND WPCOMSH. |
| 4 | * |
| 5 | * DANGER DANGER DANGER!!! |
| 6 | * If you make any changes to this file you must MANUALLY update this file in both WPCOM and WPCOMSH. |
| 7 | * |
| 8 | * This file provides WPCOM_Features class wrapper functions that make checking for a specific feature easy and uniform |
| 9 | * across WPCOM and WPCOMSH. |
| 10 | * |
| 11 | * @package WPCOM_Features |
| 12 | */ |
| 13 | |
| 14 | /** |
| 15 | * Load `WPCOM_Features` class. |
| 16 | */ |
| 17 | require_once __DIR__ . '/class-wpcom-features.php'; |
| 18 | |
| 19 | /** |
| 20 | * Internal function to retrieve the current WP.com blog ID depending on the environment. |
| 21 | * |
| 22 | * @return int The current blog ID. |
| 23 | */ |
| 24 | function _wpcom_get_current_blog_id() { |
| 25 | if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { |
| 26 | return get_current_blog_id(); |
| 27 | } |
| 28 | |
| 29 | /* |
| 30 | * Atomic sites have the WP.com blog ID stored as a Jetpack option. This code deliberately |
| 31 | * doesn't use `Jetpack_Options::get_option` so it works even when Jetpack has not been loaded. |
| 32 | */ |
| 33 | $jetpack_options = get_option( 'jetpack_options' ); |
| 34 | if ( is_array( $jetpack_options ) && isset( $jetpack_options['id'] ) ) { |
| 35 | return (int) $jetpack_options['id']; |
| 36 | } |
| 37 | |
| 38 | return get_current_blog_id(); |
| 39 | } |
| 40 | |
| 41 | /** |
| 42 | * Whether a given feature is available to the current (or specified) site. |
| 43 | * |
| 44 | * This function pulls the purchases for a given site and uses WPCOM_Features to check if any of those purchases |
| 45 | * include the requested $feature. |
| 46 | * |
| 47 | * @param string $feature A singular feature. |
| 48 | * @param int $blog_id Optional. Blog ID. Defaults to current blog. |
| 49 | * |
| 50 | * @return bool Does the site have the feature? |
| 51 | */ |
| 52 | function wpcom_site_has_feature( $feature, $blog_id = 0 ) { |
| 53 | if ( ! $blog_id ) { |
| 54 | $blog_id = _wpcom_get_current_blog_id(); |
| 55 | } |
| 56 | |
| 57 | $blog = null; |
| 58 | if ( defined( 'IS_ATOMIC' ) && IS_ATOMIC ) { |
| 59 | $site_type = 'wpcom'; |
| 60 | } else { |
| 61 | $blog = get_blog_details( $blog_id, false ); |
| 62 | $site_type = is_blog_wpcom( $blog ) || is_blog_atomic( $blog ) ? 'wpcom' : 'jetpack'; |
| 63 | } |
| 64 | |
| 65 | // A8C override for certain sites. |
| 66 | if ( $feature === WPCOM_Features::ADVANCED_SEO && in_array( $blog_id, WPCOM_FEATURES::A8C_SITES_WITH_ADDITIONAL_SEO_FEATURES, true ) ) { |
| 67 | return true; |
| 68 | } |
| 69 | |
| 70 | /* |
| 71 | * A8C override for internal P2s |
| 72 | */ |
| 73 | if ( $feature === WPCOM_Features::AI_ASSISTANT && ( function_exists( 'wpcom_is_automattic_p2_site' ) && wpcom_is_automattic_p2_site( $blog_id ) ) ) { |
| 74 | return true; |
| 75 | } |
| 76 | |
| 77 | /* |
| 78 | * A8C override for wp.org sites to enable JP search |
| 79 | */ |
| 80 | if ( $feature === WPCOM_Features::CLASSIC_SEARCH && ( function_exists( 'wpcom_is_wporg_jp_index' ) && wpcom_is_wporg_jp_index( $blog_id ) ) ) { |
| 81 | return true; |
| 82 | } |
| 83 | |
| 84 | $purchases = wpcom_get_site_purchases( $blog_id ); |
| 85 | |
| 86 | if ( isset( $blog->registered ) ) { |
| 87 | WPCOM_Features::add_free_plan_purchase( $purchases, $site_type, $blog->registered ); |
| 88 | } |
| 89 | |
| 90 | return WPCOM_Features::has_feature( $feature, $purchases, $site_type, $blog_id ); |
| 91 | } |
| 92 | |
| 93 | /** |
| 94 | * Find out if the site can upload video files. |
| 95 | * |
| 96 | * This checks if the site has either VideoPress or the general video upload capability. |
| 97 | * Sites with the UPLOAD_VIDEO_FILES feature can upload videos even without VideoPress |
| 98 | * (e.g. Premium plans with the gating-business-q1 sticker). |
| 99 | * |
| 100 | * @param int $blog_id Blog ID. Defaults to the current blog ID if none is passed. |
| 101 | * @return bool Whether the site can upload video files. |
| 102 | */ |
| 103 | function wpcom_site_can_upload_videos( $blog_id = 0 ) { |
| 104 | if ( ! $blog_id ) { |
| 105 | $blog_id = _wpcom_get_current_blog_id(); |
| 106 | } |
| 107 | |
| 108 | // VideoPress includes video upload capability. |
| 109 | // On WPCOM, use wpcom_site_has_videopress() to respect the filter. |
| 110 | // On WPCOMSH/Atomic, that function doesn't exist so use direct feature check. |
| 111 | if ( function_exists( 'wpcom_site_has_videopress' ) ) { |
| 112 | if ( wpcom_site_has_videopress( $blog_id ) ) { |
| 113 | return true; |
| 114 | } |
| 115 | } elseif ( wpcom_site_has_feature( WPCOM_Features::VIDEOPRESS, $blog_id ) ) { |
| 116 | return true; |
| 117 | } |
| 118 | |
| 119 | // Check for the general video upload feature (Premium+ plans with gating-business-q1 sticker). |
| 120 | if ( wpcom_site_has_feature( WPCOM_Features::UPLOAD_VIDEO_FILES, $blog_id ) ) { |
| 121 | return true; |
| 122 | } |
| 123 | |
| 124 | /** |
| 125 | * Filters whether the site can upload video files. |
| 126 | * |
| 127 | * @param bool $can_upload_videos Whether the site can upload video files. |
| 128 | * @param int $blog_id Blog ID. |
| 129 | */ |
| 130 | return apply_filters( 'wpcom_site_can_upload_videos', false, $blog_id ); |
| 131 | } |
| 132 | |
| 133 | /** |
| 134 | * Returns a list of purchased products. |
| 135 | * |
| 136 | * This function checks if we're on an Atomic (WPCOMSH) or Simple (WPCOM) site, and pulls the purchases for that current |
| 137 | * site. |
| 138 | * |
| 139 | * @throws Error If $blog_id !== current_blog_id on Atomic sites. |
| 140 | * |
| 141 | * @param int $blog_id Optional. Blog ID. Defaults to current blog. |
| 142 | * |
| 143 | * @return array An array of product objects containing product_slug, product_id, subscribed_date, and expiry_date. |
| 144 | */ |
| 145 | function wpcom_get_site_purchases( $blog_id = 0 ) { |
| 146 | if ( ! $blog_id ) { |
| 147 | $blog_id = _wpcom_get_current_blog_id(); |
| 148 | } |
| 149 | |
| 150 | if ( defined( 'IS_ATOMIC' ) && IS_ATOMIC ) { |
| 151 | if ( _wpcom_get_current_blog_id() !== $blog_id ) { |
| 152 | throw new Error( |
| 153 | 'Atomic sites do not support looking up features for sites other than the current site.' |
| 154 | ); |
| 155 | } |
| 156 | |
| 157 | // Atomic site (WPCOMSH) purchases are stored in Atomic Persistent Data as a JSON encoded string. |
| 158 | $persistent_data = new Atomic_Persistent_Data(); |
| 159 | |
| 160 | if ( ! $persistent_data->WPCOM_PURCHASES ) { // phpcs:ignore WordPress.NamingConventions |
| 161 | return array(); |
| 162 | } |
| 163 | |
| 164 | $purchases = (array) json_decode( $persistent_data->WPCOM_PURCHASES ); // phpcs:ignore WordPress.NamingConventions |
| 165 | |
| 166 | } else { |
| 167 | // Allow overriding the blog ID for feature checks. |
| 168 | $blog_id = apply_filters( 'wpcom_site_has_feature_blog_id', $blog_id ); |
| 169 | |
| 170 | $purchases = _wpcom_features_get_simple_site_purchases( $blog_id ); |
| 171 | } |
| 172 | |
| 173 | return $purchases; |
| 174 | } |
| 175 | |
| 176 | /** |
| 177 | * INTERNAL function to fetch purchases for a WPCOM Simple site. |
| 178 | * The function will return an empty array if we're running in an Atomic context. |
| 179 | * |
| 180 | * @param int $blog_id The blog ID to fetch purchases for. |
| 181 | * @return array The currently active purchases on the site. |
| 182 | */ |
| 183 | function _wpcom_features_get_simple_site_purchases( $blog_id ) { |
| 184 | global $wpdb; |
| 185 | |
| 186 | if ( defined( 'IS_ATOMIC' ) && IS_ATOMIC ) { |
| 187 | // Make _super_ sure the function is available. |
| 188 | if ( function_exists( '_doing_it_wrong' ) ) { |
| 189 | _doing_it_wrong( |
| 190 | __FUNCTION__, |
| 191 | 'Support for this function is only in available in contexts where the WordPress.com Store databases are available.', |
| 192 | false // No version. |
| 193 | ); |
| 194 | } |
| 195 | |
| 196 | return array(); |
| 197 | } |
| 198 | |
| 199 | if ( ! $blog_id ) { |
| 200 | $blog_id = _wpcom_get_current_blog_id(); |
| 201 | } |
| 202 | |
| 203 | // Optional skip, used for suspended/spam/deleted sites. |
| 204 | $skip_purchase_lookup = apply_filters( 'wpcom_simple_skip_purchase_lookup', false, $blog_id ); |
| 205 | if ( $skip_purchase_lookup ) { |
| 206 | return array(); |
| 207 | } |
| 208 | |
| 209 | // 'site_purchases' belong to $global_groups in ./wp-content/object-cache.php |
| 210 | $wp_cache_group = 'site_purchases'; |
| 211 | $wp_cache_found = false; |
| 212 | |
| 213 | // The DB table is included in $wp_cache_key to avoid cache pollution between the production and test store. |
| 214 | $wp_cache_key = "$blog_id-{$wpdb->store_subscriptions}"; |
| 215 | |
| 216 | // Check wp_cache_get() for $purchases. If none exist $wp_cache_found will be false. |
| 217 | $purchases = wp_cache_get( $wp_cache_key, $wp_cache_group, false, $wp_cache_found ); |
| 218 | |
| 219 | if ( false !== $wp_cache_found ) { |
| 220 | return (array) $purchases; |
| 221 | } |
| 222 | |
| 223 | $lock_key = "{$wp_cache_key}_building"; |
| 224 | $lock_found = false; |
| 225 | // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable |
| 226 | $lock = wp_cache_get( $lock_key, $wp_cache_group, false, $lock_found ); |
| 227 | if ( $lock_found ) { |
| 228 | // Another request is already rebuilding the purchases cache. |
| 229 | // Let's try 2 short sleeps of 200ms to see if that finishes, and if not, |
| 230 | // return an empty set of purchases. The DB must be backed up and we don't |
| 231 | // want to contribute to the problem. |
| 232 | $attempts = 2; |
| 233 | while ( $attempts > 0 ) { |
| 234 | // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable |
| 235 | $lock = wp_cache_get( $lock_key, $wp_cache_group, true /* force remote recheck */, $lock_found ); |
| 236 | if ( ! $lock_found ) { |
| 237 | // The lock is no longer in cache, so we can break out of the loop. |
| 238 | break; |
| 239 | } |
| 240 | // Sleep 200ms |
| 241 | usleep( 200000 ); |
| 242 | --$attempts; |
| 243 | } |
| 244 | |
| 245 | // Has the first request has populated the cache? |
| 246 | $purchases = wp_cache_get( $wp_cache_key, $wp_cache_group, true /* force remote recheck */, $wp_cache_found ); |
| 247 | if ( false !== $wp_cache_found ) { |
| 248 | return (array) $purchases; |
| 249 | } |
| 250 | |
| 251 | // It's still not there, return [] as a fallback; |
| 252 | // Also let future checks for this blog for the life of this request fail |
| 253 | $target_blog_id = $blog_id; |
| 254 | add_filter( |
| 255 | 'wpcom_simple_skip_purchase_lookup', |
| 256 | function ( $skip, $checked_blog_id ) use ( $target_blog_id ) { |
| 257 | if ( $checked_blog_id === $target_blog_id ) { |
| 258 | return true; // Skip purchases lookup |
| 259 | } |
| 260 | return $skip; |
| 261 | }, |
| 262 | 10, |
| 263 | 2 |
| 264 | ); |
| 265 | return array(); |
| 266 | } |
| 267 | |
| 268 | // Let other requests know that we are rebuilding purchases, to stop multiples of the same |
| 269 | // request from stacking up. |
| 270 | wp_cache_set( $lock_key, true, $wp_cache_group, 30 ); // (30 second TTL) |
| 271 | |
| 272 | // Get $purchases with a direct SQL query. |
| 273 | // We are intentionally NOT using the Purchases API as this code needs to be runnable |
| 274 | // in some contexts where the billing code-base is not available. |
| 275 | // See pdqkMK-18D-p2 for more discussion and context. |
| 276 | |
| 277 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery |
| 278 | $purchases = $wpdb->get_results( |
| 279 | $wpdb->prepare( |
| 280 | " |
| 281 | SELECT |
| 282 | product.product_slug, |
| 283 | product.product_id, |
| 284 | product.billing_product_id, |
| 285 | product.product_type, |
| 286 | subscription.subscribed_date, |
| 287 | subscription.expiry AS 'expiry_date', |
| 288 | subscription.id AS subscription_id, |
| 289 | subscription.auto_renew AS user_allows_auto_renew |
| 290 | FROM `$wpdb->store_subscriptions` AS subscription |
| 291 | LEFT JOIN `$wpdb->store_products` AS product ON subscription.product_id = product.product_id |
| 292 | WHERE |
| 293 | subscription.blog_id = %d |
| 294 | AND subscription.active = 1 |
| 295 | ORDER BY subscription.id DESC |
| 296 | ", |
| 297 | $blog_id |
| 298 | ) |
| 299 | ); |
| 300 | |
| 301 | static $billing_product_data = array(); |
| 302 | |
| 303 | $billing_product_ids = array_unique( wp_list_pluck( $purchases, 'billing_product_id' ) ); |
| 304 | $billing_product_ids_to_query = array_diff( $billing_product_ids, array_keys( $billing_product_data ) ); |
| 305 | if ( ! empty( $billing_product_ids_to_query ) ) { |
| 306 | // We need to query the billing_products table via a separate query. |
| 307 | |
| 308 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery |
| 309 | $billing_products = $wpdb->get_results( |
| 310 | " |
| 311 | SELECT |
| 312 | product_id, |
| 313 | product_slug |
| 314 | FROM |
| 315 | `$wpdb->billing_products` |
| 316 | WHERE |
| 317 | " . $wpdb->build_IN_condition( 'product_id', $billing_product_ids_to_query, '%d' ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared |
| 318 | ); |
| 319 | |
| 320 | foreach ( $billing_products as $billing_product ) { |
| 321 | $billing_product_data[ $billing_product->product_id ] = $billing_product->product_slug; |
| 322 | } |
| 323 | } |
| 324 | |
| 325 | // Format the dates to match WPCOMSH data. |
| 326 | foreach ( $purchases as $purchase ) { |
| 327 | $purchase->billing_product_slug = $billing_product_data[ $purchase->billing_product_id ] ?? ''; |
| 328 | $purchase->subscribed_date = wpcom_datetime_to_iso8601( $purchase->subscribed_date ); |
| 329 | $purchase->expiry_date = wpcom_datetime_to_iso8601( $purchase->expiry_date ); |
| 330 | $purchase->user_allows_auto_renew = ! empty( $purchase->user_allows_auto_renew ); |
| 331 | // Ensure we remove billing_product_id from the purchase data. |
| 332 | unset( $purchase->billing_product_id ); |
| 333 | } |
| 334 | |
| 335 | /* |
| 336 | * Cache the $purchases for 6-8 hours. Otherwise, the cache is invalidated when a purchase is made, using: |
| 337 | * add_action( 'subscription_changed', 'clear_wp_cache_site_purchases', 10, 1 ); |
| 338 | * Found in ./wp-content/mu-plugins/wpcom-features.php |
| 339 | */ |
| 340 | // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_mt_rand |
| 341 | wp_cache_set( $wp_cache_key, $purchases, $wp_cache_group, ( 6 * HOUR_IN_SECONDS ) + mt_rand( 1, 2 * 60 * MINUTE_IN_SECONDS ) ); |
| 342 | wp_cache_delete( $lock_key, $wp_cache_group ); // Release the lock telling other requests we are building purchases. |
| 343 | |
| 344 | return $purchases; |
| 345 | } |
| 346 | |
| 347 | /** |
| 348 | * Parse and format a date string to ISO8601, but fall back to $default if the string is bad or '0000-00-00'. |
| 349 | * |
| 350 | * @param string $date A string representing a datetime that we wish to format to ISO8601. |
| 351 | * @param string $default Use this datetime if $date errors. Useful for predictable unit testing. Defaults to 'now'. |
| 352 | * |
| 353 | * @return string A date string in ISO8601 format. |
| 354 | */ |
| 355 | function wpcom_datetime_to_iso8601( $date, $default = 'now' ) { |
| 356 | /* |
| 357 | * Datetimes containing '0000-00-00' convert to '-001-11-30T00:00:00+00:00' which is not useful, so set it or |
| 358 | * empty $date to $default. |
| 359 | */ |
| 360 | if ( empty( $date ) || false !== strpos( $date, '0000-00-00' ) ) { |
| 361 | $date = $default; |
| 362 | } |
| 363 | |
| 364 | try { |
| 365 | return ( new DateTime( $date ) )->format( 'c' ); |
| 366 | } catch ( Exception $e ) { |
| 367 | return ( new DateTime( $default ) )->format( 'c' ); |
| 368 | } |
| 369 | } |
| 370 | |
| 371 | /** |
| 372 | * Checks whether the given product contains the passed feature. |
| 373 | * |
| 374 | * This function converts atomic supported plan slugs and other product aliases to product objects. It then uses |
| 375 | * WPCOM_Features to check if product include the requested $feature. |
| 376 | * |
| 377 | * Do not pass a Store_Subscription to this function. For that case, use wpcom_purchase_has_feature(). |
| 378 | * |
| 379 | * @param string|int|Store_Product $product A Store_Product object, a product slug, or ID. |
| 380 | * @param string $feature The name of the feature to check. |
| 381 | * |
| 382 | * @return bool |
| 383 | */ |
| 384 | function wpcom_product_has_feature( $product, $feature ) { |
| 385 | if ( defined( 'IS_ATOMIC' ) && IS_ATOMIC ) { |
| 386 | _doing_it_wrong( |
| 387 | __FUNCTION__, |
| 388 | 'Support for this function is only in available in contexts where the store products database is available.', |
| 389 | '' // No version. |
| 390 | ); |
| 391 | return false; |
| 392 | } |
| 393 | |
| 394 | $purchase = _convert_product_to_purchase( $product ); |
| 395 | if ( ! $purchase ) { |
| 396 | return false; |
| 397 | } |
| 398 | |
| 399 | return wpcom_purchase_has_feature( $purchase, $feature ); |
| 400 | } |
| 401 | |
| 402 | /** |
| 403 | * Checks whether the given purchase (Store_Subscription) contains the passed feature. |
| 404 | * |
| 405 | * This function is similar to `wpcom_product_has_feature` with the difference that this function can check for legacy |
| 406 | * features because purchases contain a `subscribed_date` field whereas products do not. |
| 407 | * |
| 408 | * Do not pass a Store_Product to this function. For that case, use wpcom_product_has_feature(). |
| 409 | * |
| 410 | * @param Store_Subscription|object $purchase A Store_Subscription object or purchase serialized object. |
| 411 | * @param string $feature The name of the feature to check. |
| 412 | * |
| 413 | * @return bool |
| 414 | */ |
| 415 | function wpcom_purchase_has_feature( $purchase, $feature ) { |
| 416 | if ( $purchase instanceof Store_Subscription ) { |
| 417 | /** |
| 418 | * We retrieve the product_slug and product_type directly from the Store_Product_List |
| 419 | * cache instead of relying on the internals of Store_Subscription to retrieve it. |
| 420 | * |
| 421 | * The issue is that simply "->product_slug" or "->product_type" can call a custom __get(), |
| 422 | * which can issue SQL queries that take > 10ms for information is not needed. This assignment |
| 423 | * grabs the value directly from the cached store_products data avoiding any unnecessary queries. |
| 424 | */ |
| 425 | $product = Store_Product_List::get_from_cache()[ $purchase->product_id ]; |
| 426 | |
| 427 | $purchase = (object) array( |
| 428 | 'product_slug' => $product['product_slug'], |
| 429 | 'product_id' => (string) $purchase->product_id, |
| 430 | 'product_type' => $product['product_type'], |
| 431 | 'subscribed_date' => wpcom_datetime_to_iso8601( $purchase->subscribed_date ), |
| 432 | 'expiry_date' => wpcom_datetime_to_iso8601( $purchase->expiry ), |
| 433 | ); |
| 434 | } |
| 435 | |
| 436 | return WPCOM_Features::has_feature( $feature, array( $purchase ) ); |
| 437 | } |
| 438 | |
| 439 | /** |
| 440 | * Returns a list of features that are associated with the passed product. |
| 441 | * |
| 442 | * @param string|int|Store_Product $product A Store_Product object, a product slug, or ID. |
| 443 | * |
| 444 | * @return string[] |
| 445 | */ |
| 446 | function wpcom_get_product_features( $product ) { |
| 447 | if ( defined( 'IS_ATOMIC' ) && IS_ATOMIC ) { |
| 448 | _doing_it_wrong( |
| 449 | __FUNCTION__, |
| 450 | 'Support for this function is only in available in contexts where the store products database is available.', |
| 451 | '' // No version. |
| 452 | ); |
| 453 | return array(); |
| 454 | } |
| 455 | // @codeCoverageIgnoreStart |
| 456 | $purchase = _convert_product_to_purchase( $product ); |
| 457 | if ( ! $purchase ) { |
| 458 | return array(); |
| 459 | } |
| 460 | |
| 461 | $cache_group = 'site_purchases'; |
| 462 | $cache_found = false; |
| 463 | |
| 464 | // Include sticker status in cache key only for Personal and Premium plans since they're affected by feature gating experiments. |
| 465 | $sticker_cache_suffix = ''; |
| 466 | // @phan-suppress-next-line PhanRedundantCondition |
| 467 | if ( function_exists( 'has_blog_sticker' ) && $purchase ) { |
| 468 | // Use existing WPCOM_Store helper methods to check plan types |
| 469 | $is_personal_or_premium_plan = false; |
| 470 | if ( isset( $purchase->product_id ) ) { |
| 471 | $is_personal_or_premium_plan = WPCOM_Store::is_wpcom_personal_plan( $purchase->product_id ) || WPCOM_Store::is_wpcom_premium_plan( $purchase->product_id ); |
| 472 | } |
| 473 | |
| 474 | if ( $is_personal_or_premium_plan ) { |
| 475 | $current_blog_id = get_current_blog_id(); |
| 476 | if ( has_blog_sticker( 'gating-business-q1', $current_blog_id ) ) { |
| 477 | $sticker_cache_suffix .= '_gatingbq1'; |
| 478 | } |
| 479 | } |
| 480 | } |
| 481 | |
| 482 | $cache_key = $purchase->product_slug . $sticker_cache_suffix . filemtime( __DIR__ . '/class-wpcom-features.php' ); |
| 483 | $features = wp_cache_get( $cache_key, $cache_group, false, $cache_found ); |
| 484 | |
| 485 | if ( false === $cache_found ) { |
| 486 | $features = array(); |
| 487 | |
| 488 | foreach ( WPCOM_Features::get_feature_slugs() as $feature ) { |
| 489 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable |
| 490 | if ( wpcom_purchase_has_feature( $purchase, $feature ) ) { |
| 491 | $features[] = $feature; |
| 492 | } |
| 493 | } |
| 494 | |
| 495 | wp_cache_set( $cache_key, $features, $cache_group, DAY_IN_SECONDS ); |
| 496 | } |
| 497 | |
| 498 | return $features; |
| 499 | // @codeCoverageIgnoreEnd |
| 500 | } |
| 501 | |
| 502 | /** |
| 503 | * Converts a store product to a purchase object compatible with `WPCOM_Features::has_feature`. |
| 504 | * |
| 505 | * @param string|int|Store_Product $product A Store_Product object, a product slug, or ID. |
| 506 | * |
| 507 | * @return null|object |
| 508 | */ |
| 509 | function _convert_product_to_purchase( $product ) { |
| 510 | if ( ! is_numeric( $product ) && ! is_string( $product ) && ! ( $product instanceof Store_Product ) ) { |
| 511 | _doing_it_wrong( |
| 512 | __FUNCTION__, |
| 513 | 'The $purchase parameter should be of type string|int|Store_Product.', |
| 514 | false // No version. |
| 515 | ); |
| 516 | return null; |
| 517 | } |
| 518 | |
| 519 | if ( is_string( $product ) && ! is_numeric( $product ) ) { |
| 520 | require_once WP_CONTENT_DIR . '/admin-plugins/wpcom-billing/class.wpcom-billingdaddy.php'; |
| 521 | $product = WPCOM_Billingdaddy::store_product_slug_to_product_id( $product ); |
| 522 | } |
| 523 | |
| 524 | if ( is_numeric( $product ) ) { |
| 525 | $product_cache = Store_Product_List::get_from_cache(); |
| 526 | if ( ! array_key_exists( $product, $product_cache ) ) { |
| 527 | return null; |
| 528 | } |
| 529 | $product = (object) $product_cache[ $product ]; |
| 530 | } |
| 531 | |
| 532 | return (object) array( |
| 533 | 'product_slug' => $product->product_slug, |
| 534 | 'product_id' => (string) $product->product_id, |
| 535 | 'product_type' => $product->product_type, |
| 536 | 'subscribed_date' => null, |
| 537 | 'expiry_date' => null, |
| 538 | ); |
| 539 | } |
| 540 | |
| 541 | /** |
| 542 | * Checks whether the given feature exists in WordPress.com. |
| 543 | * |
| 544 | * @param string $feature The name of the feature to check. |
| 545 | * |
| 546 | * @return bool Whether the feature exists. |
| 547 | */ |
| 548 | function wpcom_feature_exists( $feature ) { |
| 549 | return WPCOM_Features::feature_exists( $feature ); |
| 550 | } |