helper
4 weeks ago
importers
1 year ago
list-tables
4 months ago
marketplace-suggestions
10 months ago
meta-boxes
4 weeks ago
notes
4 weeks ago
plugin-updates
2 years ago
reports
2 months ago
settings
1 week ago
views
2 months ago
class-wc-admin-addons.php
7 months ago
class-wc-admin-api-keys-table-list.php
2 years ago
class-wc-admin-api-keys.php
10 months ago
class-wc-admin-assets.php
4 weeks ago
class-wc-admin-attributes.php
3 years ago
class-wc-admin-brands.php
3 months ago
class-wc-admin-customize.php
5 years ago
class-wc-admin-dashboard-setup.php
10 months ago
class-wc-admin-dashboard.php
3 months ago
class-wc-admin-duplicate-product.php
4 months ago
class-wc-admin-exporters.php
1 year ago
class-wc-admin-help.php
2 years ago
class-wc-admin-importers.php
10 months ago
class-wc-admin-log-table-list.php
3 months ago
class-wc-admin-marketplace-promotions.php
3 months ago
class-wc-admin-menus.php
3 months ago
class-wc-admin-meta-boxes.php
1 year ago
class-wc-admin-notices.php
4 weeks ago
class-wc-admin-permalink-settings.php
5 years ago
class-wc-admin-pointers.php
3 years ago
class-wc-admin-post-types.php
1 year ago
class-wc-admin-profile.php
1 year ago
class-wc-admin-reports.php
3 months ago
class-wc-admin-settings.php
2 months ago
class-wc-admin-setup-wizard.php
3 months ago
class-wc-admin-status.php
1 year ago
class-wc-admin-taxonomies.php
6 months ago
class-wc-admin-upload-downloadable-product.php
2 years ago
class-wc-admin-webhooks-table-list.php
1 year ago
class-wc-admin-webhooks.php
10 months ago
class-wc-admin.php
2 months ago
wc-admin-functions.php
6 months ago
wc-meta-box-functions.php
1 year ago
woocommerce-legacy-reports.php
1 year ago
class-wc-admin-addons.php
421 lines
| 1 | <?php |
| 2 | /** |
| 3 | * Addons Page |
| 4 | * |
| 5 | * @package WooCommerce\Admin |
| 6 | * @version 2.5.0 |
| 7 | */ |
| 8 | |
| 9 | use Automattic\Jetpack\Constants; |
| 10 | use Automattic\WooCommerce\Admin\RemoteInboxNotifications as PromotionRuleEngine; |
| 11 | use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\RuleEvaluator; |
| 12 | |
| 13 | if ( ! defined( 'ABSPATH' ) ) { |
| 14 | exit; |
| 15 | } |
| 16 | |
| 17 | /** |
| 18 | * WC_Admin_Addons Class. |
| 19 | */ |
| 20 | class WC_Admin_Addons { |
| 21 | |
| 22 | /** |
| 23 | * Fetch featured products from WCCOM's the Featured 3.0 Endpoint and cache the data for a day. |
| 24 | * |
| 25 | * @return array|WP_Error |
| 26 | */ |
| 27 | public static function fetch_featured() { |
| 28 | $transient_name = 'wc_addons_featured'; |
| 29 | // Important: WCCOM Extensions API v4.0 is used. |
| 30 | $url = 'https://woocommerce.com/wp-json/wccom-extensions/4.0/featured'; |
| 31 | $locale = get_user_locale(); |
| 32 | $featured = self::get_locale_data_from_transient( $transient_name, $locale ); |
| 33 | |
| 34 | if ( false === $featured ) { |
| 35 | $fetch_options = array( |
| 36 | 'auth' => true, |
| 37 | 'locale' => true, |
| 38 | 'country' => true, |
| 39 | ); |
| 40 | $raw_featured = self::fetch( $url, $fetch_options ); |
| 41 | |
| 42 | $featured = self::process_api_response( $raw_featured, 'featured' ); |
| 43 | |
| 44 | if ( ! is_wp_error( $featured ) && $featured ) { |
| 45 | self::set_locale_data_in_transient( $transient_name, $featured, $locale, DAY_IN_SECONDS ); |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | return $featured; |
| 50 | } |
| 51 | |
| 52 | /** |
| 53 | * Fetch markup and other info for the preview of a product. |
| 54 | * |
| 55 | * @param int $product_id The ID of the product to fetch preview for. |
| 56 | * @return array|WP_Error Preview data or error object. |
| 57 | */ |
| 58 | public static function fetch_product_preview( int $product_id ) { |
| 59 | $url = 'https://woocommerce.com/wp-json/wccom-extensions/1.0/product-previews?product_id=' . $product_id; |
| 60 | |
| 61 | $fetch_options = array( |
| 62 | 'locale' => true, |
| 63 | ); |
| 64 | |
| 65 | $raw_preview = self::fetch( $url, $fetch_options ); |
| 66 | |
| 67 | return self::process_api_response( $raw_preview, 'product preview', true ); |
| 68 | } |
| 69 | |
| 70 | /** |
| 71 | * Check if the error is due to an SSL error |
| 72 | * |
| 73 | * @param string $error_message Error message. |
| 74 | * |
| 75 | * @return bool True if SSL error, false otherwise |
| 76 | */ |
| 77 | public static function is_ssl_error( $error_message ) { |
| 78 | return false !== stripos( $error_message, 'cURL error 35' ); |
| 79 | } |
| 80 | |
| 81 | /** |
| 82 | * Get sections for the addons screen |
| 83 | * |
| 84 | * @return array of objects |
| 85 | */ |
| 86 | public static function get_sections() { |
| 87 | $locale = get_user_locale(); |
| 88 | $addon_sections = self::get_locale_data_from_transient( 'wc_addons_sections', $locale ); |
| 89 | if ( false === ( $addon_sections ) ) { |
| 90 | $parameter_string = '?' . http_build_query( array( 'locale' => get_user_locale() ) ); |
| 91 | $raw_sections = wp_safe_remote_get( |
| 92 | 'https://woocommerce.com/wp-json/wccom-extensions/1.0/categories' . $parameter_string, |
| 93 | array( |
| 94 | 'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ), |
| 95 | ) |
| 96 | ); |
| 97 | if ( ! is_wp_error( $raw_sections ) ) { |
| 98 | $addon_sections = json_decode( wp_remote_retrieve_body( $raw_sections ) ); |
| 99 | if ( $addon_sections ) { |
| 100 | self::set_locale_data_in_transient( 'wc_addons_sections', $addon_sections, $locale, WEEK_IN_SECONDS ); |
| 101 | } |
| 102 | } |
| 103 | } |
| 104 | return apply_filters( 'woocommerce_addons_sections', $addon_sections ); |
| 105 | } |
| 106 | |
| 107 | /** |
| 108 | * Get section for the addons screen. |
| 109 | * |
| 110 | * @param string $section_id Required section ID. |
| 111 | * |
| 112 | * @return object|bool |
| 113 | */ |
| 114 | public static function get_section( $section_id ) { |
| 115 | $sections = self::get_sections(); |
| 116 | if ( isset( $sections[ $section_id ] ) ) { |
| 117 | return $sections[ $section_id ]; |
| 118 | } |
| 119 | return false; |
| 120 | } |
| 121 | |
| 122 | /** |
| 123 | * Returns in-app-purchase URL params. |
| 124 | */ |
| 125 | public static function get_in_app_purchase_url_params() { |
| 126 | // Get url (from path onward) for the current page, |
| 127 | // so WCCOM "back" link returns user to where they were. |
| 128 | $back_admin_path = add_query_arg( array() ); |
| 129 | return array( |
| 130 | 'wccom-site' => site_url(), |
| 131 | 'wccom-back' => rawurlencode( $back_admin_path ), |
| 132 | 'wccom-woo-version' => WC()->stable_version(), |
| 133 | 'wccom-connect-nonce' => wp_create_nonce( 'connect' ), |
| 134 | ); |
| 135 | } |
| 136 | |
| 137 | /** |
| 138 | * Add in-app-purchase URL params to link. |
| 139 | * |
| 140 | * Adds various url parameters to a url to support a streamlined |
| 141 | * flow for obtaining and setting up WooCommerce extensons. |
| 142 | * |
| 143 | * @param string $url Destination URL. |
| 144 | */ |
| 145 | public static function add_in_app_purchase_url_params( $url ) { |
| 146 | return add_query_arg( |
| 147 | self::get_in_app_purchase_url_params(), |
| 148 | $url |
| 149 | ); |
| 150 | } |
| 151 | |
| 152 | /** |
| 153 | * Outputs a button. |
| 154 | * |
| 155 | * @param string $url Destination URL. |
| 156 | * @param string $text Button label text. |
| 157 | * @param string $style Button style class. |
| 158 | * @param string $plugin The plugin the button is promoting. |
| 159 | */ |
| 160 | public static function output_button( $url, $text, $style, $plugin = '' ) { |
| 161 | $style = __( 'Free', 'woocommerce' ) === $text ? 'addons-button-outline-purple' : $style; |
| 162 | $style = is_plugin_active( $plugin ) ? 'addons-button-installed' : $style; |
| 163 | $text = is_plugin_active( $plugin ) ? __( 'Installed', 'woocommerce' ) : $text; |
| 164 | $url = self::add_in_app_purchase_url_params( $url ); |
| 165 | ?> |
| 166 | <a |
| 167 | class="addons-button <?php echo esc_attr( $style ); ?>" |
| 168 | href="<?php echo esc_url( $url ); ?>"> |
| 169 | <?php echo esc_html( $text ); ?> |
| 170 | </a> |
| 171 | <?php |
| 172 | } |
| 173 | |
| 174 | /** |
| 175 | * Process requests to legacy marketplace menu and redirect to correct in-app pages. |
| 176 | * |
| 177 | * @return void |
| 178 | */ |
| 179 | public static function handle_legacy_marketplace_redirects() { |
| 180 | $section = isset( $_GET['section'] ) ? sanitize_text_field( wp_unslash( $_GET['section'] ) ) : '_featured'; |
| 181 | $search = isset( $_GET['search'] ) ? sanitize_text_field( wp_unslash( $_GET['search'] ) ) : ''; |
| 182 | |
| 183 | if ( 'helper' === $section ) { |
| 184 | $url = admin_url( 'admin.php?page=wc-admin&tab=my-subscriptions&path=%2Fextensions' ); |
| 185 | |
| 186 | if ( isset( $_GET['connect'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 187 | $url .= '&connect'; |
| 188 | } |
| 189 | |
| 190 | wp_safe_redirect( $url ); |
| 191 | exit(); |
| 192 | } |
| 193 | |
| 194 | if ( 'search' === $section || ! empty( $search ) ) { |
| 195 | wp_safe_redirect( admin_url( 'admin.php?page=wc-admin&term=' . $search . '&tab=search&path=%2Fextensions' ) ); |
| 196 | exit(); |
| 197 | } |
| 198 | |
| 199 | $sections = self::get_sections(); |
| 200 | $allowed_sections = array_map( fn( $section_object ) => $section_object->slug, $sections ); |
| 201 | // Validate if the category is supported. |
| 202 | $section = in_array( $section, $allowed_sections, true ) ? $section : '_featured'; |
| 203 | |
| 204 | if ( '_featured' === $section ) { |
| 205 | wp_safe_redirect( admin_url( 'admin.php?page=wc-admin&path=%2Fextensions' ) ); |
| 206 | exit(); |
| 207 | } |
| 208 | |
| 209 | wp_safe_redirect( admin_url( 'admin.php?page=wc-admin&tab=extensions&path=%2Fextensions&category=' . $section ) ); |
| 210 | exit(); |
| 211 | } |
| 212 | |
| 213 | /** |
| 214 | * We're displaying page=wc-addons and page=wc-addons§ion=helper as two separate pages. |
| 215 | * When we're on those pages, add body classes to distinguishe them. |
| 216 | * |
| 217 | * @param string $admin_body_class Unfiltered body class. |
| 218 | * |
| 219 | * @return string Body class with added class for Marketplace or My Subscriptions page. |
| 220 | */ |
| 221 | public static function filter_admin_body_classes( string $admin_body_class = '' ): string { |
| 222 | if ( isset( $_GET['section'] ) && 'helper' === $_GET['section'] ) { |
| 223 | return " $admin_body_class woocommerce-page-wc-subscriptions "; |
| 224 | } |
| 225 | |
| 226 | return " $admin_body_class woocommerce-page-wc-marketplace "; |
| 227 | } |
| 228 | |
| 229 | /** |
| 230 | * Take an action object and return the URL based on properties of the action. |
| 231 | * |
| 232 | * @param object $action Action object. |
| 233 | * @return string URL. |
| 234 | */ |
| 235 | public static function get_action_url( $action ): string { |
| 236 | if ( ! isset( $action->url ) ) { |
| 237 | return ''; |
| 238 | } |
| 239 | |
| 240 | if ( isset( $action->url_is_admin_query ) && $action->url_is_admin_query ) { |
| 241 | return wc_admin_url( $action->url ); |
| 242 | } |
| 243 | |
| 244 | if ( isset( $action->url_is_admin_nonce_query ) && $action->url_is_admin_nonce_query ) { |
| 245 | if ( empty( $action->nonce ) ) { |
| 246 | return ''; |
| 247 | } |
| 248 | return wp_nonce_url( |
| 249 | admin_url( $action->url ), |
| 250 | $action->nonce |
| 251 | ); |
| 252 | } |
| 253 | |
| 254 | return $action->url; |
| 255 | } |
| 256 | |
| 257 | /** |
| 258 | * Retrieves the locale data from a transient. |
| 259 | * |
| 260 | * Transient value is an array of locale data in the following format: |
| 261 | * array( |
| 262 | * 'en_US' => ..., |
| 263 | * 'fr_FR' => ..., |
| 264 | * ) |
| 265 | * |
| 266 | * If the transient does not exist, does not have a value, or has expired, |
| 267 | * then the return value will be false. |
| 268 | * |
| 269 | * @param string $transient Transient name. Expected to not be SQL-escaped. |
| 270 | * @param string $locale Locale to retrieve. |
| 271 | * @return mixed Value of transient. |
| 272 | */ |
| 273 | private static function get_locale_data_from_transient( $transient, $locale ) { |
| 274 | $transient_value = get_transient( $transient ); |
| 275 | $transient_value = is_array( $transient_value ) ? $transient_value : array(); |
| 276 | return $transient_value[ $locale ] ?? false; |
| 277 | } |
| 278 | |
| 279 | /** |
| 280 | * Sets the locale data in a transient. |
| 281 | * |
| 282 | * Transient value is an array of locale data in the following format: |
| 283 | * array( |
| 284 | * 'en_US' => ..., |
| 285 | * 'fr_FR' => ..., |
| 286 | * ) |
| 287 | * |
| 288 | * @param string $transient Transient name. Expected to not be SQL-escaped. |
| 289 | * Must be 172 characters or fewer in length. |
| 290 | * @param mixed $value Transient value. Must be serializable if non-scalar. |
| 291 | * Expected to not be SQL-escaped. |
| 292 | * @param string $locale Locale to set. |
| 293 | * @param int $expiration Optional. Time until expiration in seconds. Default 0 (no expiration). |
| 294 | * @return bool True if the value was set, false otherwise. |
| 295 | */ |
| 296 | private static function set_locale_data_in_transient( $transient, $value, $locale, $expiration = 0 ) { |
| 297 | $transient_value = get_transient( $transient ); |
| 298 | $transient_value = is_array( $transient_value ) ? $transient_value : array(); |
| 299 | $transient_value[ $locale ] = $value; |
| 300 | return set_transient( $transient, $transient_value, $expiration ); |
| 301 | } |
| 302 | |
| 303 | /** |
| 304 | * Process API response from WooCommerce.com endpoints. |
| 305 | * |
| 306 | * @param array|WP_Error $response The response from the API request. |
| 307 | * @param string $context Context for error messages (e.g. 'featured', 'product-preview'). |
| 308 | * @param bool $associative Whether to decode the JSON as an associative array. |
| 309 | * |
| 310 | * @return array|WP_Error Processed API data or WP_Error on failure. |
| 311 | */ |
| 312 | private static function process_api_response( $response, $context = 'api', $associative = false ) { |
| 313 | if ( is_wp_error( $response ) ) { |
| 314 | /** |
| 315 | * Hook fired when there is a connection error with WooCommerce.com. |
| 316 | * |
| 317 | * @since 6.1.0 |
| 318 | * @param string $error_message The error message. |
| 319 | */ |
| 320 | do_action( 'woocommerce_page_wc_addons_connection_error', $response->get_error_message() ); |
| 321 | |
| 322 | $message = self::is_ssl_error( $response->get_error_message() ) |
| 323 | ? __( |
| 324 | 'We encountered an SSL error. Please ensure your site supports TLS version 1.2 or above.', |
| 325 | 'woocommerce' |
| 326 | ) |
| 327 | : $response->get_error_message(); |
| 328 | |
| 329 | return new WP_Error( 'wc-addons-connection-error', $message ); |
| 330 | } |
| 331 | |
| 332 | $response_code = (int) wp_remote_retrieve_response_code( $response ); |
| 333 | if ( 200 !== $response_code ) { |
| 334 | /** |
| 335 | * Hook fired when there is a connection error with WooCommerce.com. |
| 336 | * |
| 337 | * @since 6.1.0 |
| 338 | * @param int $response_code The HTTP response code. |
| 339 | */ |
| 340 | do_action( 'woocommerce_page_wc_addons_connection_error', $response_code ); |
| 341 | |
| 342 | $message = sprintf( |
| 343 | /* translators: 1: Context (e.g. 'featured', 'product-preview') 2: HTTP error code */ |
| 344 | __( 'Our request to the %1$s API got error code %2$d.', 'woocommerce' ), |
| 345 | $context, |
| 346 | $response_code |
| 347 | ); |
| 348 | |
| 349 | return new WP_Error( 'wc-addons-connection-error', $message ); |
| 350 | } |
| 351 | |
| 352 | $data = json_decode( wp_remote_retrieve_body( $response ), $associative ); |
| 353 | if ( empty( $data ) || ! is_array( $data ) ) { |
| 354 | /** |
| 355 | * Hook fired when there is a connection error with WooCommerce.com. |
| 356 | * |
| 357 | * @since 6.1.0 |
| 358 | * @param string $error_message The error message. |
| 359 | */ |
| 360 | do_action( 'woocommerce_page_wc_addons_connection_error', 'Empty or malformed response' ); |
| 361 | |
| 362 | $message = sprintf( |
| 363 | /* translators: %s: Context (e.g. 'featured', 'product-preview') */ |
| 364 | __( 'Our request to the %s API got a malformed response.', 'woocommerce' ), |
| 365 | $context |
| 366 | ); |
| 367 | |
| 368 | return new WP_Error( 'wc-addons-connection-error', $message ); |
| 369 | } |
| 370 | |
| 371 | return $data; |
| 372 | } |
| 373 | |
| 374 | /** |
| 375 | * Make wp_safe_remote_get request to WooCommerce.com endpoint. |
| 376 | * Optionally pass user auth token, locale or country. |
| 377 | * |
| 378 | * @param string $url URL to request. |
| 379 | * @param ?array $options Options for the request. For example, to pass auth token, locale and country, |
| 380 | * pass array( 'auth' => true, 'locale' => true, 'country' => true, ). |
| 381 | * |
| 382 | * @return array|WP_Error |
| 383 | */ |
| 384 | public static function fetch( $url, $options = array() ) { |
| 385 | $headers = array(); |
| 386 | |
| 387 | if ( isset( $options['auth'] ) && $options['auth'] ) { |
| 388 | $auth = WC_Helper_Options::get( 'auth' ); |
| 389 | |
| 390 | if ( isset( $auth['access_token'] ) && ! empty( $auth['access_token'] ) ) { |
| 391 | $headers['Authorization'] = 'Bearer ' . $auth['access_token']; |
| 392 | } |
| 393 | } |
| 394 | |
| 395 | $parameters = array(); |
| 396 | |
| 397 | if ( isset( $options['locale'] ) && $options['locale'] ) { |
| 398 | $parameters['locale'] = get_user_locale(); |
| 399 | } |
| 400 | |
| 401 | if ( isset( $options['country'] ) && $options['country'] ) { |
| 402 | $country = WC()->countries->get_base_country(); |
| 403 | if ( ! empty( $country ) ) { |
| 404 | $parameters['country'] = $country; |
| 405 | } |
| 406 | } |
| 407 | |
| 408 | // Check if URL already has query parameters. |
| 409 | $connector = strpos( $url, '?' ) !== false ? '&' : '?'; |
| 410 | $query_string = ! empty( $parameters ) ? $connector . http_build_query( $parameters ) : ''; |
| 411 | |
| 412 | return wp_safe_remote_get( |
| 413 | $url . $query_string, |
| 414 | array( |
| 415 | 'headers' => $headers, |
| 416 | 'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ), |
| 417 | ) |
| 418 | ); |
| 419 | } |
| 420 | } |
| 421 |