Exceptions
1 year ago
PaymentsProviders
3 months ago
SettingsUIPages
1 month ago
LegacySettingsPageAdapter.php
1 month ago
Payments.php
3 months ago
PaymentsController.php
11 months ago
PaymentsProviders.php
2 months ago
PaymentsRestController.php
3 months ago
RegisteredSettingsSectionAdapter.php
2 weeks ago
SettingsUIPageInterface.php
1 month ago
SettingsUIRequestContext.php
2 weeks ago
SettingsUISchema.php
2 weeks ago
Utils.php
2 months ago
Utils.php
472 lines
| 1 | <?php |
| 2 | declare( strict_types=1 ); |
| 3 | |
| 4 | namespace Automattic\WooCommerce\Internal\Admin\Settings; |
| 5 | |
| 6 | use Automattic\WooCommerce\Internal\Jetpack\JetpackConnection; |
| 7 | use WP_REST_Request; |
| 8 | |
| 9 | defined( 'ABSPATH' ) || exit; |
| 10 | /** |
| 11 | * Payments settings utilities class. |
| 12 | * |
| 13 | * @internal |
| 14 | */ |
| 15 | class Utils { |
| 16 | /** |
| 17 | * Apply order mappings to a base order map. |
| 18 | * |
| 19 | * @param array $base_map The base order map. |
| 20 | * @param array $new_mappings The order mappings to apply. |
| 21 | * This can be a full or partial list of the base one, |
| 22 | * but it can also contain (only) new IDs and their orders. |
| 23 | * |
| 24 | * @return array The updated base order map, normalized. |
| 25 | */ |
| 26 | public static function order_map_apply_mappings( array $base_map, array $new_mappings ): array { |
| 27 | // Make sure the base map is sorted ascending by their order values. |
| 28 | // We don't normalize first because the order values have meaning. |
| 29 | asort( $base_map ); |
| 30 | |
| 31 | $updated_map = $base_map; |
| 32 | // Apply the new mappings in the order they were given. |
| 33 | foreach ( $new_mappings as $id => $order ) { |
| 34 | // If the ID is not in the base map, we ADD it at the desired order. Otherwise, we MOVE it. |
| 35 | if ( ! isset( $base_map[ $id ] ) ) { |
| 36 | $updated_map = self::order_map_add_at_order( $updated_map, $id, $order ); |
| 37 | continue; |
| 38 | } |
| 39 | |
| 40 | $updated_map = self::order_map_move_at_order( $updated_map, $id, $order ); |
| 41 | } |
| 42 | |
| 43 | return self::order_map_normalize( $updated_map ); |
| 44 | } |
| 45 | |
| 46 | /** |
| 47 | * Move an id at a specific order in an order map. |
| 48 | * |
| 49 | * This method is used to simulate the behavior of a drag&drop sorting UI: |
| 50 | * - When moving an id down, all the ids with an order equal or lower than the desired order |
| 51 | * but equal or higher than the current order are decreased by 1. |
| 52 | * - When moving an id up, all the ids with an order equal or higher than the desired order |
| 53 | * but equal or lower than the current order are increased by 1. |
| 54 | * |
| 55 | * @param array $order_map The order map. |
| 56 | * @param string $id The id to place. |
| 57 | * @param int $order The order at which to place the id. |
| 58 | * |
| 59 | * @return array The updated order map. This map is not normalized. |
| 60 | */ |
| 61 | public static function order_map_move_at_order( array $order_map, string $id, int $order ): array { |
| 62 | // If the id is not in the order map, return the order map as is. |
| 63 | if ( ! isset( $order_map[ $id ] ) ) { |
| 64 | return $order_map; |
| 65 | } |
| 66 | |
| 67 | // If the id is already at the desired order, return the order map as is. |
| 68 | if ( $order_map[ $id ] === $order ) { |
| 69 | return $order_map; |
| 70 | } |
| 71 | |
| 72 | // If there is no id at the desired order, just place the id there. |
| 73 | if ( ! in_array( $order, $order_map, true ) ) { |
| 74 | $order_map[ $id ] = $order; |
| 75 | |
| 76 | return $order_map; |
| 77 | } |
| 78 | |
| 79 | // We apply the normal behavior of a drag&drop sorting UI. |
| 80 | $existing_order = $order_map[ $id ]; |
| 81 | if ( $order > $existing_order ) { |
| 82 | // Moving down. |
| 83 | foreach ( $order_map as $key => $value ) { |
| 84 | if ( $value <= $order && $value >= $existing_order ) { |
| 85 | --$order_map[ $key ]; |
| 86 | } |
| 87 | } |
| 88 | } else { |
| 89 | // Moving up. |
| 90 | foreach ( $order_map as $key => $value ) { |
| 91 | if ( $value >= $order && $value <= $existing_order ) { |
| 92 | ++$order_map[ $key ]; |
| 93 | } |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | // Place the id at the desired order. |
| 98 | $order_map[ $id ] = $order; |
| 99 | |
| 100 | return $order_map; |
| 101 | } |
| 102 | |
| 103 | /** |
| 104 | * Place an id at a specific order in an order map. |
| 105 | * |
| 106 | * @param array $order_map The order map. |
| 107 | * @param string $id The id to place. |
| 108 | * @param int $order The order at which to place the id. |
| 109 | * |
| 110 | * @return array The updated order map. |
| 111 | */ |
| 112 | public static function order_map_place_at_order( array $order_map, string $id, int $order ): array { |
| 113 | // If the id is already at the desired order, return the order map as is. |
| 114 | if ( isset( $order_map[ $id ] ) && $order_map[ $id ] === $order ) { |
| 115 | return $order_map; |
| 116 | } |
| 117 | |
| 118 | // If there is no id at the desired order, just place the id there. |
| 119 | if ( ! in_array( $order, $order_map, true ) ) { |
| 120 | $order_map[ $id ] = $order; |
| 121 | |
| 122 | return $order_map; |
| 123 | } |
| 124 | |
| 125 | // Bump the order of everything with an order equal or higher than the desired order. |
| 126 | foreach ( $order_map as $key => $value ) { |
| 127 | if ( $value >= $order ) { |
| 128 | ++$order_map[ $key ]; |
| 129 | } |
| 130 | } |
| 131 | |
| 132 | // Place the id at the desired order. |
| 133 | $order_map[ $id ] = $order; |
| 134 | |
| 135 | return $order_map; |
| 136 | } |
| 137 | |
| 138 | /** |
| 139 | * Add an id to a specific order in an order map. |
| 140 | * |
| 141 | * @param array $order_map The order map. |
| 142 | * @param string $id The id to move. |
| 143 | * @param int $order The order to move the id to. |
| 144 | * |
| 145 | * @return array The updated order map. If the id is already in the order map, the order map is returned as is. |
| 146 | */ |
| 147 | public static function order_map_add_at_order( array $order_map, string $id, int $order ): array { |
| 148 | // If the id is in the order map, return the order map as is. |
| 149 | if ( isset( $order_map[ $id ] ) ) { |
| 150 | return $order_map; |
| 151 | } |
| 152 | |
| 153 | return self::order_map_place_at_order( $order_map, $id, $order ); |
| 154 | } |
| 155 | |
| 156 | /** |
| 157 | * Normalize an order map. |
| 158 | * |
| 159 | * Sort the order map by the order and ensure the order values start from 0 and are consecutive. |
| 160 | * |
| 161 | * @param array $order_map The order map. |
| 162 | * |
| 163 | * @return array The normalized order map. |
| 164 | */ |
| 165 | public static function order_map_normalize( array $order_map ): array { |
| 166 | // Remove entries with non-string keys (legacy/corrupt data). |
| 167 | $order_map = array_filter( |
| 168 | $order_map, |
| 169 | fn( $key ) => is_string( $key ), |
| 170 | ARRAY_FILTER_USE_KEY |
| 171 | ); |
| 172 | |
| 173 | asort( $order_map ); |
| 174 | |
| 175 | return array_flip( array_keys( $order_map ) ); |
| 176 | } |
| 177 | |
| 178 | /** |
| 179 | * Change the minimum order of an order map. |
| 180 | * |
| 181 | * @param array $order_map The order map. |
| 182 | * @param int $new_min_order The new minimum order. |
| 183 | * |
| 184 | * @return array The updated order map. |
| 185 | */ |
| 186 | public static function order_map_change_min_order( array $order_map, int $new_min_order ): array { |
| 187 | // Sanity checks. |
| 188 | if ( empty( $order_map ) ) { |
| 189 | return array(); |
| 190 | } |
| 191 | |
| 192 | $updated_map = array(); |
| 193 | $bump = $new_min_order - min( $order_map ); |
| 194 | foreach ( $order_map as $id => $order ) { |
| 195 | $updated_map[ $id ] = $order + $bump; |
| 196 | } |
| 197 | |
| 198 | asort( $updated_map ); |
| 199 | |
| 200 | return $updated_map; |
| 201 | } |
| 202 | |
| 203 | /** |
| 204 | * Get the list of plugin slug suffixes used for handling non-standard testing slugs. |
| 205 | * |
| 206 | * @return string[] The list of plugin slug suffixes used for handling non-standard testing slugs. |
| 207 | */ |
| 208 | public static function get_testing_plugin_slug_suffixes(): array { |
| 209 | return array( '-dev', '-rc', '-test', '-beta', '-alpha' ); |
| 210 | } |
| 211 | |
| 212 | /** |
| 213 | * Generate a list of testing plugin slugs from a standard/official plugin slug. |
| 214 | * |
| 215 | * @param string $slug The standard/official plugin slug. Most likely the WPORG slug. |
| 216 | * @param bool $include_original Optional. Whether to include the original slug in the list. |
| 217 | * If true, the original slug will be the first item in the list. |
| 218 | * |
| 219 | * @return string[] The list of testing plugin slugs generated from the standard/official plugin slug. |
| 220 | */ |
| 221 | public static function generate_testing_plugin_slugs( string $slug, bool $include_original = false ): array { |
| 222 | $slugs = array(); |
| 223 | if ( $include_original ) { |
| 224 | $slugs[] = $slug; |
| 225 | } |
| 226 | |
| 227 | foreach ( self::get_testing_plugin_slug_suffixes() as $suffix ) { |
| 228 | $slugs[] = $slug . $suffix; |
| 229 | } |
| 230 | |
| 231 | return $slugs; |
| 232 | } |
| 233 | |
| 234 | /** |
| 235 | * Normalize a plugin slug to a standard/official slug. |
| 236 | * |
| 237 | * This is a best-effort approach. |
| 238 | * It will remove beta testing suffixes and lowercase the slug. |
| 239 | * It will NOT convert plugin titles to slugs or sanitize the slug like sanitize_title() does. |
| 240 | * |
| 241 | * @param string $slug The plugin slug. |
| 242 | * |
| 243 | * @return string The normalized plugin slug. |
| 244 | */ |
| 245 | public static function normalize_plugin_slug( string $slug ): string { |
| 246 | // If the slug is empty or contains anything other than alphanumeric and dash characters, it will be left as is. |
| 247 | if ( empty( $slug ) || ! preg_match( '/^[\w-]+$/', $slug, $matches ) ) { |
| 248 | return $slug; |
| 249 | } |
| 250 | |
| 251 | // Lowercase the slug. |
| 252 | $slug = strtolower( $slug ); |
| 253 | // Remove testing suffixes. |
| 254 | foreach ( self::get_testing_plugin_slug_suffixes() as $suffix ) { |
| 255 | $slug = str_ends_with( $slug, $suffix ) ? substr( $slug, 0, -strlen( $suffix ) ) : $slug; |
| 256 | } |
| 257 | |
| 258 | return $slug; |
| 259 | } |
| 260 | |
| 261 | /** |
| 262 | * Trim the .php file extension from a path. |
| 263 | * |
| 264 | * @param string $path The path to trim. |
| 265 | * |
| 266 | * @return string The trimmed path. If the path does not end with .php, it will be returned as is. |
| 267 | */ |
| 268 | public static function trim_php_file_extension( string $path ): string { |
| 269 | if ( ! empty( $path ) && str_ends_with( $path, '.php' ) ) { |
| 270 | $path = substr( $path, 0, - 4 ); |
| 271 | } |
| 272 | |
| 273 | return $path; |
| 274 | } |
| 275 | |
| 276 | /** |
| 277 | * Truncate a text to a target character length while preserving whole words. |
| 278 | * |
| 279 | * We take a greedy approach: if some characters of a word fit in the target length, the whole word is included. |
| 280 | * This means we might exceed the target length by a few characters. |
| 281 | * The append string length is not included in the character count. |
| 282 | * |
| 283 | * @param string $text The text to truncate. |
| 284 | * It will not be sanitized, stripped of HTML tags, or modified in any way before truncation. |
| 285 | * @param int $target_length The target character length of the truncated text. |
| 286 | * @param string $append Optional. The string to append to the truncated text, if there is any truncation. |
| 287 | * |
| 288 | * @return string The truncated text. |
| 289 | */ |
| 290 | public static function truncate_with_words( string $text, int $target_length, string $append = '' ): string { |
| 291 | // First, deal with locale that doesn't have words separated by spaces, but instead deals with characters. |
| 292 | // Borrowed from wp_trim_words(). |
| 293 | if ( str_starts_with( wp_get_word_count_type(), 'characters' ) && preg_match( '/^utf\-?8$/i', get_option( 'blog_charset' ) ) ) { |
| 294 | $text = trim( preg_replace( "/[\n\r\t ]+/", ' ', $text ), ' ' ); |
| 295 | preg_match_all( '/./u', $text, $words_array ); |
| 296 | |
| 297 | // Nothing to do if the text is already short enough. |
| 298 | if ( count( $words_array[0] ) <= $target_length ) { |
| 299 | return $text; |
| 300 | } |
| 301 | |
| 302 | $words_array = array_slice( $words_array[0], 0, $target_length ); |
| 303 | $truncated = implode( '', $words_array ); |
| 304 | if ( $append ) { |
| 305 | $truncated .= $append; |
| 306 | } |
| 307 | |
| 308 | return $truncated; |
| 309 | } |
| 310 | |
| 311 | // Deal with locale that has words separated by spaces. |
| 312 | if ( strlen( $text ) <= $target_length ) { |
| 313 | return $text; |
| 314 | } |
| 315 | |
| 316 | $words_array = preg_split( "/[\n\r\t ]+/", $text, - 1, PREG_SPLIT_NO_EMPTY ); |
| 317 | $sep = ' '; |
| 318 | |
| 319 | // Include words until the target length is reached. |
| 320 | $truncated = ''; |
| 321 | $remaining_length = $target_length; |
| 322 | while ( $remaining_length > 0 && ! empty( $words_array ) ) { |
| 323 | $word = array_shift( $words_array ); |
| 324 | $truncated .= $word . $sep; |
| 325 | $remaining_length -= strlen( $word . $sep ); |
| 326 | } |
| 327 | |
| 328 | // Remove the last separator. |
| 329 | $truncated = rtrim( $truncated, $sep ); |
| 330 | |
| 331 | if ( null !== $append ) { |
| 332 | $truncated .= $append; |
| 333 | } |
| 334 | |
| 335 | return $truncated; |
| 336 | } |
| 337 | |
| 338 | /** |
| 339 | * Retrieves a URL to relative path inside WooCommerce admin Payments settings with |
| 340 | * the provided query parameters. |
| 341 | * |
| 342 | * @param string|null $path Relative path of the desired page. |
| 343 | * @param array $query Query parameters to append to the path. |
| 344 | * |
| 345 | * @return string Fully qualified URL pointing to the desired path. |
| 346 | */ |
| 347 | public static function wc_payments_settings_url( ?string $path = null, array $query = array() ): string { |
| 348 | $path = $path ? '&path=' . $path : ''; |
| 349 | |
| 350 | $query_string = ''; |
| 351 | if ( ! empty( $query ) ) { |
| 352 | $query_string = '&' . http_build_query( $query ); |
| 353 | } |
| 354 | |
| 355 | return admin_url( 'admin.php?page=wc-settings&tab=checkout' . $path . $query_string ); |
| 356 | } |
| 357 | |
| 358 | /** |
| 359 | * Get data from a WooCommerce API endpoint. |
| 360 | * |
| 361 | * @param string $endpoint Endpoint. |
| 362 | * @param array $params Params to pass with request query. |
| 363 | * |
| 364 | * @return array|\WP_Error The response data or a WP_Error object. |
| 365 | */ |
| 366 | public static function rest_endpoint_get_request( string $endpoint, array $params = array() ) { |
| 367 | $request = new \WP_REST_Request( 'GET', $endpoint ); |
| 368 | if ( $params ) { |
| 369 | $request->set_query_params( $params ); |
| 370 | } |
| 371 | |
| 372 | // Do the internal request. |
| 373 | // This has minimal overhead compared to an external request. |
| 374 | $response = rest_do_request( $request ); |
| 375 | |
| 376 | $server = rest_get_server(); |
| 377 | $response_data = json_decode( wp_json_encode( $server->response_to_data( $response, false ) ), true ); |
| 378 | |
| 379 | // Handle non-200 responses. |
| 380 | if ( 200 !== $response->get_status() ) { |
| 381 | return new \WP_Error( |
| 382 | 'woocommerce_settings_payments_rest_error', |
| 383 | sprintf( |
| 384 | /* translators: 1: the endpoint relative URL, 2: error code, 3: error message */ |
| 385 | esc_html__( 'REST request GET %1$s failed with: (%2$s) %3$s', 'woocommerce' ), |
| 386 | $endpoint, |
| 387 | $response_data['code'] ?? 'unknown_error', |
| 388 | $response_data['message'] ?? esc_html__( 'Unknown error', 'woocommerce' ) |
| 389 | ), |
| 390 | $response_data |
| 391 | ); |
| 392 | } |
| 393 | |
| 394 | // If the response is 200, return the data. |
| 395 | return $response_data; |
| 396 | } |
| 397 | |
| 398 | /** |
| 399 | * Post data to a WooCommerce API endpoint and return the response data. |
| 400 | * |
| 401 | * @param string $endpoint Endpoint. |
| 402 | * @param array $params Params to pass with request body. |
| 403 | * |
| 404 | * @return array|\WP_Error The response data or a WP_Error object. |
| 405 | */ |
| 406 | public static function rest_endpoint_post_request( string $endpoint, array $params = array() ) { |
| 407 | $request = new \WP_REST_Request( 'POST', $endpoint ); |
| 408 | if ( $params ) { |
| 409 | $request->set_body_params( $params ); |
| 410 | } |
| 411 | |
| 412 | // Do the internal request. |
| 413 | // This has minimal overhead compared to an external request. |
| 414 | $response = rest_do_request( $request ); |
| 415 | |
| 416 | $server = rest_get_server(); |
| 417 | $response_data = json_decode( wp_json_encode( $server->response_to_data( $response, false ) ), true ); |
| 418 | |
| 419 | // Handle non-200 responses. |
| 420 | if ( 200 !== $response->get_status() ) { |
| 421 | return new \WP_Error( |
| 422 | 'woocommerce_settings_payments_rest_error', |
| 423 | sprintf( |
| 424 | /* translators: 1: the endpoint relative URL, 2: error code, 3: error message */ |
| 425 | esc_html__( 'REST request POST %1$s failed with: (%2$s) %3$s', 'woocommerce' ), |
| 426 | $endpoint, |
| 427 | $response_data['code'] ?? 'unknown_error', |
| 428 | $response_data['message'] ?? esc_html__( 'Unknown error', 'woocommerce' ) |
| 429 | ), |
| 430 | $response_data |
| 431 | ); |
| 432 | } |
| 433 | |
| 434 | // If the response is 200, return the data. |
| 435 | return $response_data; |
| 436 | } |
| 437 | |
| 438 | /** |
| 439 | * Get the details to authorize a connection to WordPress.com. |
| 440 | * |
| 441 | * The most important part of the result is the URL to redirect to for authorization. |
| 442 | * |
| 443 | * @param string $return_url The URL to redirect to after the connection is authorized. |
| 444 | * |
| 445 | * @return array { |
| 446 | * 'success' => bool Whether the request was successful. |
| 447 | * 'errors' => array An array of error messages, if any. |
| 448 | * 'color_scheme' => string The color scheme to use for the authorization page. |
| 449 | * 'url' => string The URL to redirect to for authorization. |
| 450 | * } |
| 451 | */ |
| 452 | public static function get_wpcom_connection_authorization( string $return_url ): array { |
| 453 | $result = JetpackConnection::get_authorization_url( $return_url ); |
| 454 | |
| 455 | if ( ! empty( $result['url'] ) ) { |
| 456 | $result['url'] = add_query_arg( |
| 457 | array( |
| 458 | // We use the new WooDNA value. |
| 459 | 'from' => 'woocommerce-onboarding', |
| 460 | // We inform Calypso that this is a WooPayments onboarding flow. |
| 461 | 'plugin_name' => 'woocommerce-payments', |
| 462 | // Use the current user's WP admin color scheme. |
| 463 | 'color_scheme' => $result['color_scheme'], |
| 464 | ), |
| 465 | $result['url'] |
| 466 | ); |
| 467 | } |
| 468 | |
| 469 | return $result; |
| 470 | } |
| 471 | } |
| 472 |