_Gateway $payment_gateway The payment gateway object. * * @return bool True if the payment gateway needs setup, false otherwise. */ public function needs_setup( WC_Payment_Gateway $payment_gateway ): bool { return filter_var( $payment_gateway->needs_setup(), FILTER_VALIDATE_BOOLEAN ); } /** * Try to determine if the payment gateway is in test mode. * * This is a best-effort attempt, as there is no standard way to determine this. * Trust the true value, but don't consider a false value as definitive. * * @param WC_Payment_Gateway $payment_gateway The payment gateway object. * * @return bool True if the payment gateway is in test mode, false otherwise. */ public function is_in_test_mode( WC_Payment_Gateway $payment_gateway ): bool { // Try various gateway methods to check if the payment gateway is in test mode. if ( is_callable( array( $payment_gateway, 'is_test_mode' ) ) ) { return filter_var( $payment_gateway->is_test_mode(), FILTER_VALIDATE_BOOLEAN ); } if ( is_callable( array( $payment_gateway, 'is_in_test_mode' ) ) ) { return filter_var( $payment_gateway->is_in_test_mode(), FILTER_VALIDATE_BOOLEAN ); } // Try various gateway option entries to check if the payment gateway is in test mode. if ( is_callable( array( $payment_gateway, 'get_option' ) ) ) { $test_mode = filter_var( $payment_gateway->get_option( 'test_mode', 'not_found' ), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ); if ( ! is_null( $test_mode ) ) { return $test_mode; } $test_mode = filter_var( $payment_gateway->get_option( 'testmode', 'not_found' ), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ); if ( ! is_null( $test_mode ) ) { return $test_mode; } } return false; } /** * Try to determine if the payment gateway is in dev mode. * * This is a best-effort attempt, as there is no standard way to determine this. * Trust the true value, but don't consider a false value as definitive. * * @param WC_Payment_Gateway $payment_gateway The payment gateway object. * * @return bool True if the payment gateway is in dev mode, false otherwise. */ public function is_in_dev_mode( WC_Payment_Gateway $payment_gateway ): bool { // Try various gateway methods to check if the payment gateway is in dev mode. if ( is_callable( array( $payment_gateway, 'is_dev_mode' ) ) ) { return filter_var( $payment_gateway->is_dev_mode(), FILTER_VALIDATE_BOOLEAN ); } if ( is_callable( array( $payment_gateway, 'is_in_dev_mode' ) ) ) { return filter_var( $payment_gateway->is_in_dev_mode(), FILTER_VALIDATE_BOOLEAN ); } return false; } /** * Check if the payment gateway has a payments processor account connected. * * @param WC_Payment_Gateway $payment_gateway The payment gateway object. * * @return bool True if the payment gateway account is connected, false otherwise. * If the payment gateway does not provide the information, it will return true. */ public function is_account_connected( WC_Payment_Gateway $payment_gateway ): bool { if ( is_callable( array( $payment_gateway, 'is_account_connected' ) ) ) { return filter_var( $payment_gateway->is_account_connected(), FILTER_VALIDATE_BOOLEAN ); } if ( is_callable( array( $payment_gateway, 'is_connected' ) ) ) { return filter_var( $payment_gateway->is_connected(), FILTER_VALIDATE_BOOLEAN ); } // Fall back to assuming that it is connected. This is the safest option. return true; } /** * Check if the payment gateway has started the onboarding process. * * @param WC_Payment_Gateway $payment_gateway The payment gateway object. * * @return bool True if the payment gateway has started the onboarding process, false otherwise. * If the payment gateway does not provide the information, * it will infer it from having a connected account. */ public function is_onboarding_started( WC_Payment_Gateway $payment_gateway ): bool { if ( is_callable( array( $payment_gateway, 'is_onboarding_started' ) ) ) { return filter_var( $payment_gateway->is_onboarding_started(), FILTER_VALIDATE_BOOLEAN ); } // Fall back to inferring this from having a connected account. return $this->is_account_connected( $payment_gateway ); } /** * Check if the payment gateway has completed the onboarding process. * * @param WC_Payment_Gateway $payment_gateway The payment gateway object. * * @return bool True if the payment gateway has completed the onboarding process, false otherwise. * If the payment gateway does not provide the information, * it will infer it from having a connected account. */ public function is_onboarding_completed( WC_Payment_Gateway $payment_gateway ): bool { // Sanity check: If the onboarding has not started, it cannot be completed. if ( ! $this->is_onboarding_started( $payment_gateway ) ) { return false; } if ( is_callable( array( $payment_gateway, 'is_onboarding_completed' ) ) ) { return filter_var( $payment_gateway->is_onboarding_completed(), FILTER_VALIDATE_BOOLEAN ); } // Note: This is what WooPayments provides, but it should become standard. if ( is_callable( array( $payment_gateway, 'is_account_partially_onboarded' ) ) ) { return ! filter_var( $payment_gateway->is_account_partially_onboarded(), FILTER_VALIDATE_BOOLEAN ); } // Fall back to inferring this from having a connected account. return $this->is_account_connected( $payment_gateway ); } /** * Try to determine if the payment gateway is in test mode onboarding (aka sandbox or test-drive). * * This is a best-effort attempt, as there is no standard way to determine this. * Trust the true value, but don't consider a false value as definitive. * * @param WC_Payment_Gateway $payment_gateway The payment gateway object. * * @return bool True if the payment gateway is in test mode onboarding, false otherwise. */ public function is_in_test_mode_onboarding( WC_Payment_Gateway $payment_gateway ): bool { // Try various gateway methods to check if the payment gateway is in test mode onboarding. if ( is_callable( array( $payment_gateway, 'is_test_mode_onboarding' ) ) ) { return filter_var( $payment_gateway->is_test_mode_onboarding(), FILTER_VALIDATE_BOOLEAN ); } if ( is_callable( array( $payment_gateway, 'is_in_test_mode_onboarding' ) ) ) { return filter_var( $payment_gateway->is_in_test_mode_onboarding(), FILTER_VALIDATE_BOOLEAN ); } return false; } /** * Get the settings URL for a payment gateway. * * @param WC_Payment_Gateway $payment_gateway The payment gateway object. * * @return string The settings URL for the payment gateway. */ public function get_settings_url( WC_Payment_Gateway $payment_gateway ): string { if ( is_callable( array( $payment_gateway, 'get_settings_url' ) ) ) { return (string) $payment_gateway->get_settings_url(); } return Utils::wc_payments_settings_url( null, array( 'section' => strtolower( $payment_gateway->id ), 'from' => Payments::FROM_PAYMENTS_SETTINGS, ) ); } /** * Get the onboarding URL for the payment gateway. * * This URL should start or continue the onboarding process. * * @param WC_Payment_Gateway $payment_gateway The payment gateway object. * @param string $return_url Optional. The URL to return to after onboarding. * This will likely get attached to the onboarding URL. * * @return string The onboarding URL for the payment gateway. */ public function get_onboarding_url( WC_Payment_Gateway $payment_gateway, string $return_url = '' ): string { if ( is_callable( array( $payment_gateway, 'get_connection_url' ) ) ) { // If we received no return URL, we will set the WC Payments Settings page as the return URL. $return_url = ! empty( $return_url ) ? $return_url : admin_url( 'admin.php?page=wc-settings&tab=checkout&from=' . Payments::FROM_PROVIDER_ONBOARDING ); return (string) $payment_gateway->get_connection_url( $return_url ); } // Fall back to pointing users to the payment gateway settings page to handle onboarding. return $this->get_settings_url( $payment_gateway ); } /** * Get the plugin details for a payment gateway. * * @param WC_Payment_Gateway $payment_gateway The payment gateway object. * * @return array The plugin details for the payment gateway. */ public function get_plugin_details( WC_Payment_Gateway $payment_gateway ): array { $entity_type = $this->get_containing_entity_type( $payment_gateway ); return array( '_type' => $entity_type, 'slug' => $this->get_plugin_slug( $payment_gateway ), // Only include the plugin file if the entity type is a regular plugin. // We don't want to try to change the state of must-use plugins or themes. 'file' => PaymentProviders::EXTENSION_TYPE_WPORG === $entity_type ? $this->get_plugin_file( $payment_gateway ) : '', // The gateway's underlying plugin is obviously active (aka the code is running). 'status' => PaymentProviders::EXTENSION_ACTIVE, ); } /** * Get the source plugin slug of a payment gateway instance. * * It accounts for both regular and must-use plugins. * If the gateway is registered through a theme, it will return the theme slug. * * @param WC_Payment_Gateway $payment_gateway The payment gateway object. * * @return string The plugin slug of the payment gateway. * Empty string if a plugin slug could not be determined. */ public function get_plugin_slug( WC_Payment_Gateway $payment_gateway ): string { global $wp_theme_directories; // If the payment gateway object has a `plugin_slug` property, use it. // This is useful for testing. if ( isset( $payment_gateway->plugin_slug ) ) { return (string) $payment_gateway->plugin_slug; } $gateway_class_filename = $this->get_class_filename( $payment_gateway ); // Bail if we couldn't get the gateway class filename. if ( ! is_string( $gateway_class_filename ) ) { return ''; } $entity_type = $this->get_containing_entity_type( $payment_gateway ); // Bail if we couldn't determine the entity type. if ( PaymentProviders::EXTENSION_TYPE_UNKNOWN === $entity_type ) { return ''; } if ( PaymentProviders::EXTENSION_TYPE_THEME === $entity_type ) { // Find the theme directory it is part of and extract the slug. // This accounts for both parent and child themes. if ( is_array( $wp_theme_directories ) ) { foreach ( $wp_theme_directories as $dir ) { if ( str_starts_with( $gateway_class_filename, $dir ) ) { return $this->extract_slug_from_path( substr( $gateway_class_filename, strlen( $dir ) ) ); } } } // Bail if we couldn't find a match. return ''; } // By this point, we know that the payment gateway is part of a plugin. // Extract the relative path of the class file to the plugins directory. // We account for both regular and must-use plugins. $gateway_class_plugins_path = trim( plugin_basename( $gateway_class_filename ), DIRECTORY_SEPARATOR ); return $this->extract_slug_from_path( $gateway_class_plugins_path ); } /** * Get the corresponding plugin file of the payment gateway, without the .php extension. * * This is useful for using the WP API to change the state of the plugin (activate or deactivate). * We remove the .php extension since the WP API expects plugin files without it. * * @param WC_Payment_Gateway $payment_gateway The payment gateway object. * @param string $plugin_slug Optional. The payment gateway plugin slug to use directly. * * @return string The plugin file corresponding to the payment gateway plugin. Does not include the .php extension. * In case of failures, it will return an empty string. */ public function get_plugin_file( WC_Payment_Gateway $payment_gateway, string $plugin_slug = '' ): string { // If the payment gateway object has a `plugin_file` property, use it. // This is useful for testing. if ( isset( $payment_gateway->plugin_file ) ) { $plugin_file = $payment_gateway->plugin_file; // Sanity check. if ( ! is_string( $plugin_file ) ) { return ''; } // Remove the .php extension from the file path. The WP API expects it without it. return Utils::trim_php_file_extension( $plugin_file ); } if ( empty( $plugin_slug ) ) { $plugin_slug = $this->get_plugin_slug( $payment_gateway ); } // Bail if we couldn't determine the plugin slug. if ( empty( $plugin_slug ) ) { return ''; } $plugin_file = PluginsHelper::get_plugin_path_from_slug( $plugin_slug ); // Bail if we couldn't determine the plugin file. if ( ! is_string( $plugin_file ) || empty( $plugin_file ) ) { return ''; } // Remove the .php extension from the file path. The WP API expects it without it. return Utils::trim_php_file_extension( $plugin_file ); } /** * Try and determine a list of recommended payment methods for a payment gateway. * * This data is not always available, and it is up to the payment gateway to provide it. * This is not a definitive list of payment methods that the gateway supports. * The data is aimed at helping the user understand what payment methods are recommended for the gateway * and potentially help them make a decision on which payment methods to enable. * * @param WC_Payment_Gateway $payment_gateway The payment gateway object. * @param string $country_code Optional. The country code for which to get recommended payment methods. * This should be a ISO 3166-1 alpha-2 country code. * * @return array The recommended payment methods list for the payment gateway. * Empty array if there are none. */ public function get_recommended_payment_methods( WC_Payment_Gateway $payment_gateway, string $country_code = '' ): array { // Bail if the payment gateway does not implement the method. if ( ! is_callable( array( $payment_gateway, 'get_recommended_payment_methods' ) ) ) { return array(); } // Get the "raw" recommended payment methods from the payment gateway. $recommended_pms = call_user_func_array( array( $payment_gateway, 'get_recommended_payment_methods' ), array( 'country_code' => $country_code ), ); if ( ! is_array( $recommended_pms ) ) { // Bail if the recommended payment methods are not an array. return array(); } // Validate the received list items. $recommended_pms = array_filter( $recommended_pms, array( $this, 'validate_recommended_payment_method' ) ); // Sort the list. $recommended_pms = $this->sort_recommended_payment_methods( $recommended_pms ); // Extract, standardize, and sanitize the details for each recommended payment method. $standardized_pms = array(); foreach ( $recommended_pms as $index => $recommended_pm ) { // Use the index as the order since we sorted (and normalized) the list earlier. $standardized_pms[] = $this->standardize_recommended_payment_method( $recommended_pm, $index ); } return $standardized_pms; } /** * Validate a recommended payment method entry. * * @param mixed $recommended_pm The recommended payment method entry to validate. * * @return bool True if the recommended payment method entry is valid, false otherwise. */ protected function validate_recommended_payment_method( $recommended_pm ): bool { // We require at least `id` and `title`. return is_array( $recommended_pm ) && ! empty( $recommended_pm['id'] ) && ! empty( $recommended_pm['title'] ); } /** * Sort the recommended payment methods. * * @param array $recommended_pms The recommended payment methods list to sort. * * @return array The sorted recommended payment methods list. * List keys are not preserved. */ protected function sort_recommended_payment_methods( array $recommended_pms ): array { // Sort the recommended payment methods by order/priority, if available. usort( $recommended_pms, function ( $a, $b ) { // `order` takes precedence over `priority`. // Entries that don't have the order/priority are placed at the end. return array( ( $a['order'] ?? PHP_INT_MAX ), ( $a['priority'] ?? PHP_INT_MAX ) ) <=> array( ( $b['order'] ?? PHP_INT_MAX ), ( $b['priority'] ?? PHP_INT_MAX ) ); } ); return array_values( $recommended_pms ); } /** * Standardize a recommended payment method entry. * * @param array $recommended_pm The recommended payment method entry to standardize. * @param int $order Optional. The order of the recommended payment method. * Defaults to 0 if not provided. * * @return array The standardized recommended payment method entry. */ protected function standardize_recommended_payment_method( array $recommended_pm, int $order = 0 ): array { $standard_details = array( 'id' => sanitize_key( $recommended_pm['id'] ), '_order' => $order, // Default to enabled if not explicit. 'enabled' => filter_var( $recommended_pm['enabled'] ?? true, FILTER_VALIDATE_BOOLEAN ), // Default to not required if not explicit. 'required' => filter_var( $recommended_pm['required'] ?? false, FILTER_VALIDATE_BOOLEAN ), 'title' => sanitize_text_field( $recommended_pm['title'] ), 'description' => '', 'icon' => '', 'category' => self::PAYMENT_METHOD_CATEGORY_PRIMARY, // Default to primary. ); // If the payment method has a description, sanitize it before use. if ( ! empty( $recommended_pm['description'] ) ) { $standard_details['description'] = (string) $recommended_pm['description']; // Make sure that if we have HTML tags, we only allow stylistic tags and anchors. if ( preg_match( '/<[^>]+>/', $standard_details['description'] ) ) { // Only allow stylistic tags with a few modifications. $allowed_tags = wp_kses_allowed_html( 'data' ); $allowed_tags = array_merge( $allowed_tags, array( 'a' => array( 'href' => true, 'target' => true, ), ) ); $standard_details['description'] = wp_kses( $standard_details['description'], $allowed_tags ); } } // If the payment method has an icon, try to use it. if ( ! empty( $recommended_pm['icon'] ) && wc_is_valid_url( $recommended_pm['icon'] ) ) { $standard_details['icon'] = sanitize_url( $recommended_pm['icon'] ); } // If the payment method has a category, use it if it's one of the known categories. if ( ! empty( $recommended_pm['category'] ) && in_array( $recommended_pm['category'], array( self::PAYMENT_METHOD_CATEGORY_PRIMARY, self::PAYMENT_METHOD_CATEGORY_SECONDARY ), true ) ) { $standard_details['category'] = $recommended_pm['category']; } return $standard_details; } /** * Get the filename of the payment gateway class. * * @param WC_Payment_Gateway $payment_gateway The payment gateway object. * * @return string|null The filename of the payment gateway class or null if it cannot be determined. */ private function get_class_filename( WC_Payment_Gateway $payment_gateway ): ?string { // If the payment gateway object has a `class_filename` property, use it. // It is only used in development environments (including when running tests). if ( isset( $payment_gateway->class_filename ) && in_array( wp_get_environment_type(), array( 'local', 'development' ), true ) ) { $class_filename = $payment_gateway->class_filename; } else { try { $reflector = new \ReflectionClass( get_class( $payment_gateway ) ); $class_filename = $reflector->getFileName(); } catch ( Throwable $e ) { // Bail if we couldn't get the gateway class filename. return null; } } // Bail if we couldn't get the gateway class filename. if ( ! is_string( $class_filename ) ) { return null; } return $class_filename; } /** * Get the type of entity the payment gateway class is contained in. * * @param WC_Payment_Gateway $payment_gateway The payment gateway object. * * @return string The type of extension containing the payment gateway class. */ private function get_containing_entity_type( WC_Payment_Gateway $payment_gateway ): string { global $wp_plugin_paths, $wp_theme_directories; // If the payment gateway object has a `extension_type` property, use it. // This is useful for testing. if ( isset( $payment_gateway->extension_type ) ) { // Validate the extension type. if ( ! in_array( $payment_gateway->extension_type, array( PaymentProviders::EXTENSION_TYPE_WPORG, PaymentProviders::EXTENSION_TYPE_MU_PLUGIN, PaymentProviders::EXTENSION_TYPE_THEME, ), true ) ) { return PaymentProviders::EXTENSION_TYPE_UNKNOWN; } return $payment_gateway->extension_type; } $gateway_class_filename = $this->get_class_filename( $payment_gateway ); // Bail if we couldn't get the gateway class filename. if ( ! is_string( $gateway_class_filename ) ) { return PaymentProviders::EXTENSION_TYPE_UNKNOWN; } // Plugin paths logic closely matches the one in plugin_basename(). // $wp_plugin_paths contains normalized paths. $file = wp_normalize_path( $gateway_class_filename ); arsort( $wp_plugin_paths ); // Account for symlinks in the plugin paths. foreach ( $wp_plugin_paths as $dir => $realdir ) { if ( str_starts_with( $file, $realdir ) ) { $gateway_class_filename = $dir . substr( $gateway_class_filename, strlen( $realdir ) ); } } // Test for regular plugins. if ( str_starts_with( $gateway_class_filename, wp_normalize_path( WP_PLUGIN_DIR ) ) ) { // For now, all plugins are considered WordPress.org plugins. return PaymentProviders::EXTENSION_TYPE_WPORG; } // Test for must-use plugins. if ( str_starts_with( $gateway_class_filename, wp_normalize_path( WPMU_PLUGIN_DIR ) ) ) { return PaymentProviders::EXTENSION_TYPE_MU_PLUGIN; } // Check if it is part of a theme. if ( is_array( $wp_theme_directories ) ) { foreach ( $wp_theme_directories as $dir ) { // Check if the class file is in a theme directory. if ( str_starts_with( $gateway_class_filename, $dir ) ) { return PaymentProviders::EXTENSION_TYPE_THEME; } } } // Default to an unknown type. return PaymentProviders::EXTENSION_TYPE_UNKNOWN; } /** * Extract the slug from a given path. * * It can be a directory or file path. * This should be a relative path since the top-level directory or file name will be used as the slug. * * @param string $path The path to extract the slug from. * * @return string The slug extracted from the path. */ private function extract_slug_from_path( string $path ): string { $path = trim( $path ); $path = trim( $path, DIRECTORY_SEPARATOR ); // If the path is just a file name, use it as the slug. if ( false === strpos( $path, DIRECTORY_SEPARATOR ) ) { return Utils::trim_php_file_extension( $path ); } $parts = explode( DIRECTORY_SEPARATOR, $path ); // Bail if we couldn't get the parts. if ( ! is_array( $parts ) ) { return ''; } return reset( $parts ); } }