PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 11.0.0-beta.1
WooCommerce v11.0.0-beta.1
11.0.0-beta.1 10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / src / Internal / Admin / Settings / PaymentsProviders / PaymentGateway.php
woocommerce / src / Internal / Admin / Settings / PaymentsProviders Last commit date
WooPayments 6 days ago Affirm.php 1 year ago AfterpayClearpay.php 1 year ago Airwallex.php 1 year ago AmazonPay.php 1 year ago Antom.php 1 year ago Eway.php 10 months ago GoCardless.php 1 year ago HelioPay.php 1 year ago Klarna.php 1 year ago KlarnaCheckout.php 1 year ago MercadoPago.php 1 year ago Mollie.php 1 year ago Monei.php 1 year ago NexiCheckout.php 10 months ago PayPal.php 1 year ago PayUIndia.php 1 year ago Payfast.php 1 year ago PaymentGateway.php 3 months ago Paymob.php 1 year ago Payoneer.php 1 year ago Paystack.php 1 year ago Paytrail.php 1 year ago PseudoWCPaymentGateway.php 1 year ago Razorpay.php 1 year ago Stripe.php 1 year ago Tilopay.php 1 year ago Visa.php 10 months ago Vivacom.php 1 year ago WCCore.php 7 months ago WooPayments.php 7 months ago
PaymentGateway.php
1280 lines
1 <?php
2 declare( strict_types=1 );
3
4 namespace Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders;
5
6 use Automattic\WooCommerce\Admin\PluginsHelper;
7 use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders;
8 use Automattic\WooCommerce\Internal\Admin\Settings\Payments;
9 use Automattic\WooCommerce\Internal\Admin\Settings\Utils;
10 use Automattic\WooCommerce\Internal\Logging\SafeGlobalFunctionProxy;
11 use Automattic\WooCommerce\Proxies\LegacyProxy;
12 use Throwable;
13 use WC_HTTPS;
14 use WC_Payment_Gateway;
15
16 defined( 'ABSPATH' ) || exit;
17
18 /**
19 * The payment gateway provider class to handle all payment gateways that don't have a dedicated class.
20 *
21 * Extend this class for introducing gateway-specific behavior.
22 */
23 class PaymentGateway {
24
25 // This is the default onboarding type for all gateways.
26 // It means that the payment extension will handle the onboarding.
27 const ONBOARDING_TYPE_EXTERNAL = 'external';
28
29 // This is the onboarding type for gateways that have a WooCommerce-tailored onboarding flow.
30 // This might mean just having the payment methods select step in the WooCommerce settings.
31 const ONBOARDING_TYPE_NATIVE = 'native';
32
33 // This is the onboarding type for gateways that have a WooCommerce in-context onboarding flow.
34 const ONBOARDING_TYPE_NATIVE_IN_CONTEXT = 'native_in_context';
35
36 // Payment method categories to inform the UI about grouping or the emphasis of payment methods.
37 const PAYMENT_METHOD_CATEGORY_PRIMARY = 'primary';
38 const PAYMENT_METHOD_CATEGORY_SECONDARY = 'secondary';
39
40 /**
41 * The LegacyProxy instance.
42 *
43 * @var LegacyProxy
44 */
45 protected LegacyProxy $proxy;
46
47 /**
48 * Constructor.
49 *
50 * @param LegacyProxy $proxy The LegacyProxy instance.
51 */
52 public function __construct( LegacyProxy $proxy ) {
53 $this->proxy = $proxy;
54 }
55
56 /**
57 * Extract the payment gateway provider details from the object.
58 *
59 * @param WC_Payment_Gateway $gateway The payment gateway object.
60 * @param int $order Optional. The order to assign.
61 * Defaults to 0 if not provided.
62 * @param string $country_code Optional. The country code for which the details are being gathered.
63 * This should be an ISO 3166-1 alpha-2 country code.
64 *
65 * @return array The payment gateway provider details.
66 */
67 public function get_details( WC_Payment_Gateway $gateway, int $order = 0, string $country_code = '' ): array {
68 $onboarding_supported = $this->is_onboarding_supported( $gateway, $country_code ) ?? true; // Assume supported if unknown.
69
70 return array(
71 'id' => $gateway->id,
72 '_order' => $order,
73 'title' => $this->get_title( $gateway ),
74 'description' => $this->get_description( $gateway ),
75 'icon' => $this->get_icon( $gateway ),
76 'supports' => $this->get_supports_list( $gateway ),
77 'links' => $this->get_provider_links( $gateway, $country_code ),
78 'state' => array(
79 'enabled' => $this->is_enabled( $gateway ),
80 'account_connected' => $this->is_account_connected( $gateway ),
81 'needs_setup' => $this->needs_setup( $gateway ),
82 'test_mode' => $this->is_in_test_mode( $gateway ),
83 'dev_mode' => $this->is_in_dev_mode( $gateway ),
84 ),
85 'management' => array(
86 '_links' => array(
87 'settings' => array(
88 'href' => $this->get_settings_url( $gateway ),
89 ),
90 ),
91 ),
92 'onboarding' => array(
93 'type' => self::ONBOARDING_TYPE_EXTERNAL,
94 'state' => array(
95 'supported' => $onboarding_supported,
96 'started' => $this->is_onboarding_started( $gateway ),
97 'completed' => $this->is_onboarding_completed( $gateway ),
98 'test_mode' => $this->is_in_test_mode_onboarding( $gateway ),
99 ),
100 'messages' => array(
101 'not_supported' => ! $onboarding_supported ? $this->get_onboarding_not_supported_message( $gateway, $country_code ) : null,
102 ),
103 '_links' => array(
104 'onboard' => array(
105 'href' => $this->get_onboarding_url( $gateway ),
106 ),
107 ),
108 'recommended_payment_methods' => $this->get_recommended_payment_methods( $gateway, $country_code ),
109 ),
110 'plugin' => $this->get_plugin_details( $gateway ),
111 );
112 }
113
114 /**
115 * Enhance this provider's payment extension suggestion with additional information.
116 *
117 * The details added do not require the payment extension to be active or a gateway instance.
118 *
119 * @param array $extension_suggestion The extension suggestion details.
120 *
121 * @return array The enhanced payment extension suggestion details.
122 */
123 public function enhance_extension_suggestion( array $extension_suggestion ): array {
124 if ( empty( $extension_suggestion['onboarding'] ) || ! is_array( $extension_suggestion['onboarding'] ) ) {
125 $extension_suggestion['onboarding'] = array();
126 }
127
128 if ( ! isset( $extension_suggestion['onboarding']['type'] ) ) {
129 $extension_suggestion['onboarding']['type'] = self::ONBOARDING_TYPE_EXTERNAL;
130 }
131
132 return $extension_suggestion;
133 }
134
135 /**
136 * Get the provider title of the payment gateway.
137 *
138 * This is the intended gateway title to use throughout the WC admin. It should be short.
139 *
140 * Note: We don't allow HTML tags in the title. All HTML tags will be stripped, including their contents.
141 *
142 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
143 *
144 * @return string The provider title of the payment gateway.
145 */
146 public function get_title( WC_Payment_Gateway $payment_gateway ): string {
147 $title = $payment_gateway->get_method_title();
148 // If we still couldn't get the WC admin title, fall back to the main title.
149 if ( ! is_string( $title ) || empty( $title ) ) {
150 $title = $payment_gateway->get_title();
151 }
152 // If we still couldn't get the title, return a default value.
153 if ( ! is_string( $title ) || empty( $title ) ) {
154 return esc_html__( 'Unknown', 'woocommerce' );
155 }
156
157 // No HTML tags allowed in the title.
158 $title = wp_strip_all_tags( html_entity_decode( $title, ENT_QUOTES | ENT_SUBSTITUTE ), true );
159
160 // Truncate the title.
161 return Utils::truncate_with_words( $title, 75 );
162 }
163
164 /**
165 * Get the provider description of the payment gateway.
166 *
167 * This is the intended gateway description to use throughout the WC admin. It should be short and to the point.
168 *
169 * Note: We don't allow HTML tags in the description. All HTML tags will be stripped, including their contents.
170 *
171 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
172 *
173 * @return string The provider description of the payment gateway.
174 */
175 public function get_description( WC_Payment_Gateway $payment_gateway ): string {
176 $description = $payment_gateway->get_method_description();
177 // If we couldn't get the WC admin description, fall back to the main description.
178 if ( ! is_string( $description ) || empty( $description ) ) {
179 $description = $payment_gateway->get_description();
180 }
181 // If we still couldn't get the description, use an empty string since the description is not critical.
182 if ( ! is_string( $description ) || empty( $description ) ) {
183 return '';
184 }
185
186 // No HTML tags allowed in the description.
187 $description = wp_strip_all_tags( html_entity_decode( $description, ENT_QUOTES | ENT_SUBSTITUTE ), true );
188
189 // Truncate the description.
190 return Utils::truncate_with_words( $description, 130, '' );
191 }
192
193 /**
194 * Get the provider icon URL of the payment gateway.
195 *
196 * We expect to receive a URL to an image file.
197 * If the gateway provides an <img> tag or a list of them, we will fall back to the default payments icon.
198 *
199 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
200 *
201 * @return string The provider icon URL of the payment gateway.
202 */
203 public function get_icon( WC_Payment_Gateway $payment_gateway ): string {
204 $icon_url = $payment_gateway->icon ?? '';
205 if ( ! is_string( $icon_url ) || empty( $icon_url ) ) {
206 $icon_url = '';
207 }
208
209 $icon_url = trim( $icon_url );
210
211 // Test if it actually is a URL as some gateways put an <img> tag or a list of them.
212 if ( ! wc_is_valid_url( $icon_url ) ) {
213 // Fall back to the default payments icon.
214 return plugins_url( 'assets/images/icons/default-payments.svg', WC_PLUGIN_FILE );
215 }
216
217 return WC_HTTPS::force_https_url( $icon_url );
218 }
219
220 /**
221 * Get the provider supports list of the payment gateway.
222 *
223 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
224 *
225 * @return string[] The provider supports list of the payment gateway.
226 */
227 public function get_supports_list( WC_Payment_Gateway $payment_gateway ): array {
228 $supports_list = $payment_gateway->supports ?? array();
229 if ( ! is_array( $supports_list ) ) {
230 return array();
231 }
232
233 // Sanitize the list to ensure it only contains a list of key-like strings.
234 $sanitized_list = array();
235 foreach ( $supports_list as $support ) {
236 if ( ! is_string( $support ) ) {
237 continue;
238 }
239
240 $sanitized_list[] = sanitize_key( $support );
241 }
242
243 // Ensure the list contains unique values and re-indexed.
244 return array_values( array_unique( $sanitized_list ) );
245 }
246
247 /**
248 * Get the provider links list.
249 *
250 * These are contextual, in general external links aimed to help the user learn more about the payment provider and
251 * reach out for help.
252 *
253 * Each link is an associative array with '_type' and 'url' keys.
254 * The type is a string indicating the type of link, e.g., 'documentation', 'support', 'pricing', etc.
255 * The only accepted types are the ones documented in the PaymentsProviders::LINK_TYPE_* constants.
256 *
257 * Example:
258 * array(
259 * array(
260 * '_type' => 'documentation',
261 * 'url' => 'https://example.com/docs',
262 * ),
263 * array(
264 * '_type' => 'support',
265 * 'url' => 'https://example.com/support',
266 * ),
267 * );
268 *
269 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
270 * @param string $country_code Optional. The country code for which the providers are being requested.
271 * This should be an ISO 3166-1 alpha-2 country code.
272 * If invalid, it will be ignored.
273 *
274 * @return array The provider links list. Empty array if none are available or an error occurs.
275 */
276 public function get_provider_links( WC_Payment_Gateway $payment_gateway, string $country_code = '' ): array {
277 $country_code = strtoupper( sanitize_text_field( $country_code ) );
278 // Validate the country code format - expect ISO 3166-1 alpha-2.
279 // Empty country code is valid (parameter is optional), so only validate non-empty values.
280 if ( '' !== $country_code && ( strlen( $country_code ) !== 2 || ! ctype_upper( $country_code ) ) ) {
281 // Log so we can investigate non-empty invalid country codes.
282 SafeGlobalFunctionProxy::wc_get_logger()->debug(
283 'Received invalid country code when getting provider links. Ignoring it.',
284 array(
285 'gateway' => $payment_gateway->id,
286 'source' => 'settings-payments',
287 'country' => $country_code,
288 )
289 );
290
291 $country_code = '';
292 }
293
294 $provider_links = array();
295
296 try {
297 // Try to get the links list from the payment gateway if it provides such method.
298 if ( method_exists( $payment_gateway, 'get_provider_links' ) &&
299 is_callable( array( $payment_gateway, 'get_provider_links' ) ) ) {
300
301 $provider_links = call_user_func(
302 array( $payment_gateway, 'get_provider_links' ),
303 $country_code
304 );
305
306 // Validate and normalize the links list.
307 $accepted_types = array(
308 PaymentsProviders::LINK_TYPE_ABOUT,
309 PaymentsProviders::LINK_TYPE_DOCS,
310 PaymentsProviders::LINK_TYPE_SUPPORT,
311 PaymentsProviders::LINK_TYPE_PRICING,
312 PaymentsProviders::LINK_TYPE_TERMS,
313 );
314 $validated_links = array();
315 if ( is_array( $provider_links ) ) {
316 foreach ( $provider_links as $link ) {
317 if ( ! is_array( $link ) ) {
318 continue;
319 }
320
321 $type = ( isset( $link['_type'] ) && is_scalar( $link['_type'] ) ) ? sanitize_key( (string) $link['_type'] ) : '';
322 if ( empty( $type ) || ! in_array( $type, $accepted_types, true ) ) {
323 continue;
324 }
325 if ( empty( $link['url'] ) || ! is_string( $link['url'] ) || ! wc_is_valid_url( $link['url'] ) ) {
326 continue;
327 }
328
329 $url = sanitize_url( $link['url'] );
330
331 // Create a unique key for deduplication (type + URL).
332 $link_key = $type . '|' . $url;
333
334 // Skip if we already have this exact link.
335 if ( isset( $validated_links[ $link_key ] ) ) {
336 continue;
337 }
338
339 $validated_links[ $link_key ] = array(
340 '_type' => $type,
341 'url' => $url,
342 );
343 }
344 }
345
346 $provider_links = array_values( $validated_links );
347 }
348 } catch ( Throwable $e ) {
349 // Do nothing but log so we can investigate.
350 SafeGlobalFunctionProxy::wc_get_logger()->debug(
351 'Failed to get provider links: ' . $e->getMessage(),
352 array(
353 'gateway' => $payment_gateway->id,
354 'source' => 'settings-payments',
355 'exception' => $e,
356 )
357 );
358
359 return array();
360 }
361
362 return $provider_links;
363 }
364
365 /**
366 * Check if the payment gateway is enabled.
367 *
368 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
369 *
370 * @return bool True if the payment gateway is enabled, false otherwise.
371 */
372 public function is_enabled( WC_Payment_Gateway $payment_gateway ): bool {
373 try {
374 return wc_string_to_bool( $payment_gateway->enabled ?? 'no' );
375 } catch ( Throwable $e ) {
376 // Do nothing but log so we can investigate.
377 SafeGlobalFunctionProxy::wc_get_logger()->debug(
378 'Failed to determine if gateway is enabled: ' . $e->getMessage(),
379 array(
380 'gateway' => $payment_gateway->id,
381 'source' => 'settings-payments',
382 'exception' => $e,
383 )
384 );
385 }
386
387 // If we reach here, just assume that the gateway is not enabled.
388 return false;
389 }
390
391 /**
392 * Check if the payment gateway needs setup.
393 *
394 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
395 *
396 * @return bool True if the payment gateway needs setup, false otherwise.
397 */
398 public function needs_setup( WC_Payment_Gateway $payment_gateway ): bool {
399 try {
400 $needs_setup = wc_string_to_bool( $payment_gateway->needs_setup() );
401 // If we get a true value, it means the gateway needs setup.
402 if ( $needs_setup ) {
403 return true;
404 }
405 } catch ( Throwable $e ) {
406 // Do nothing but log so we can investigate.
407 SafeGlobalFunctionProxy::wc_get_logger()->debug(
408 'Failed to determine if gateway needs setup: ' . $e->getMessage(),
409 array(
410 'gateway' => $payment_gateway->id,
411 'source' => 'settings-payments',
412 'exception' => $e,
413 )
414 );
415 }
416
417 // If we get a false value, it might mean that it doesn't need setup,
418 // but it can also mean that the gateway does not provide the information and just falls back to the default.
419 // Check if there is a connected account, as that is the most common indicator of a setup.
420 if ( ! $this->is_account_connected( $payment_gateway ) ) {
421 return true;
422 }
423
424 // If we reach here, just assume that the gateway does not need setup.
425 return false;
426 }
427
428 /**
429 * Try to determine if the payment gateway is in test mode.
430 *
431 * This is a best-effort attempt, as there is no standard way to determine this.
432 * Trust the true value, but don't consider a false value as definitive.
433 *
434 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
435 *
436 * @return bool True if the payment gateway is in test mode, false otherwise.
437 */
438 public function is_in_test_mode( WC_Payment_Gateway $payment_gateway ): bool {
439 try {
440 // Try various gateway methods to check if the payment gateway is in test mode.
441 if ( is_callable( array( $payment_gateway, 'is_test_mode' ) ) ) {
442 return wc_string_to_bool( $payment_gateway->is_test_mode() );
443 }
444 if ( is_callable( array( $payment_gateway, 'is_in_test_mode' ) ) ) {
445 return wc_string_to_bool( $payment_gateway->is_in_test_mode() );
446 }
447
448 // Try various gateway public properties to check if the payment gateway is in test mode.
449 if ( isset( $payment_gateway->testmode ) ) {
450 return wc_string_to_bool( $payment_gateway->testmode );
451 }
452 if ( isset( $payment_gateway->test_mode ) ) {
453 return wc_string_to_bool( $payment_gateway->test_mode );
454 }
455
456 // Try various gateway option entries to check if the payment gateway is in test mode.
457 if ( is_callable( array( $payment_gateway, 'get_option' ) ) ) {
458 $test_mode = filter_var( $payment_gateway->get_option( 'test_mode', 'not_found' ), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE );
459 if ( ! is_null( $test_mode ) ) {
460 return $test_mode;
461 }
462
463 $test_mode = filter_var( $payment_gateway->get_option( 'testmode', 'not_found' ), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE );
464 if ( ! is_null( $test_mode ) ) {
465 return $test_mode;
466 }
467
468 $mode = strtolower( (string) $payment_gateway->get_option( 'mode', 'not_found' ) );
469 if ( in_array( $mode, array( 'test', 'sandbox', 'dev' ), true ) ) {
470 return true;
471 } elseif ( in_array( $mode, array( 'live', 'production', 'prod' ), true ) ) {
472 return false;
473 }
474 }
475 } catch ( Throwable $e ) {
476 // Do nothing but log so we can investigate.
477 SafeGlobalFunctionProxy::wc_get_logger()->debug(
478 'Failed to determine if gateway is in test mode: ' . $e->getMessage(),
479 array(
480 'gateway' => $payment_gateway->id,
481 'source' => 'settings-payments',
482 'exception' => $e,
483 )
484 );
485 }
486
487 return false;
488 }
489
490 /**
491 * Try to determine if the payment gateway is in dev mode.
492 *
493 * This is a best-effort attempt, as there is no standard way to determine this.
494 * Trust the true value, but don't consider a false value as definitive.
495 *
496 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
497 *
498 * @return bool True if the payment gateway is in dev mode, false otherwise.
499 */
500 public function is_in_dev_mode( WC_Payment_Gateway $payment_gateway ): bool {
501 try {
502 // Try various gateway methods to check if the payment gateway is in dev mode.
503 if ( is_callable( array( $payment_gateway, 'is_dev_mode' ) ) ) {
504 return wc_string_to_bool( $payment_gateway->is_dev_mode() );
505 }
506 if ( is_callable( array( $payment_gateway, 'is_in_dev_mode' ) ) ) {
507 return wc_string_to_bool( $payment_gateway->is_in_dev_mode() );
508 }
509 } catch ( Throwable $e ) {
510 // Do nothing but log so we can investigate.
511 SafeGlobalFunctionProxy::wc_get_logger()->debug(
512 'Failed to determine if gateway is in dev mode: ' . $e->getMessage(),
513 array(
514 'gateway' => $payment_gateway->id,
515 'source' => 'settings-payments',
516 'exception' => $e,
517 )
518 );
519 }
520
521 return false;
522 }
523
524 /**
525 * Check if the payment gateway has a payments processor account connected.
526 *
527 * Note: Be extra careful if you override this method and rely on needs_setup() since it could lead to an infinite loop.
528 *
529 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
530 *
531 * @return bool True if the payment gateway account is connected, false otherwise.
532 * If the payment gateway does not provide the information, it will return true.
533 */
534 public function is_account_connected( WC_Payment_Gateway $payment_gateway ): bool {
535 try {
536 if ( method_exists( $payment_gateway, 'is_account_connected' ) && is_callable( array( $payment_gateway, 'is_account_connected' ) ) ) {
537 return wc_string_to_bool( $payment_gateway->is_account_connected() );
538 }
539
540 if ( method_exists( $payment_gateway, 'is_connected' ) && is_callable( array( $payment_gateway, 'is_connected' ) ) ) {
541 return wc_string_to_bool( $payment_gateway->is_connected() );
542 }
543 } catch ( Throwable $e ) {
544 // Do nothing but log so we can investigate.
545 SafeGlobalFunctionProxy::wc_get_logger()->debug(
546 'Failed to determine if gateway account is connected: ' . $e->getMessage(),
547 array(
548 'gateway' => $payment_gateway->id,
549 'source' => 'settings-payments',
550 'exception' => $e,
551 )
552 );
553 }
554
555 // Fall back to assuming that it is connected. This is the safest option.
556 return true;
557 }
558
559 /**
560 * Check if the payment gateway supports the current store state for onboarding.
561 *
562 * Most of the time the current business location should be the main factor, but could also
563 * consider other store settings like currency.
564 *
565 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
566 * @param string $country_code Optional. The country code for which to check.
567 * This should be an ISO 3166-1 alpha-2 country code.
568 *
569 * @return bool|null True if the payment gateway supports onboarding, false otherwise.
570 * If the payment gateway does not provide the information,
571 * we will return null to indicate that we don't know.
572 */
573 public function is_onboarding_supported( WC_Payment_Gateway $payment_gateway, string $country_code = '' ): ?bool {
574 try {
575 if ( method_exists( $payment_gateway, 'is_onboarding_supported' ) &&
576 is_callable( array( $payment_gateway, 'is_onboarding_supported' ) ) ) {
577
578 // Call with positional argument; normalize to bool|null.
579 $result = call_user_func( array( $payment_gateway, 'is_onboarding_supported' ), $country_code );
580 // Preserve null to indicate "unknown" state.
581 if ( is_null( $result ) ) {
582 return null;
583 }
584 if ( is_bool( $result ) ) {
585 return $result;
586 }
587 return filter_var( $result, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE );
588 }
589 } catch ( Throwable $e ) {
590 // Do nothing but log so we can investigate.
591 SafeGlobalFunctionProxy::wc_get_logger()->debug(
592 'Failed to determine if gateway supports onboarding: ' . $e->getMessage(),
593 array(
594 'gateway' => $payment_gateway->id,
595 'country' => $country_code,
596 'source' => 'settings-payments',
597 'exception' => $e,
598 )
599 );
600 }
601
602 // If we reach here, just assume that we don't know if the gateway supports onboarding.
603 return null;
604 }
605
606 /**
607 * Get the message to show when the payment gateway does not support onboarding.
608 *
609 * @see self::is_onboarding_supported()
610 *
611 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
612 * @param string $country_code Optional. The country code for which to check.
613 * This should be an ISO 3166-1 alpha-2 country code.
614 *
615 * @return string|null The message to show when the payment gateway does not support onboarding,
616 * or null if no specific message should be provided.
617 */
618 public function get_onboarding_not_supported_message( WC_Payment_Gateway $payment_gateway, string $country_code = '' ): ?string {
619 try {
620 if ( method_exists( $payment_gateway, 'get_onboarding_not_supported_message' ) &&
621 is_callable( array( $payment_gateway, 'get_onboarding_not_supported_message' ) ) ) {
622
623 $message = call_user_func( array( $payment_gateway, 'get_onboarding_not_supported_message' ), $country_code, );
624 if ( is_string( $message ) && ! empty( $message ) ) {
625 return sanitize_textarea_field( trim( $message ) );
626 }
627 }
628 } catch ( Throwable $e ) {
629 // Do nothing but log so we can investigate.
630 SafeGlobalFunctionProxy::wc_get_logger()->debug(
631 'Failed to determine the gateway onboarding not supported message: ' . $e->getMessage(),
632 array(
633 'gateway' => $payment_gateway->id,
634 'country' => $country_code,
635 'source' => 'settings-payments',
636 'exception' => $e,
637 )
638 );
639 }
640
641 // If we reach here, just assume that no specific message should be provided.
642 return null;
643 }
644
645 /**
646 * Check if the payment gateway has started the onboarding process.
647 *
648 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
649 *
650 * @return bool True if the payment gateway has started the onboarding process, false otherwise.
651 * If the payment gateway does not provide the information,
652 * it will infer it from having a connected account.
653 */
654 public function is_onboarding_started( WC_Payment_Gateway $payment_gateway ): bool {
655 try {
656 if ( method_exists( $payment_gateway, 'is_onboarding_started' ) && is_callable( array( $payment_gateway, 'is_onboarding_started' ) ) ) {
657 return wc_string_to_bool( $payment_gateway->is_onboarding_started() );
658 }
659 } catch ( Throwable $e ) {
660 // Do nothing but log so we can investigate.
661 SafeGlobalFunctionProxy::wc_get_logger()->debug(
662 'Failed to determine if gateway onboarding started: ' . $e->getMessage(),
663 array(
664 'gateway' => $payment_gateway->id,
665 'source' => 'settings-payments',
666 'exception' => $e,
667 )
668 );
669 }
670
671 // Fall back to inferring this from having a connected account.
672 return $this->is_account_connected( $payment_gateway );
673 }
674
675 /**
676 * Check if the payment gateway has completed the onboarding process.
677 *
678 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
679 *
680 * @return bool True if the payment gateway has completed the onboarding process, false otherwise.
681 * If the payment gateway does not provide the information,
682 * it will infer it from having a connected account.
683 */
684 public function is_onboarding_completed( WC_Payment_Gateway $payment_gateway ): bool {
685 // Sanity check: If the onboarding has not started, it cannot be completed.
686 if ( ! $this->is_onboarding_started( $payment_gateway ) ) {
687 return false;
688 }
689
690 try {
691 if ( method_exists( $payment_gateway, 'is_onboarding_completed' ) && is_callable( array( $payment_gateway, 'is_onboarding_completed' ) ) ) {
692 return wc_string_to_bool( $payment_gateway->is_onboarding_completed() );
693 }
694
695 // Note: This is what WooPayments provides, but it should become standard.
696 if ( method_exists( $payment_gateway, 'is_account_partially_onboarded' ) && is_callable( array( $payment_gateway, 'is_account_partially_onboarded' ) ) ) {
697 return ! wc_string_to_bool( $payment_gateway->is_account_partially_onboarded() );
698 }
699 } catch ( Throwable $e ) {
700 // Do nothing but log so we can investigate.
701 SafeGlobalFunctionProxy::wc_get_logger()->debug(
702 'Failed to determine if gateway onboarding is completed: ' . $e->getMessage(),
703 array(
704 'gateway' => $payment_gateway->id,
705 'source' => 'settings-payments',
706 'exception' => $e,
707 )
708 );
709 }
710
711 // Fall back to inferring this from having a connected account.
712 return $this->is_account_connected( $payment_gateway );
713 }
714
715 /**
716 * Try to determine if the payment gateway is in test mode onboarding (aka sandbox).
717 *
718 * This is a best-effort attempt, as there is no standard way to determine this.
719 * Trust the true value, but don't consider a false value as definitive.
720 *
721 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
722 *
723 * @return bool True if the payment gateway is in test mode onboarding, false otherwise.
724 */
725 public function is_in_test_mode_onboarding( WC_Payment_Gateway $payment_gateway ): bool {
726 try {
727 // Try various gateway methods to check if the payment gateway is in test mode onboarding.
728 if ( method_exists( $payment_gateway, 'is_test_mode_onboarding' ) && is_callable( array( $payment_gateway, 'is_test_mode_onboarding' ) ) ) {
729 return wc_string_to_bool( $payment_gateway->is_test_mode_onboarding() );
730 }
731 if ( method_exists( $payment_gateway, 'is_in_test_mode_onboarding' ) && is_callable( array( $payment_gateway, 'is_in_test_mode_onboarding' ) ) ) {
732 return wc_string_to_bool( $payment_gateway->is_in_test_mode_onboarding() );
733 }
734 } catch ( Throwable $e ) {
735 // Do nothing but log so we can investigate.
736 SafeGlobalFunctionProxy::wc_get_logger()->debug(
737 'Failed to determine if gateway is in test mode onboarding: ' . $e->getMessage(),
738 array(
739 'gateway' => $payment_gateway->id,
740 'source' => 'settings-payments',
741 'exception' => $e,
742 )
743 );
744 }
745
746 return false;
747 }
748
749 /**
750 * Get the settings URL for a payment gateway.
751 *
752 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
753 *
754 * @return string The settings URL for the payment gateway.
755 */
756 public function get_settings_url( WC_Payment_Gateway $payment_gateway ): string {
757 try {
758 if ( method_exists( $payment_gateway, 'get_settings_url' ) && is_callable( array( $payment_gateway, 'get_settings_url' ) ) ) {
759 $url = trim( (string) $payment_gateway->get_settings_url() );
760 if ( ! empty( $url ) && ! wc_is_valid_url( $url ) ) {
761 // Back-compat: normalize common relative admin URLs.
762 $url = ltrim( $url, '/' );
763 // Remove the '/wp-admin/' prefix if it exists.
764 if ( 0 === strpos( $url, 'wp-admin/' ) ) {
765 $url = substr( $url, strlen( 'wp-admin/' ) );
766 }
767 if ( 0 === strpos( $url, 'admin.php' ) || 0 === strpos( $url, '/admin.php' ) ) {
768 $url = admin_url( ltrim( $url, '/' ) );
769 }
770 }
771 if ( ! empty( $url ) && wc_is_valid_url( $url ) ) {
772 return add_query_arg(
773 array(
774 'from' => Payments::FROM_PAYMENTS_SETTINGS,
775 ),
776 $url
777 );
778 }
779 }
780 } catch ( Throwable $e ) {
781 // Do nothing but log so we can investigate.
782 SafeGlobalFunctionProxy::wc_get_logger()->debug(
783 'Failed to get gateway settings URL: ' . $e->getMessage(),
784 array(
785 'gateway' => $payment_gateway->id,
786 'source' => 'settings-payments',
787 'exception' => $e,
788 )
789 );
790 }
791
792 // If we couldn't get a valid settings URL from the gateway, fall back to a general gateway settings URL.
793 return Utils::wc_payments_settings_url(
794 null,
795 array(
796 'section' => strtolower( $payment_gateway->id ),
797 'from' => Payments::FROM_PAYMENTS_SETTINGS,
798 )
799 );
800 }
801
802 /**
803 * Get the onboarding URL for the payment gateway.
804 *
805 * This URL should start or continue the onboarding process.
806 *
807 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
808 * @param string $return_url Optional. The URL to return to after onboarding.
809 * This will likely get attached to the onboarding URL.
810 *
811 * @return string The onboarding URL for the payment gateway.
812 */
813 public function get_onboarding_url( WC_Payment_Gateway $payment_gateway, string $return_url = '' ): string {
814 try {
815 if ( method_exists( $payment_gateway, 'get_connection_url' ) && is_callable( array( $payment_gateway, 'get_connection_url' ) ) ) {
816 // If we received no return URL, we will set the WC Payments Settings page as the return URL.
817 $return_url = ! empty( $return_url ) ? $return_url : admin_url( 'admin.php?page=wc-settings&tab=checkout&from=' . Payments::FROM_PROVIDER_ONBOARDING );
818
819 return (string) $payment_gateway->get_connection_url( $return_url );
820 }
821 } catch ( Throwable $e ) {
822 // Do nothing but log so we can investigate.
823 SafeGlobalFunctionProxy::wc_get_logger()->debug(
824 'Failed to get gateway connection URL: ' . $e->getMessage(),
825 array(
826 'gateway' => $payment_gateway->id,
827 'source' => 'settings-payments',
828 'exception' => $e,
829 )
830 );
831 }
832
833 // Fall back to pointing users to the payment gateway settings page to handle onboarding.
834 return $this->get_settings_url( $payment_gateway );
835 }
836
837 /**
838 * Get the plugin details for a payment gateway.
839 *
840 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
841 *
842 * @return array The plugin details for the payment gateway.
843 */
844 public function get_plugin_details( WC_Payment_Gateway $payment_gateway ): array {
845 $entity_type = $this->get_containing_entity_type( $payment_gateway );
846
847 return array(
848 '_type' => $entity_type,
849 'slug' => $this->get_plugin_slug( $payment_gateway ),
850 // Only include the plugin file if the entity type is a regular plugin.
851 // We don't want to try to change the state of must-use plugins or themes.
852 'file' => PaymentsProviders::EXTENSION_TYPE_WPORG === $entity_type ? $this->get_plugin_file( $payment_gateway ) : '',
853 // The gateway's underlying plugin is obviously active (aka the code is running).
854 'status' => PaymentsProviders::EXTENSION_ACTIVE,
855 );
856 }
857
858 /**
859 * Get the source plugin slug of a payment gateway instance.
860 *
861 * It accounts for both regular and must-use plugins.
862 * If the gateway is registered through a theme, it will return the theme slug.
863 *
864 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
865 *
866 * @return string The plugin slug of the payment gateway.
867 * Empty string if a plugin slug could not be determined.
868 */
869 public function get_plugin_slug( WC_Payment_Gateway $payment_gateway ): string {
870 global $wp_theme_directories;
871
872 // If the payment gateway object has a `plugin_slug` property, use it.
873 // This is useful for testing.
874 if ( isset( $payment_gateway->plugin_slug ) ) {
875 return (string) $payment_gateway->plugin_slug;
876 }
877
878 $gateway_class_filename = $this->get_class_filename( $payment_gateway );
879 // Bail if we couldn't get the gateway class filename.
880 if ( ! is_string( $gateway_class_filename ) ) {
881 return '';
882 }
883
884 $entity_type = $this->get_containing_entity_type( $payment_gateway );
885 // Bail if we couldn't determine the entity type.
886 if ( PaymentsProviders::EXTENSION_TYPE_UNKNOWN === $entity_type ) {
887 return '';
888 }
889
890 if ( PaymentsProviders::EXTENSION_TYPE_THEME === $entity_type ) {
891 // Find the theme directory it is part of and extract the slug.
892 // This accounts for both parent and child themes.
893 if ( is_array( $wp_theme_directories ) ) {
894 foreach ( $wp_theme_directories as $dir ) {
895 if ( str_starts_with( $gateway_class_filename, $dir ) ) {
896 return $this->extract_slug_from_path( substr( $gateway_class_filename, strlen( $dir ) ) );
897 }
898 }
899 }
900
901 // Bail if we couldn't find a match.
902 return '';
903 }
904
905 // By this point, we know that the payment gateway is part of a plugin.
906 // Extract the relative path of the class file to the plugins directory.
907 // We account for both regular and must-use plugins.
908 $gateway_class_plugins_path = trim( plugin_basename( $gateway_class_filename ), DIRECTORY_SEPARATOR );
909
910 return $this->extract_slug_from_path( $gateway_class_plugins_path );
911 }
912
913 /**
914 * Get the corresponding plugin file of the payment gateway, without the .php extension.
915 *
916 * This is useful for using the WP API to change the state of the plugin (activate or deactivate).
917 * We remove the .php extension since the WP API expects plugin files without it.
918 *
919 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
920 * @param string $plugin_slug Optional. The payment gateway plugin slug to use directly.
921 *
922 * @return string The plugin file corresponding to the payment gateway plugin. Does not include the .php extension.
923 * In case of failures, it will return an empty string.
924 */
925 public function get_plugin_file( WC_Payment_Gateway $payment_gateway, string $plugin_slug = '' ): string {
926 // If the payment gateway object has a `plugin_file` property, use it.
927 // This is useful for testing.
928 if ( isset( $payment_gateway->plugin_file ) ) {
929 $plugin_file = $payment_gateway->plugin_file;
930 // Sanity check.
931 if ( ! is_string( $plugin_file ) ) {
932 return '';
933 }
934 // Remove the .php extension from the file path. The WP API expects it without it.
935 return Utils::trim_php_file_extension( $plugin_file );
936 }
937
938 if ( empty( $plugin_slug ) ) {
939 $plugin_slug = $this->get_plugin_slug( $payment_gateway );
940 }
941
942 // Bail if we couldn't determine the plugin slug.
943 if ( empty( $plugin_slug ) ) {
944 return '';
945 }
946
947 $plugin_file = PluginsHelper::get_plugin_path_from_slug( $plugin_slug );
948 // Bail if we couldn't determine the plugin file.
949 if ( ! is_string( $plugin_file ) || empty( $plugin_file ) ) {
950 return '';
951 }
952
953 // Remove the .php extension from the file path. The WP API expects it without it.
954 return Utils::trim_php_file_extension( $plugin_file );
955 }
956
957 /**
958 * Try and determine a list of recommended payment methods for a payment gateway.
959 *
960 * This data is not always available, and it is up to the payment gateway to provide it.
961 * This is not a definitive list of payment methods that the gateway supports.
962 * The data is aimed at helping the user understand what payment methods are recommended for the gateway
963 * and potentially help them make a decision on which payment methods to enable.
964 *
965 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
966 * @param string $country_code Optional. The country code for which to get recommended payment methods.
967 * This should be an ISO 3166-1 alpha-2 country code.
968 *
969 * @return array The recommended payment methods list for the payment gateway.
970 * Empty array if there are none.
971 */
972 public function get_recommended_payment_methods( WC_Payment_Gateway $payment_gateway, string $country_code = '' ): array {
973 // Bail if the payment gateway does not implement the method.
974 if ( ! method_exists( $payment_gateway, 'get_recommended_payment_methods' ) ||
975 ! is_callable( array( $payment_gateway, 'get_recommended_payment_methods' ) ) ) {
976
977 return array();
978 }
979
980 try {
981 // Get the "raw" recommended payment methods from the payment gateway.
982 $recommended_pms = call_user_func( array( $payment_gateway, 'get_recommended_payment_methods' ), $country_code );
983 if ( ! is_array( $recommended_pms ) ) {
984 // Bail if the recommended payment methods are not an array.
985 return array();
986 }
987 } catch ( Throwable $e ) {
988 // Log so we can investigate.
989 SafeGlobalFunctionProxy::wc_get_logger()->debug(
990 'Failed to get recommended payment methods: ' . $e->getMessage(),
991 array(
992 'gateway' => $payment_gateway->id,
993 'country' => $country_code,
994 'source' => 'settings-payments',
995 'exception' => $e,
996 )
997 );
998
999 return array();
1000 }
1001
1002 // Validate the received list items.
1003 $recommended_pms = array_filter(
1004 $recommended_pms,
1005 array( $this, 'validate_recommended_payment_method' )
1006 );
1007
1008 // Sort the list.
1009 $recommended_pms = $this->sort_recommended_payment_methods( $recommended_pms );
1010
1011 // Extract, standardize, and sanitize the details for each recommended payment method.
1012 $standardized_pms = array();
1013 foreach ( $recommended_pms as $index => $recommended_pm ) {
1014 // Use the index as the order since we sorted (and normalized) the list earlier.
1015 $standardized_pms[] = $this->standardize_recommended_payment_method( $recommended_pm, $index );
1016 }
1017
1018 return $standardized_pms;
1019 }
1020
1021 /**
1022 * Validate a recommended payment method entry.
1023 *
1024 * @param mixed $recommended_pm The recommended payment method entry to validate.
1025 *
1026 * @return bool True if the recommended payment method entry is valid, false otherwise.
1027 */
1028 protected function validate_recommended_payment_method( $recommended_pm ): bool {
1029 // We require at least `id` and `title`.
1030 return is_array( $recommended_pm ) &&
1031 ! empty( $recommended_pm['id'] ) &&
1032 ! empty( $recommended_pm['title'] );
1033 }
1034
1035 /**
1036 * Sort the recommended payment methods.
1037 *
1038 * @param array $recommended_pms The recommended payment methods list to sort.
1039 *
1040 * @return array The sorted recommended payment methods list.
1041 * List keys are not preserved.
1042 */
1043 protected function sort_recommended_payment_methods( array $recommended_pms ): array {
1044 // Sort the recommended payment methods by order/priority, if available.
1045 usort(
1046 $recommended_pms,
1047 function ( $a, $b ) {
1048 // `order` takes precedence over `priority`.
1049 // Entries that don't have the order/priority are placed at the end.
1050 return array( ( $a['order'] ?? PHP_INT_MAX ), ( $a['priority'] ?? PHP_INT_MAX ) ) <=> array( ( $b['order'] ?? PHP_INT_MAX ), ( $b['priority'] ?? PHP_INT_MAX ) );
1051 }
1052 );
1053
1054 return array_values( $recommended_pms );
1055 }
1056
1057 /**
1058 * Standardize a recommended payment method entry.
1059 *
1060 * @param array $recommended_pm The recommended payment method entry to standardize.
1061 * @param int $order Optional. The order of the recommended payment method.
1062 * Defaults to 0 if not provided.
1063 *
1064 * @return array The standardized recommended payment method entry.
1065 */
1066 protected function standardize_recommended_payment_method( array $recommended_pm, int $order = 0 ): array {
1067 $standard_details = array(
1068 'id' => sanitize_key( $recommended_pm['id'] ),
1069 '_order' => $order,
1070 // Default to enabled if not explicit.
1071 'enabled' => wc_string_to_bool( $recommended_pm['enabled'] ?? true ),
1072 // Default to not required if not explicit.
1073 'required' => wc_string_to_bool( $recommended_pm['required'] ?? false ),
1074 'title' => sanitize_text_field( $recommended_pm['title'] ),
1075 'description' => '',
1076 'icon' => '',
1077 'category' => self::PAYMENT_METHOD_CATEGORY_PRIMARY, // Default to primary.
1078 'notice' => array(
1079 'badge' => '',
1080 'message' => '',
1081 'link_text' => '',
1082 'link_url' => '',
1083 ),
1084 );
1085
1086 // If the payment method has a description, sanitize it before use.
1087 if ( ! empty( $recommended_pm['description'] ) ) {
1088 $standard_details['description'] = (string) $recommended_pm['description'];
1089 // Make sure that if we have HTML tags, we only allow stylistic tags and anchors.
1090 if ( preg_match( '/<[^>]+>/', $standard_details['description'] ) ) {
1091 // Only allow stylistic tags with a few modifications.
1092 $allowed_tags = wp_kses_allowed_html( 'data' );
1093 $allowed_tags = array_merge(
1094 $allowed_tags,
1095 array(
1096 'a' => array(
1097 'href' => true,
1098 'target' => true,
1099 ),
1100 )
1101 );
1102
1103 $standard_details['description'] = wp_kses( $standard_details['description'], $allowed_tags );
1104 }
1105 }
1106
1107 // If the payment method has an icon, try to use it.
1108 if ( ! empty( $recommended_pm['icon'] ) && wc_is_valid_url( $recommended_pm['icon'] ) ) {
1109 $standard_details['icon'] = sanitize_url( $recommended_pm['icon'] );
1110 }
1111
1112 // If the payment method has a category, use it if it's one of the known categories.
1113 if ( ! empty( $recommended_pm['category'] ) &&
1114 in_array( $recommended_pm['category'], array( self::PAYMENT_METHOD_CATEGORY_PRIMARY, self::PAYMENT_METHOD_CATEGORY_SECONDARY ), true ) ) {
1115 $standard_details['category'] = $recommended_pm['category'];
1116 }
1117
1118 // If the payment method has a notice, sanitize and use its fields.
1119 if ( ! empty( $recommended_pm['notice'] ) && is_array( $recommended_pm['notice'] ) ) {
1120 $notice = $recommended_pm['notice'];
1121
1122 if ( ! empty( $notice['badge'] ) ) {
1123 $standard_details['notice']['badge'] = sanitize_text_field( $notice['badge'] );
1124 }
1125
1126 if ( ! empty( $notice['message'] ) ) {
1127 $standard_details['notice']['message'] = sanitize_text_field( $notice['message'] );
1128 }
1129
1130 if ( ! empty( $notice['link_text'] ) ) {
1131 $standard_details['notice']['link_text'] = sanitize_text_field( $notice['link_text'] );
1132 }
1133
1134 if ( ! empty( $notice['link_url'] ) && is_string( $notice['link_url'] ) && wc_is_valid_url( $notice['link_url'] ) ) {
1135 $standard_details['notice']['link_url'] = sanitize_url( $notice['link_url'] );
1136 }
1137 }
1138
1139 return $standard_details;
1140 }
1141
1142 /**
1143 * Get the filename of the payment gateway class.
1144 *
1145 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
1146 *
1147 * @return string|null The filename of the payment gateway class or null if it cannot be determined.
1148 */
1149 private function get_class_filename( WC_Payment_Gateway $payment_gateway ): ?string {
1150 // If the payment gateway object has a `class_filename` property, use it.
1151 // It is only used in development environments (including when running tests).
1152 if ( isset( $payment_gateway->class_filename ) && in_array( wp_get_environment_type(), array( 'local', 'development' ), true ) ) {
1153 $class_filename = $payment_gateway->class_filename;
1154 } else {
1155 try {
1156 $reflector = new \ReflectionClass( get_class( $payment_gateway ) );
1157 $class_filename = $reflector->getFileName();
1158 } catch ( Throwable $e ) {
1159 // Bail but log so we can investigate.
1160 SafeGlobalFunctionProxy::wc_get_logger()->debug(
1161 'Failed to get gateway class filename: ' . $e->getMessage(),
1162 array(
1163 'gateway' => $payment_gateway->id,
1164 'source' => 'settings-payments',
1165 'exception' => $e,
1166 )
1167 );
1168 return null;
1169 }
1170 }
1171
1172 // Bail if we couldn't get the gateway class filename.
1173 if ( ! is_string( $class_filename ) ) {
1174 return null;
1175 }
1176
1177 return $class_filename;
1178 }
1179
1180 /**
1181 * Get the type of entity the payment gateway class is contained in.
1182 *
1183 * @param WC_Payment_Gateway $payment_gateway The payment gateway object.
1184 *
1185 * @return string The type of extension containing the payment gateway class.
1186 */
1187 private function get_containing_entity_type( WC_Payment_Gateway $payment_gateway ): string {
1188 global $wp_plugin_paths, $wp_theme_directories;
1189
1190 // If the payment gateway object has a `extension_type` property, use it.
1191 // This is useful for testing.
1192 if ( isset( $payment_gateway->extension_type ) ) {
1193 // Validate the extension type.
1194 if ( ! in_array(
1195 $payment_gateway->extension_type,
1196 array(
1197 PaymentsProviders::EXTENSION_TYPE_WPORG,
1198 PaymentsProviders::EXTENSION_TYPE_MU_PLUGIN,
1199 PaymentsProviders::EXTENSION_TYPE_THEME,
1200 ),
1201 true
1202 ) ) {
1203 return PaymentsProviders::EXTENSION_TYPE_UNKNOWN;
1204 }
1205
1206 return $payment_gateway->extension_type;
1207 }
1208
1209 $gateway_class_filename = $this->get_class_filename( $payment_gateway );
1210 // Bail if we couldn't get the gateway class filename.
1211 if ( ! is_string( $gateway_class_filename ) ) {
1212 return PaymentsProviders::EXTENSION_TYPE_UNKNOWN;
1213 }
1214
1215 // Plugin paths logic closely matches the one in plugin_basename().
1216 // $wp_plugin_paths contains normalized paths.
1217 $file = wp_normalize_path( $gateway_class_filename );
1218
1219 arsort( $wp_plugin_paths );
1220 // Account for symlinks in the plugin paths.
1221 foreach ( $wp_plugin_paths as $dir => $realdir ) {
1222 if ( str_starts_with( $file, $realdir ) ) {
1223 $gateway_class_filename = $dir . substr( $gateway_class_filename, strlen( $realdir ) );
1224 }
1225 }
1226
1227 // Test for regular plugins.
1228 if ( str_starts_with( $gateway_class_filename, wp_normalize_path( WP_PLUGIN_DIR ) ) ) {
1229 // For now, all plugins are considered WordPress.org plugins.
1230 return PaymentsProviders::EXTENSION_TYPE_WPORG;
1231 }
1232
1233 // Test for must-use plugins.
1234 if ( str_starts_with( $gateway_class_filename, wp_normalize_path( WPMU_PLUGIN_DIR ) ) ) {
1235 return PaymentsProviders::EXTENSION_TYPE_MU_PLUGIN;
1236 }
1237
1238 // Check if it is part of a theme.
1239 if ( is_array( $wp_theme_directories ) ) {
1240 foreach ( $wp_theme_directories as $dir ) {
1241 // Check if the class file is in a theme directory.
1242 if ( str_starts_with( $gateway_class_filename, $dir ) ) {
1243 return PaymentsProviders::EXTENSION_TYPE_THEME;
1244 }
1245 }
1246 }
1247
1248 // Default to an unknown type.
1249 return PaymentsProviders::EXTENSION_TYPE_UNKNOWN;
1250 }
1251
1252 /**
1253 * Extract the slug from a given path.
1254 *
1255 * It can be a directory or file path.
1256 * This should be a relative path since the top-level directory or file name will be used as the slug.
1257 *
1258 * @param string $path The path to extract the slug from.
1259 *
1260 * @return string The slug extracted from the path.
1261 */
1262 private function extract_slug_from_path( string $path ): string {
1263 $path = trim( $path );
1264 $path = trim( $path, DIRECTORY_SEPARATOR );
1265
1266 // If the path is just a file name, use it as the slug.
1267 if ( false === strpos( $path, DIRECTORY_SEPARATOR ) ) {
1268 return Utils::trim_php_file_extension( $path );
1269 }
1270
1271 $parts = explode( DIRECTORY_SEPARATOR, $path );
1272 // Bail if we couldn't get the parts.
1273 if ( ! is_array( $parts ) ) {
1274 return '';
1275 }
1276
1277 return reset( $parts );
1278 }
1279 }
1280