PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 1.7.0.9 All 211 releases
charitable / includes / square / admin / class-charitable-square-webhookshealthcheck.php

class-charitable-square-webhookshealthcheck.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.13, at includes/square/admin/class-charitable-square-webhookshealthcheck.php

314 lines 8.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * The class responsible for monitoring Square webhooks health.
4 *
5 * @package Charitable Square/Classes/Charitable_Square_WebhooksHealthCheck
6 * @author David Bisset
7 * @copyright Copyright (c) 2021-2022, WP Charitable LLC
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.1.0
10 * @version 1.3.0
11 */
12
13 // Exit if accessed directly.
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 if ( ! class_exists( 'Charitable_Square_WebhooksHealthCheck' ) ) :
19
20 /**
21 * Webhooks Health Check class.
22 *
23 * @since 1.8.7
24 */
25 class Charitable_Square_WebhooksHealthCheck {
26
27 /**
28 * Endpoint status option name.
29 *
30 * @since 1.8.7
31 */
32 public const ENDPOINT_OPTION = 'charitable_square_webhooks_endpoint_status';
33
34 /**
35 * Webhook health check cron hook.
36 *
37 * @since 1.8.7
38 */
39 private const CRON_HOOK = 'charitable_square_webhooks_health_check';
40
41 /**
42 * Status OK key.
43 *
44 * @since 1.8.7
45 */
46 public const STATUS_OK = 'ok';
47
48 /**
49 * Status error key.
50 *
51 * @since 1.8.7
52 */
53 private const STATUS_ERROR = 'error';
54
55 /**
56 * Admin notice ID.
57 *
58 * @since 1.8.7
59 */
60 private const NOTICE_ID = 'charitable_square_webhooks_site_health';
61
62 /**
63 * Missing signing secret notice ID.
64 *
65 * @since 1.8.12
66 */
67 private const MISSING_SECRET_NOTICE_ID = 'charitable_square_webhooks_missing_secret';
68
69 /**
70 * Initialization.
71 *
72 * @since 1.8.7
73 */
74 public function init() {
75 $this->hooks();
76 return $this;
77 }
78
79 /**
80 * Register hooks.
81 *
82 * @since 1.8.7
83 */
84 private function hooks() {
85 add_action( 'admin_notices', array( $this, 'admin_notice' ) );
86 add_action( self::CRON_HOOK, array( $this, 'process_webhooks_status_check' ) );
87 add_action( 'init', array( $this, 'maybe_schedule_check' ) );
88
89 // Hook into the charitable_uninstall action for cleanup.
90 add_action( 'charitable_uninstall', array( $this, 'uninstall' ) );
91 }
92
93 /**
94 * Schedule webhook health check using WP Cron.
95 *
96 * @since 1.8.7
97 */
98 public function maybe_schedule_check() {
99 // Skip if webhook health check should be disabled.
100 if ( apply_filters( 'charitable_square_webhooks_health_check_disable', false ) ) {
101 return;
102 }
103
104 // Skip if not configured or already scheduled.
105 if ( ! Charitable_Square_Helpers::is_webhook_enabled() || wp_next_scheduled( self::CRON_HOOK ) ) {
106 return;
107 }
108
109 // Schedule health check to run hourly.
110 wp_schedule_event( time(), 'hourly', self::CRON_HOOK );
111 }
112
113 /**
114 * Clean up scheduled events and options on uninstall.
115 *
116 * @since 1.8.7
117 */
118 public function uninstall() {
119 // Clear cron event.
120 $timestamp = wp_next_scheduled( self::CRON_HOOK );
121 if ( $timestamp ) {
122 wp_unschedule_event( $timestamp, self::CRON_HOOK );
123 }
124
125 // Clean up options.
126 delete_option( self::ENDPOINT_OPTION );
127 }
128
129 /**
130 * Process webhook health check status.
131 *
132 * @since 1.8.7
133 */
134 public function process_webhooks_status_check() {
135 // Skip if webhooks are not enabled.
136 if ( ! Charitable_Square_Helpers::is_webhook_enabled() ) {
137 return;
138 }
139
140 $last_payment = $this->get_last_square_payment();
141
142 // If no Square payments found, remove status option to avoid false positives.
143 if ( empty( $last_payment ) ) {
144 delete_option( self::ENDPOINT_OPTION );
145 return;
146 }
147
148 // If payment is processed but webhook didn't update status within 15 minutes,
149 // we likely have a webhook configuration issue.
150 if (
151 isset( $last_payment['status'] ) &&
152 $last_payment['status'] === 'processed' &&
153 time() > ( strtotime( $last_payment['date_created_gmt'] ) + ( 15 * MINUTE_IN_SECONDS ) )
154 ) {
155 Charitable_Square_Helpers::reset_webhook_configuration();
156 self::save_status( self::STATUS_ERROR );
157 return;
158 }
159
160 // Everything looks good.
161 self::save_status( self::STATUS_OK );
162 }
163
164 /**
165 * Get the most recent Square payment.
166 *
167 * @since 1.8.7
168 *
169 * @return array Payment data or empty array if none found.
170 */
171 private function get_last_square_payment() {
172 global $wpdb;
173
174 // Find the last 'donation' custom post type with a meta_key of 'gateway' set to 'square'.
175 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQLPlaceholders.UnnecessaryPrepare
176 $payments = $wpdb->get_results(
177 "SELECT * FROM {$wpdb->posts} p
178 INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
179 WHERE p.post_type = 'donation'
180 AND p.post_status = 'charitable-completed'
181 AND pm.meta_key = 'donation_gateway'
182 AND pm.meta_value = 'square'
183 ORDER BY ID DESC
184 LIMIT 1",
185 ARRAY_A
186 );
187
188 return $payments ? $payments[0] : array();
189 }
190
191 /**
192 * Display admin notice about webhook issues.
193 *
194 * @since 1.8.7
195 */
196 public function admin_notice() {
197 // Only show notice if Square is configured.
198 if ( ! Charitable_Square_Helpers::is_square_configured() ) {
199 return;
200 }
201
202 // Only show if webhooks are enabled but not properly configured.
203 if (
204 ! Charitable_Square_Helpers::is_webhook_enabled() ||
205 Charitable_Square_Helpers::is_webhook_configured()
206 ) {
207 return;
208 }
209
210 // Only show if we've detected a webhook error.
211 if ( get_option( self::ENDPOINT_OPTION, self::STATUS_OK ) === self::STATUS_OK ) {
212 return;
213 }
214
215 // Only show if there are Square payments.
216 if ( empty( $this->get_last_square_payment() ) ) {
217 return;
218 }
219
220 // Display the admin notice.
221 $notice = sprintf(
222 wp_kses(
223 /* translators: %1$s: URL to the webhooks documentation */
224 __( 'Looks like you have a problem with your webhooks configuration. Please check and confirm that you\'ve configured the WPCharitable webhooks in your Square account. This notice will disappear automatically when a new Square request comes in. See our <a href="%1$s" rel="nofollow noopener" target="_blank">documentation</a> for more information.', 'charitable' ),
225 array(
226 'a' => array(
227 'href' => array(),
228 'target' => array(),
229 'rel' => array(),
230 ),
231 )
232 ),
233 esc_url( charitable_utm_link( 'https://wpcharitable.com/docs/setting-up-square-webhooks/', 'Admin', 'Square Webhooks not active' ) )
234 );
235
236 // Use WordPress admin notices.
237 ?>
238 <div class="notice notice-error is-dismissible" data-notice-id="<?php echo esc_attr( self::NOTICE_ID ); ?>">
239 <p><?php echo wp_kses_post( $notice ); ?></p>
240 </div>
241 <?php
242 }
243
244 /**
245 * Warn when Square is connected but no webhook signing secret is stored.
246 *
247 * Since 1.8.12 the webhook processor fails closed when it has no signing
248 * secret to verify against, so an affected site quietly stops reconciling
249 * Square donations and refunds. The hourly health check cannot catch this:
250 * it keys off the most recent *completed* Square donation, which is the
251 * very thing a refused webhook prevents. Read the stored setting directly
252 * instead so the warning does not depend on donation history or the cron.
253 *
254 * @since 1.8.12
255 */
256 public function missing_signing_secret_notice() {
257
258 if ( ! current_user_can( 'manage_options' ) ) {
259 return;
260 }
261
262 // Only warn sites that actually have a valid Square connection.
263 if ( ! Charitable_Square_Helpers::is_square_configured() ) {
264 return;
265 }
266
267 $mode = Charitable_Square_Helpers::get_mode();
268 $settings = charitable_get_option( 'gateways_square' );
269
270 // A secret is stored for the active mode, so verification works.
271 if ( is_array( $settings ) && ! empty( $settings[ $mode ]['webhooks-secret'] ) ) {
272 return;
273 }
274
275 $notice = sprintf(
276 wp_kses(
277 /* translators: %1$s: URL to the Square settings screen, %2$s: URL to the webhooks documentation */
278 __( 'Charitable cannot verify incoming Square webhooks because no webhook signing secret is stored for the current mode. Unverified webhooks are rejected, so Square donations and refunds will not update automatically. Add your signing secret on the <a href="%1$s">Square settings screen</a>. See our <a href="%2$s" rel="nofollow noopener" target="_blank">documentation</a> for the steps.', 'charitable' ),
279 array(
280 'a' => array(
281 'href' => array(),
282 'target' => array(),
283 'rel' => array(),
284 ),
285 )
286 ),
287 esc_url( admin_url( 'admin.php?page=charitable-settings&tab=gateways&group=gateways_square_core' ) ),
288 esc_url( charitable_utm_link( 'https://wpcharitable.com/docs/setting-up-square-webhooks/', 'Admin', 'Square Webhook Signing Secret Missing' ) )
289 );
290
291 ?>
292 <div class="notice notice-error is-dismissible" data-notice-id="<?php echo esc_attr( self::MISSING_SECRET_NOTICE_ID ); ?>">
293 <p><?php echo wp_kses_post( $notice ); ?></p>
294 </div>
295 <?php
296 }
297
298 /**
299 * Save webhooks status.
300 *
301 * @since 1.8.7
302 *
303 * @param string $value Status value.
304 */
305 public static function save_status( string $value ) {
306 if ( ! in_array( $value, array( self::STATUS_OK, self::STATUS_ERROR ), true ) ) {
307 return;
308 }
309
310 update_option( self::ENDPOINT_OPTION, $value );
311 }
312 }
313
314 endif;