views
1 month ago
class-wc-settings-accounts.php
5 months ago
class-wc-settings-advanced.php
1 month ago
class-wc-settings-checkout.php
5 years ago
class-wc-settings-emails.php
1 month ago
class-wc-settings-general.php
1 month ago
class-wc-settings-integrations.php
1 year ago
class-wc-settings-page.php
1 week ago
class-wc-settings-payment-gateways.php
1 week ago
class-wc-settings-point-of-sale.php
3 months ago
class-wc-settings-products.php
1 month ago
class-wc-settings-shipping.php
3 months ago
class-wc-settings-site-visibility.php
2 years ago
class-wc-settings-tax.php
5 months ago
class-wc-settings-payment-gateways.php
480 lines
| 1 | <?php |
| 2 | /** |
| 3 | * WooCommerce Checkout Settings |
| 4 | * |
| 5 | * @package WooCommerce\Admin |
| 6 | */ |
| 7 | |
| 8 | declare( strict_types = 1 ); |
| 9 | |
| 10 | use Automattic\WooCommerce\Admin\Settings\SettingsSectionRegistry; |
| 11 | use Automattic\WooCommerce\Internal\Admin\Loader; |
| 12 | |
| 13 | defined( 'ABSPATH' ) || exit; |
| 14 | |
| 15 | if ( class_exists( 'WC_Settings_Payment_Gateways', false ) ) { |
| 16 | return new WC_Settings_Payment_Gateways(); |
| 17 | } |
| 18 | |
| 19 | /** |
| 20 | * WC_Settings_Payment_Gateways. |
| 21 | */ |
| 22 | class WC_Settings_Payment_Gateways extends WC_Settings_Page { |
| 23 | |
| 24 | const TAB_NAME = 'checkout'; |
| 25 | |
| 26 | const MAIN_SECTION_NAME = 'main'; |
| 27 | const OFFLINE_SECTION_NAME = 'offline'; |
| 28 | const COD_SECTION_NAME = 'cod'; // Cash on delivery. |
| 29 | const BACS_SECTION_NAME = 'bacs'; // Direct bank transfer. |
| 30 | const CHEQUE_SECTION_NAME = 'cheque'; // Cheque payments. |
| 31 | |
| 32 | /** |
| 33 | * Setting page icon. |
| 34 | * |
| 35 | * @var string |
| 36 | */ |
| 37 | public $icon = 'payment'; |
| 38 | |
| 39 | /** |
| 40 | * Memoized list of sections to render using React. |
| 41 | * |
| 42 | * @var array|null |
| 43 | */ |
| 44 | private ?array $reactified_sections_memo = null; |
| 45 | |
| 46 | /** |
| 47 | * Constructor. |
| 48 | */ |
| 49 | public function __construct() { |
| 50 | $this->id = self::TAB_NAME; |
| 51 | $this->label = esc_html_x( 'Payments', 'Settings tab label', 'woocommerce' ); |
| 52 | |
| 53 | // Add filters and actions. |
| 54 | add_filter( 'admin_body_class', array( $this, 'add_body_classes' ), 30 ); |
| 55 | add_action( 'admin_head', array( $this, 'hide_help_tabs' ) ); |
| 56 | // Hook in as late as possible - `in_admin_header` is the last action before the `admin_notices` action is fired. |
| 57 | // It is too risky to hook into `admin_notices` with a low priority because the callbacks might be cached. |
| 58 | add_action( 'in_admin_header', array( $this, 'suppress_admin_notices' ), PHP_INT_MAX ); |
| 59 | |
| 60 | // Do not show any store alerts (WC admin notes with type: 'error,update' and status: 'unactioned') |
| 61 | // on the WooCommerce Payments settings page and Reactified sections. |
| 62 | add_filter( 'woocommerce_admin_features', array( $this, 'suppress_store_alerts' ), PHP_INT_MAX ); |
| 63 | |
| 64 | parent::__construct(); |
| 65 | } |
| 66 | |
| 67 | /** |
| 68 | * Check if the given section should be rendered using React. |
| 69 | * |
| 70 | * @param mixed $section The section name to check. |
| 71 | * Since this value originates from the global `$current_section` variable, |
| 72 | * it is best to accept anything and standardize it to a string. |
| 73 | * |
| 74 | * @return bool Whether the section should be rendered using React. |
| 75 | */ |
| 76 | public function should_render_react_section( $section ): bool { |
| 77 | return in_array( $this->standardize_section_name( $section ), $this->get_reactified_sections(), true ); |
| 78 | } |
| 79 | |
| 80 | /** |
| 81 | * Add body classes. |
| 82 | * |
| 83 | * @param string $classes The existing body classes. |
| 84 | * |
| 85 | * @return string The modified body classes. |
| 86 | */ |
| 87 | public function add_body_classes( $classes ) { |
| 88 | global $current_tab, $current_section; |
| 89 | |
| 90 | // Bail if the $classes variable is not a string. |
| 91 | if ( ! is_string( $classes ) ) { |
| 92 | return $classes; |
| 93 | } |
| 94 | |
| 95 | // If we are not on the WooCommerce Payments settings page, return the classes as they are. |
| 96 | if ( self::TAB_NAME !== $current_tab ) { |
| 97 | return $classes; |
| 98 | } |
| 99 | |
| 100 | if ( ! $this->should_render_react_section( $current_section ) ) { |
| 101 | // Add a class to indicate that the payments settings section page is rendered in legacy mode. |
| 102 | $classes .= ' woocommerce-settings-payments-section_legacy'; |
| 103 | // Add a class to indicate that the current section is rendered in legacy mode. |
| 104 | $classes .= ' woocommerce_page_wc-settings-checkout-section-' . esc_attr( $this->standardize_section_name( $current_section ) ) . '_legacy'; |
| 105 | } |
| 106 | |
| 107 | return $classes; |
| 108 | } |
| 109 | |
| 110 | /** |
| 111 | * Output the settings. |
| 112 | */ |
| 113 | public function output() { |
| 114 | // phpcs:disable WordPress.Security.NonceVerification.Recommended |
| 115 | global $current_section; |
| 116 | |
| 117 | // We don't want to output anything from the action for now. So we buffer it and discard it. |
| 118 | ob_start(); |
| 119 | /** |
| 120 | * Fires before the payment gateways settings fields are rendered. |
| 121 | * |
| 122 | * @since 1.5.7 |
| 123 | */ |
| 124 | do_action( 'woocommerce_admin_field_payment_gateways' ); |
| 125 | ob_end_clean(); |
| 126 | |
| 127 | if ( is_string( $current_section ) && $this->is_registered_settings_section( $current_section ) ) { |
| 128 | parent::output(); |
| 129 | return; |
| 130 | } |
| 131 | |
| 132 | if ( is_string( $current_section ) && $this->should_render_react_section( $current_section ) ) { |
| 133 | $this->render_react_section( $this->standardize_section_name( $current_section ) ); |
| 134 | } elseif ( is_string( $current_section ) && ! empty( $current_section ) ) { |
| 135 | // Load gateways so we can show any global options they may have. |
| 136 | $payment_gateways = WC()->payment_gateways()->payment_gateways; |
| 137 | $this->render_classic_gateway_settings_page( $payment_gateways, $current_section ); |
| 138 | } else { |
| 139 | $this->render_react_section( self::MAIN_SECTION_NAME ); |
| 140 | } |
| 141 | |
| 142 | parent::output(); |
| 143 | //phpcs:enable |
| 144 | } |
| 145 | |
| 146 | /** |
| 147 | * Get settings array. |
| 148 | * |
| 149 | * This is just for backward compatibility with the rest of the codebase (primarily API responses). |
| 150 | * |
| 151 | * @return array |
| 152 | */ |
| 153 | protected function get_settings_for_default_section() { |
| 154 | return array( |
| 155 | array( |
| 156 | 'type' => 'title', |
| 157 | // this is needed as <table> tag is generated by this element, even if it has no other content. |
| 158 | ), |
| 159 | array( |
| 160 | 'type' => 'sectionend', |
| 161 | 'id' => 'payment_gateways_options', |
| 162 | ), |
| 163 | ); |
| 164 | } |
| 165 | |
| 166 | /** |
| 167 | * Get the whitelist of sections to render using React. |
| 168 | * |
| 169 | * @return array List of section identifiers. |
| 170 | */ |
| 171 | private function get_reactified_sections(): array { |
| 172 | if ( ! is_null( $this->reactified_sections_memo ) ) { |
| 173 | return $this->reactified_sections_memo; |
| 174 | } |
| 175 | |
| 176 | // These sections are always rendered using React. |
| 177 | $reactified_sections = array( |
| 178 | self::MAIN_SECTION_NAME, |
| 179 | self::OFFLINE_SECTION_NAME, |
| 180 | ); |
| 181 | |
| 182 | // These sections are optional and can be modified by plugins or themes. |
| 183 | $optional_reactified_sections = array( |
| 184 | self::COD_SECTION_NAME, |
| 185 | self::BACS_SECTION_NAME, |
| 186 | self::CHEQUE_SECTION_NAME, |
| 187 | ); |
| 188 | |
| 189 | /** |
| 190 | * Modify the optional set of payments settings sections to be rendered using React. |
| 191 | * |
| 192 | * This filter allows plugins to add or remove optional sections (typically offline gateways) |
| 193 | * that should be rendered using React. Sections should be identified by their gateway IDs. |
| 194 | * Note: The main Payments page ("main") and the Offline overview ("offline") are always React-only |
| 195 | * and cannot be disabled via this filter. |
| 196 | * |
| 197 | * @since 9.3.0 |
| 198 | * |
| 199 | * @param array $sections List of section identifiers to be rendered using React. |
| 200 | */ |
| 201 | $optional_reactified_sections = apply_filters( 'experimental_woocommerce_admin_payment_reactify_render_sections', $optional_reactified_sections ); |
| 202 | if ( empty( $optional_reactified_sections ) || ! is_array( $optional_reactified_sections ) ) { |
| 203 | // Sanity check: use empty array if the filter returns something unexpected. |
| 204 | $optional_reactified_sections = array(); |
| 205 | } else { |
| 206 | // Enforce a list format and string-only values for section identifiers. |
| 207 | $optional_reactified_sections = array_values( array_filter( $optional_reactified_sections, 'is_string' ) ); |
| 208 | } |
| 209 | |
| 210 | $this->reactified_sections_memo = array_unique( array_merge( $reactified_sections, $optional_reactified_sections ) ); |
| 211 | |
| 212 | return $this->reactified_sections_memo; |
| 213 | } |
| 214 | |
| 215 | /** |
| 216 | * Standardize the current section name. |
| 217 | * |
| 218 | * @param mixed $section The section name to standardize. |
| 219 | * |
| 220 | * @return string The standardized section name. |
| 221 | */ |
| 222 | private function standardize_section_name( $section ): string { |
| 223 | $section = (string) $section; |
| 224 | // If the section is empty, we are on the main settings page/section. Use a standardized name. |
| 225 | if ( '' === $section ) { |
| 226 | return self::MAIN_SECTION_NAME; |
| 227 | } |
| 228 | |
| 229 | return $section; |
| 230 | } |
| 231 | |
| 232 | /** |
| 233 | * Check if a section is registered through the settings section registry. |
| 234 | * |
| 235 | * @param mixed $section Section id. |
| 236 | * @return bool |
| 237 | */ |
| 238 | private function is_registered_settings_section( $section ): bool { |
| 239 | if ( '' === (string) $section ) { |
| 240 | return false; |
| 241 | } |
| 242 | |
| 243 | $registry = $this->get_settings_section_registry(); |
| 244 | |
| 245 | return null !== $registry && null !== $registry->get_registered( self::TAB_NAME, (string) $section ); |
| 246 | } |
| 247 | |
| 248 | /** |
| 249 | * Render the React section. |
| 250 | * |
| 251 | * @param string $section The section to render. |
| 252 | */ |
| 253 | private function render_react_section( string $section ) { |
| 254 | global $hide_save_button; |
| 255 | $hide_save_button = true; |
| 256 | echo '<div id="experimental_wc_settings_payments_' . esc_attr( $section ) . '"></div>'; |
| 257 | } |
| 258 | |
| 259 | /** |
| 260 | * Render the classic gateway settings page. |
| 261 | * |
| 262 | * @param array $payment_gateways The payment gateways. |
| 263 | * @param string $current_section The current section. |
| 264 | */ |
| 265 | private function render_classic_gateway_settings_page( array $payment_gateways, string $current_section ) { |
| 266 | foreach ( $payment_gateways as $gateway ) { |
| 267 | if ( in_array( $current_section, array( $gateway->id, sanitize_title( get_class( $gateway ) ) ), true ) ) { |
| 268 | if ( isset( $_GET['toggle_enabled'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 269 | $enabled = $gateway->get_option( 'enabled' ); |
| 270 | |
| 271 | if ( $enabled ) { |
| 272 | $gateway->settings['enabled'] = wc_string_to_bool( $enabled ) ? 'no' : 'yes'; |
| 273 | } |
| 274 | } |
| 275 | $this->run_gateway_admin_options( $gateway ); |
| 276 | break; |
| 277 | } |
| 278 | } |
| 279 | } |
| 280 | |
| 281 | /** |
| 282 | * Run the 'admin_options' method on a given gateway. |
| 283 | * |
| 284 | * This method exists to help with unit testing. |
| 285 | * |
| 286 | * @param object $gateway The gateway object to run the method on. |
| 287 | */ |
| 288 | protected function run_gateway_admin_options( $gateway ) { |
| 289 | $gateway->admin_options(); |
| 290 | } |
| 291 | |
| 292 | /** |
| 293 | * Get all sections for the current page. |
| 294 | * |
| 295 | * Reactified section pages won't have any sections. |
| 296 | * The rest of the settings pages will get the default/own section and those added via |
| 297 | * the `woocommerce_get_sections_checkout` filter. |
| 298 | * |
| 299 | * @return array The sections for this settings page. |
| 300 | */ |
| 301 | public function get_sections() { |
| 302 | global $current_tab, $current_section; |
| 303 | |
| 304 | // We only want to prevent sections on the main WooCommerce Payments settings page and Reactified sections. |
| 305 | if ( self::TAB_NAME === $current_tab && $this->should_render_react_section( $current_section ) ) { |
| 306 | return array(); |
| 307 | } |
| 308 | |
| 309 | return parent::get_sections(); |
| 310 | } |
| 311 | |
| 312 | /** |
| 313 | * Save settings. |
| 314 | */ |
| 315 | public function save() { |
| 316 | global $current_section; |
| 317 | |
| 318 | $standardized_section = $this->standardize_section_name( $current_section ); |
| 319 | |
| 320 | $wc_payment_gateways = WC_Payment_Gateways::instance(); |
| 321 | |
| 322 | $this->save_settings_for_current_section(); |
| 323 | |
| 324 | if ( self::MAIN_SECTION_NAME === $standardized_section ) { |
| 325 | // This makes sure 'gateway ordering' is saved. |
| 326 | $wc_payment_gateways->process_admin_options(); |
| 327 | $wc_payment_gateways->init(); |
| 328 | } else { |
| 329 | // This may be a gateway or some custom section. |
| 330 | foreach ( $wc_payment_gateways->payment_gateways() as $gateway ) { |
| 331 | // If the section is that of a gateway, we need to run the gateway actions and init. |
| 332 | if ( in_array( $standardized_section, array( $gateway->id, sanitize_title( get_class( $gateway ) ) ), true ) ) { |
| 333 | /** |
| 334 | * Fires update actions for payment gateways. |
| 335 | * |
| 336 | * @since 3.4.0 |
| 337 | * |
| 338 | * @param int $gateway->id Gateway ID. |
| 339 | */ |
| 340 | do_action( 'woocommerce_update_options_payment_gateways_' . $gateway->id ); |
| 341 | $wc_payment_gateways->init(); |
| 342 | |
| 343 | // There is no need to run the action and gateways init again |
| 344 | // since we can't be on the section page of multiple gateways at once. |
| 345 | break; |
| 346 | } |
| 347 | } |
| 348 | |
| 349 | $this->do_update_options_action(); |
| 350 | } |
| 351 | } |
| 352 | |
| 353 | /** |
| 354 | * Hide the help tabs. |
| 355 | */ |
| 356 | public function hide_help_tabs() { |
| 357 | global $current_tab, $current_section; |
| 358 | |
| 359 | $screen = get_current_screen(); |
| 360 | if ( ! $screen instanceof WP_Screen || 'woocommerce_page_wc-settings' !== $screen->id ) { |
| 361 | return; |
| 362 | } |
| 363 | |
| 364 | // We only want to hide the help tabs on the main WooCommerce Payments settings page and Reactified sections. |
| 365 | if ( self::TAB_NAME !== $current_tab ) { |
| 366 | return; |
| 367 | } |
| 368 | if ( ! $this->should_render_react_section( $current_section ) ) { |
| 369 | return; |
| 370 | } |
| 371 | |
| 372 | $screen->remove_help_tabs(); |
| 373 | } |
| 374 | |
| 375 | /** |
| 376 | * Suppress WP admin notices on the WooCommerce Payments settings page. |
| 377 | */ |
| 378 | public function suppress_admin_notices() { |
| 379 | global $wp_filter, $current_tab, $current_section; |
| 380 | |
| 381 | $screen = get_current_screen(); |
| 382 | if ( ! $screen instanceof WP_Screen || 'woocommerce_page_wc-settings' !== $screen->id ) { |
| 383 | return; |
| 384 | } |
| 385 | |
| 386 | // We only want to suppress notices on the main WooCommerce Payments settings page and Reactified sections. |
| 387 | if ( self::TAB_NAME !== $current_tab ) { |
| 388 | return; |
| 389 | } |
| 390 | if ( ! $this->should_render_react_section( $current_section ) ) { |
| 391 | return; |
| 392 | } |
| 393 | |
| 394 | // Generic admin notices are definitely not needed. |
| 395 | remove_all_actions( 'all_admin_notices' ); |
| 396 | |
| 397 | // WooCommerce uses the 'admin_notices' hook for its own notices. |
| 398 | // We will only allow WooCommerce core notices to be displayed. |
| 399 | $wp_admin_notices_hook = $wp_filter['admin_notices'] ?? null; |
| 400 | if ( ! $wp_admin_notices_hook || ! $wp_admin_notices_hook->has_filters() ) { |
| 401 | // Nothing to do if there are no actions hooked into `admin_notices`. |
| 402 | return; |
| 403 | } |
| 404 | |
| 405 | $wc_admin_notices = WC_Admin_Notices::get_notices(); |
| 406 | if ( empty( $wc_admin_notices ) ) { |
| 407 | // If there are no WooCommerce core notices, we can remove all actions hooked into `admin_notices`. |
| 408 | remove_all_actions( 'admin_notices' ); |
| 409 | return; |
| 410 | } |
| 411 | |
| 412 | // Go through the callbacks hooked into `admin_notices` and |
| 413 | // remove any that are NOT from the WooCommerce core (i.e. from the `WC_Admin_Notices` class). |
| 414 | foreach ( $wp_admin_notices_hook->callbacks as $priority => $callbacks ) { |
| 415 | if ( ! is_array( $callbacks ) ) { |
| 416 | continue; |
| 417 | } |
| 418 | |
| 419 | foreach ( $callbacks as $callback ) { |
| 420 | // Ignore malformed callbacks. |
| 421 | if ( ! is_array( $callback ) ) { |
| 422 | continue; |
| 423 | } |
| 424 | // WooCommerce doesn't use closures to handle notices. |
| 425 | // WooCommerce core notices are handled by `WC_Admin_Notices` class methods. |
| 426 | // Remove plain functions or closures. |
| 427 | if ( ! is_array( $callback['function'] ) ) { |
| 428 | remove_action( 'admin_notices', $callback['function'], $priority ); |
| 429 | continue; |
| 430 | } |
| 431 | |
| 432 | $class_or_object = $callback['function'][0] ?? null; |
| 433 | // We need to allow Automattic\WooCommerce\Internal\Admin\Loader methods callbacks |
| 434 | // because they are used to wrap notices. |
| 435 | // @see Automattic\WooCommerce\Internal\Admin\Loader::inject_before_notices(). |
| 436 | // @see Automattic\WooCommerce\Internal\Admin\Loader::inject_after_notices(). |
| 437 | if ( |
| 438 | ( |
| 439 | // We have a class name. |
| 440 | is_string( $class_or_object ) && |
| 441 | ! ( WC_Admin_Notices::class === $class_or_object || Loader::class === $class_or_object ) |
| 442 | ) || |
| 443 | ( |
| 444 | // We have a class instance. |
| 445 | is_object( $class_or_object ) && |
| 446 | ! ( $class_or_object instanceof WC_Admin_Notices || $class_or_object instanceof Loader ) |
| 447 | ) |
| 448 | ) { |
| 449 | remove_action( 'admin_notices', $callback['function'], $priority ); |
| 450 | } |
| 451 | } |
| 452 | } |
| 453 | } |
| 454 | |
| 455 | /** |
| 456 | * Suppress the store-alerts WCAdmin feature on the WooCommerce Payments settings page and Reactified sections. |
| 457 | * |
| 458 | * @param mixed $features The WCAdmin features list. |
| 459 | * |
| 460 | * @return mixed The modified features list. |
| 461 | */ |
| 462 | public function suppress_store_alerts( $features ) { |
| 463 | global $current_tab, $current_section; |
| 464 | |
| 465 | $feature_name = 'store-alerts'; |
| 466 | |
| 467 | if ( is_array( $features ) && |
| 468 | in_array( $feature_name, $features, true ) && |
| 469 | self::TAB_NAME === $current_tab && |
| 470 | $this->should_render_react_section( $current_section ) ) { |
| 471 | |
| 472 | unset( $features[ array_search( $feature_name, $features, true ) ] ); |
| 473 | } |
| 474 | |
| 475 | return $features; |
| 476 | } |
| 477 | } |
| 478 | |
| 479 | return new WC_Settings_Payment_Gateways(); |
| 480 |