| @@ -4,9 +4,9 @@ | ||
| 4 | 4 | * |
| 5 | 5 | * @version 1.5.0 |
| 6 | 6 | * @package Charitable/Classes/Charitable_Emails |
| 7 | 7 | * @author David Bisset |
| 8 | - * @copyright Copyright (c) 2022, WP Charitable LLC | |
| 8 | + * @copyright Copyright (c) 2023, WP Charitable LLC | |
| 9 | 9 | * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License |
| 10 | 10 | */ |
| 11 | 11 | |
| 12 | 12 | // Exit if accessed directly. |
| @@ -37,8 +37,16 @@ | ||
| 37 | 37 | */ |
| 38 | 38 | private $emails; |
| 39 | 39 | |
| 40 | 40 | /** |
| 41 | + * Registration flag to prevent multiple calls. | |
| 42 | + * | |
| 43 | + * @since 1.8.9.2 | |
| 44 | + * @var bool | |
| 45 | + */ | |
| 46 | + private $emails_registered = false; | |
| 47 | + | |
| 48 | + /** | |
| 41 | 49 | * Set up the class. |
| 42 | 50 | * |
| 43 | 51 | * Note that the only way to instantiate an object is with the charitable_start method, |
| 44 | 52 | * which can only be called during the start phase. In other words, don't try |
| @@ -46,13 +54,73 @@ | ||
| 46 | 54 | * |
| 47 | 55 | * @since 1.0.0 |
| 48 | 56 | */ |
| 49 | 57 | private function __construct() { |
| 58 | + // Initialize state | |
| 59 | + $this->emails_registered = false; | |
| 60 | + $this->emails = null; | |
| 61 | + | |
| 50 | 62 | /* 3rd party hook for overriding anything we've done so far. */ |
| 51 | 63 | do_action( 'charitable_emails_start', $this ); |
| 64 | + | |
| 65 | + add_action( 'init', array( $this, 'register_email_hooks' ) ); | |
| 52 | 66 | } |
| 53 | 67 | |
| 54 | 68 | /** |
| 69 | + * Register email hooks for donation status changes. | |
| 70 | + * | |
| 71 | + * @since 1.0.0 | |
| 72 | + * | |
| 73 | + * @return void | |
| 74 | + */ | |
| 75 | + public function register_email_hooks() { | |
| 76 | + foreach ( charitable_get_approval_statuses() as $approval_status ) { | |
| 77 | + add_action( $approval_status . '_' . Charitable::DONATION_POST_TYPE, array( 'Charitable_Email_Donation_Receipt', 'send_with_donation_id' ) ); | |
| 78 | + add_action( $approval_status . '_' . Charitable::DONATION_POST_TYPE, array( 'Charitable_Email_New_Donation', 'send_with_donation_id' ) ); | |
| 79 | + } | |
| 80 | + } | |
| 81 | + | |
| 82 | + /** | |
| 83 | + * Ensure emails are registered before accessing them. | |
| 84 | + * Implements lazy loading to handle timing race conditions. | |
| 85 | + * | |
| 86 | + * @since 1.8.9.2 | |
| 87 | + * @return bool True if emails are available, false on failure. | |
| 88 | + */ | |
| 89 | + private function ensure_emails_registered() { | |
| 90 | + // Quick check - if emails exist, we're done | |
| 91 | + if ( is_array( $this->emails ) && ! empty( $this->emails ) ) { | |
| 92 | + return true; | |
| 93 | + } | |
| 94 | + | |
| 95 | + // Multiple detection methods for maximum compatibility | |
| 96 | + if ( did_action( 'init' ) || doing_action( 'init' ) || wp_doing_ajax() || is_admin() ) { | |
| 97 | + return $this->safe_register_emails(); | |
| 98 | + } | |
| 99 | + | |
| 100 | + // Fallback registration with error handling | |
| 101 | + return $this->safe_register_emails(); | |
| 102 | + } | |
| 103 | + | |
| 104 | + /** | |
| 105 | + * Safely register emails with error handling and guard. | |
| 106 | + * | |
| 107 | + * @since 1.8.9.2 | |
| 108 | + * @return bool True if registration successful, false otherwise. | |
| 109 | + */ | |
| 110 | + private function safe_register_emails() { | |
| 111 | + try { | |
| 112 | + $this->register_emails(); | |
| 113 | + return is_array( $this->emails ) && ! empty( $this->emails ); | |
| 114 | + } catch ( Exception $e ) { | |
| 115 | + if ( charitable_is_debug() ) { | |
| 116 | + error_log( 'Charitable: Email registration failed - ' . $e->getMessage() ); | |
| 117 | + } | |
| 118 | + return false; | |
| 119 | + } | |
| 120 | + } | |
| 121 | + | |
| 122 | + /** | |
| 55 | 123 | * Returns and/or create the single instance of this class. |
| 56 | 124 | * |
| 57 | 125 | * @since 1.2.0 |
| 58 | 126 | * |
| @@ -73,18 +141,28 @@ | ||
| 73 | 141 | * |
| 74 | 142 | * @return string[] |
| 75 | 143 | */ |
| 76 | 144 | public function register_emails() { |
| 77 | - $this->emails = apply_filters( 'charitable_emails', array( | |
| 78 | - 'donation_receipt' => 'Charitable_Email_Donation_Receipt', | |
| 79 | - 'offline_donation_receipt' => 'Charitable_Email_Offline_Donation_Receipt', | |
| 80 | - 'new_donation' => 'Charitable_Email_New_Donation', | |
| 81 | - 'campaign_end' => 'Charitable_Email_Campaign_End', | |
| 82 | - 'offline_donation_notification' => 'Charitable_Email_Offline_Donation_Notification', | |
| 83 | - 'password_reset' => 'Charitable_Email_Password_Reset', | |
| 84 | - 'email_verification' => 'Charitable_Email_Email_Verification', | |
| 85 | - ) ); | |
| 145 | + // Guard against multiple registrations | |
| 146 | + if ( $this->emails_registered && is_array( $this->emails ) ) { | |
| 147 | + return $this->emails; | |
| 148 | + } | |
| 86 | 149 | |
| 150 | + $this->emails_registered = true; | |
| 151 | + | |
| 152 | + $this->emails = apply_filters( | |
| 153 | + 'charitable_emails', | |
| 154 | + array( | |
| 155 | + 'donation_receipt' => 'Charitable_Email_Donation_Receipt', | |
| 156 | + 'offline_donation_receipt' => 'Charitable_Email_Offline_Donation_Receipt', | |
| 157 | + 'new_donation' => 'Charitable_Email_New_Donation', | |
| 158 | + 'campaign_end' => 'Charitable_Email_Campaign_End', | |
| 159 | + 'offline_donation_notification' => 'Charitable_Email_Offline_Donation_Notification', | |
| 160 | + 'password_reset' => 'Charitable_Email_Password_Reset', | |
| 161 | + 'email_verification' => 'Charitable_Email_Email_Verification', | |
| 162 | + ) | |
| 163 | + ); | |
| 164 | + | |
| 87 | 165 | return $this->emails; |
| 88 | 166 | } |
| 89 | 167 | |
| 90 | 168 | /** |
| @@ -103,27 +181,34 @@ | ||
| 103 | 181 | * @since 1.5.0 |
| 104 | 182 | * |
| 105 | 183 | * @param string[] $emails The list of emails and their label. |
| 106 | 184 | */ |
| 107 | - $emails = apply_filters( 'charitable_resendable_donation_emails', array( | |
| 108 | - 'donation_receipt', | |
| 109 | - 'new_donation', | |
| 110 | - 'offline_donation_receipt', | |
| 111 | - 'offline_donation_notification', | |
| 112 | - ) ); | |
| 185 | + $emails = apply_filters( | |
| 186 | + 'charitable_resendable_donation_emails', | |
| 187 | + array( | |
| 188 | + 'donation_receipt', | |
| 189 | + 'new_donation', | |
| 190 | + 'offline_donation_receipt', | |
| 191 | + 'offline_donation_notification', | |
| 192 | + ) | |
| 193 | + ); | |
| 113 | 194 | |
| 114 | 195 | foreach ( $emails as $email ) { |
| 115 | 196 | $class = $this->get_email( $email ); |
| 116 | - $object = new $class; | |
| 197 | + $object = new $class(); | |
| 117 | 198 | |
| 118 | - $donation_actions->register( 'resend_' . $email, array( | |
| 119 | - 'label' => $object->get_name(), | |
| 120 | - 'callback' => array( $this, 'resend_email' ), | |
| 121 | - 'button_text' => __( 'Resend Email', 'charitable' ), | |
| 122 | - 'active_callback' => array( $class, 'can_be_resent' ), | |
| 123 | - 'success_message' => 11, | |
| 124 | - 'failed_message' => 12, | |
| 125 | - ), __( 'Resend Donation Emails', 'charitable' ) ); | |
| 199 | + $donation_actions->register( | |
| 200 | + 'resend_' . $email, | |
| 201 | + array( | |
| 202 | + 'label' => $object->get_name(), | |
| 203 | + 'callback' => array( $this, 'resend_email' ), | |
| 204 | + 'button_text' => __( 'Resend Email', 'charitable' ), | |
| 205 | + 'active_callback' => array( $class, 'can_be_resent' ), | |
| 206 | + 'success_message' => 11, | |
| 207 | + 'failed_message' => 12, | |
| 208 | + ), | |
| 209 | + __( 'Resend Donation Emails', 'charitable' ) | |
| 210 | + ); | |
| 126 | 211 | } |
| 127 | 212 | } |
| 128 | 213 | |
| 129 | 214 | /** |
| @@ -141,9 +226,9 @@ | ||
| 141 | 226 | */ |
| 142 | 227 | public function resend_email( $success, $object_id, $args, $action ) { |
| 143 | 228 | $email = str_replace( 'resend_', '', $action ); |
| 144 | 229 | $class = $this->get_email( $email ); |
| 145 | - $object = new $class; | |
| 230 | + $object = new $class(); | |
| 146 | 231 | |
| 147 | 232 | return $object->resend( $object_id, $args ); |
| 148 | 233 | } |
| 149 | 234 | |
| @@ -154,22 +239,27 @@ | ||
| 154 | 239 | * |
| 155 | 240 | * @return void |
| 156 | 241 | */ |
| 157 | 242 | public function handle_email_settings_request() { |
| 158 | - if ( ! wp_verify_nonce( $_REQUEST['_nonce'], 'email' ) ) { | |
| 159 | - wp_die( __( 'Cheatin\' eh?!', 'charitable' ) ); | |
| 243 | + if ( ! wp_verify_nonce( $_REQUEST['_nonce'], 'email' ) ) { // phpcs:ignore | |
| 244 | + wp_die( __( 'Cheatin\' eh?!', 'charitable' ) ); // phpcs:ignore | |
| 160 | 245 | } |
| 161 | 246 | |
| 162 | - $email = isset( $_REQUEST['email_id'] ) ? $_REQUEST['email_id'] : false; | |
| 247 | + $email = isset( $_REQUEST['email_id'] ) ? $_REQUEST['email_id'] : false; // phpcs:ignore | |
| 163 | 248 | |
| 164 | 249 | /* Gateway must be set */ |
| 165 | 250 | if ( false === $email ) { |
| 166 | - wp_die( __( 'Missing email.', 'charitable' ) ); | |
| 251 | + wp_die( __( 'Missing email.', 'charitable' ) ); // phpcs:ignore | |
| 167 | 252 | } |
| 168 | 253 | |
| 254 | + /* Ensure emails are registered. */ | |
| 255 | + if ( ! $this->ensure_emails_registered() ) { | |
| 256 | + wp_die( __( 'Email system not available.', 'charitable' ) ); | |
| 257 | + } | |
| 258 | + | |
| 169 | 259 | /* Validate email. */ |
| 170 | 260 | if ( ! isset( $this->emails[ $email ] ) ) { |
| 171 | - wp_die( __( 'Invalid email.', 'charitable' ) ); | |
| 261 | + wp_die( __( 'Invalid email.', 'charitable' ) ); // phpcs:ignore | |
| 172 | 262 | } |
| 173 | 263 | |
| 174 | 264 | /* All good, so disable or enable the email */ |
| 175 | 265 | if ( 'charitable_disable_email' == current_filter() ) { |
| @@ -186,8 +276,11 @@ | ||
| 186 | 276 | * |
| 187 | 277 | * @return string |
| 188 | 278 | */ |
| 189 | 279 | public function get_available_emails() { |
| 280 | + if ( ! $this->ensure_emails_registered() ) { | |
| 281 | + return null; // Changed from array() for better error detection | |
| 282 | + } | |
| 190 | 283 | return $this->emails; |
| 191 | 284 | } |
| 192 | 285 | |
| 193 | 286 | /** |
| @@ -221,9 +314,9 @@ | ||
| 221 | 314 | if ( ! class_exists( $class ) ) { |
| 222 | 315 | continue; |
| 223 | 316 | } |
| 224 | 317 | |
| 225 | - $email = new $class; | |
| 318 | + $email = new $class(); | |
| 226 | 319 | $emails[ $email->get_email_id() ] = $email->get_name(); |
| 227 | 320 | } |
| 228 | 321 | |
| 229 | 322 | return $emails; |
| @@ -238,8 +331,11 @@ | ||
| 238 | 331 | * @return string|false A string representing the email class name if the |
| 239 | 332 | * email is registered; false if it is not registered. |
| 240 | 333 | */ |
| 241 | 334 | public function get_email( $email ) { |
| 335 | + if ( ! $this->ensure_emails_registered() ) { | |
| 336 | + return false; | |
| 337 | + } | |
| 242 | 338 | return isset( $this->emails[ $email ] ) ? $this->emails[ $email ] : false; |
| 243 | 339 | } |
| 244 | 340 | |
| 245 | 341 | /** |
| @@ -257,14 +353,18 @@ | ||
| 257 | 353 | /** |
| 258 | 354 | * Enable an email. |
| 259 | 355 | * |
| 260 | 356 | * @since 1.0.0 |
| 357 | + * @version 1.8.2 - Added array check. | |
| 261 | 358 | * |
| 262 | 359 | * @param string $email The email to be enabled. |
| 263 | 360 | * @return void |
| 264 | 361 | */ |
| 265 | 362 | protected function enable_email( $email ) { |
| 266 | - $settings = get_option( 'charitable_settings' ); | |
| 363 | + $settings = get_option( 'charitable_settings', array() ); | |
| 364 | + if ( ! is_array( $settings ) ) { | |
| 365 | + $settings = array(); | |
| 366 | + } | |
| 267 | 367 | $enabled_emails = isset( $settings['enabled_emails'] ) ? $settings['enabled_emails'] : array(); |
| 268 | 368 | $enabled_emails[ $email ] = $this->emails[ $email ]; |
| 269 | 369 | $settings['enabled_emails'] = $enabled_emails; |
| 270 | 370 | |