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