*/ private static $removed = array(); /** * Deferred counting state before start(). * * @var array{terms: bool, comments: bool} */ private static $deferred = array( 'terms' => false, 'comments' => false, ); /** * Current kind and batch (for the quiet_end action). * * @var array{0: string, 1: string} */ private static $context = array( '', '' ); /** * Whether webhooks may still be delivered during this step (never on production). * * @var bool */ private static $webhooks = false; /** * `woocommerce_email_enabled_{id}` hooks added in this step. * * @var string[] */ private static $email_hooks = array(); /** * Email callbacks the last start() unhooked. * * @var int */ private static $unhooked = 0; /** * How many WooCommerce email callbacks the last start() unhooked. * * @since 1.0.0 * @return int */ public static function unhooked(): int { return self::$unhooked; } /** * Whether a quiet step is running in this request. * * @since 1.0.0 * @return bool */ public static function active(): bool { return self::$depth > 0; } /** * Start a quiet step. * * @since 1.0.0 * @param string $kind `generate` or `cleanup`. * @param string $batch Batch ID (the first one, for clean-ups of several). * @param bool $production Whether this site counts as production. * @return void */ public static function start( string $kind, string $batch, bool $production ): void { ++self::$depth; if ( self::$depth > 1 ) { return; } self::$context = array( $kind, $batch ); add_filter( 'pre_wp_mail', array( __CLASS__, 'no_mail' ), PHP_INT_MAX ); self::unhook_emails(); self::$unhooked = count( self::$removed ); add_filter( 'woocommerce_allow_send_queued_transactional_email', array( __CLASS__, 'off' ), PHP_INT_MAX ); // Every email off too. The mailer isn't built for this: its emails are read only when it // already exists, else when it's built (never calling WC()->mailer() in `woocommerce_email`). if ( did_action( 'woocommerce_email' ) && function_exists( 'WC' ) ) { self::disable_emails( WC()->mailer() ); } add_action( 'woocommerce_email', array( __CLASS__, 'disable_emails' ), PHP_INT_MAX ); add_filter( 'woocommerce_apply_tracking', array( __CLASS__, 'off' ), PHP_INT_MAX ); /** * Filter whether WooCommerce webhooks may still be delivered while test data is generated * or cleaned up. Ignored on production, where they never are. * * @since 1.0.0 * @param bool $deliver Deliver (default false). */ self::$webhooks = ! $production && true === apply_filters( 'apwc_test_data_deliver_webhooks', false ); add_filter( 'woocommerce_webhook_should_deliver', array( __CLASS__, 'no_webhook' ), PHP_INT_MAX ); add_filter( 'woocommerce_can_reduce_order_stock', array( __CLASS__, 'no_stock_change' ), PHP_INT_MAX, 2 ); add_filter( 'woocommerce_can_restore_order_stock', array( __CLASS__, 'no_stock_change' ), PHP_INT_MAX, 2 ); add_filter( 'woocommerce_prevent_adjust_line_item_product_stock', array( __CLASS__, 'prevent_item_stock' ), PHP_INT_MAX, 2 ); self::$deferred = array( 'terms' => (bool) wp_defer_term_counting(), 'comments' => (bool) wp_defer_comment_counting(), ); wp_defer_term_counting( true ); wp_defer_comment_counting( true ); /** * Fires when a test data step starts; other code can pause itself until * `apwc_test_data_quiet_end`. TestData\Guard::active() tells the same. * * @since 1.0.0 * @param string $kind `generate` or `cleanup`. * @param string $batch Batch ID. */ do_action( 'apwc_test_data_quiet_start', $kind, $batch ); } /** * End a quiet step and put everything back. Safe to call more often than start(). * * @since 1.0.0 * @return void */ public static function end(): void { if ( self::$depth <= 0 ) { self::$depth = 0; return; } --self::$depth; if ( self::$depth > 0 ) { return; } // Flush deferred counts while emails are still off (a count change never mails, but stay // safe). A count callback that throws must not leave the rest deferred or filtered. try { if ( ! self::$deferred['terms'] ) { wp_defer_term_counting( false ); } } finally { try { if ( ! self::$deferred['comments'] ) { wp_defer_comment_counting( false ); } } finally { self::restore(); } } } /** * Remove everything start() added and fire `apwc_test_data_quiet_end` (end(), always). * * @return void */ private static function restore(): void { remove_filter( 'woocommerce_webhook_should_deliver', array( __CLASS__, 'no_webhook' ), PHP_INT_MAX ); remove_filter( 'woocommerce_allow_send_queued_transactional_email', array( __CLASS__, 'off' ), PHP_INT_MAX ); remove_action( 'woocommerce_email', array( __CLASS__, 'disable_emails' ), PHP_INT_MAX ); remove_filter( 'woocommerce_apply_tracking', array( __CLASS__, 'off' ), PHP_INT_MAX ); foreach ( self::$email_hooks as $hook ) { remove_filter( $hook, array( __CLASS__, 'off' ), PHP_INT_MAX ); } self::$email_hooks = array(); remove_filter( 'woocommerce_can_reduce_order_stock', array( __CLASS__, 'no_stock_change' ), PHP_INT_MAX ); remove_filter( 'woocommerce_can_restore_order_stock', array( __CLASS__, 'no_stock_change' ), PHP_INT_MAX ); remove_filter( 'woocommerce_prevent_adjust_line_item_product_stock', array( __CLASS__, 'prevent_item_stock' ), PHP_INT_MAX ); // Re-added at the end of their priority: their order among same-priority callbacks changes // for the rest of this request, which ends right after the step. foreach ( self::$removed as $entry ) { add_action( $entry[0], $entry[1], $entry[2], $entry[3] ); } self::$removed = array(); remove_filter( 'pre_wp_mail', array( __CLASS__, 'no_mail' ), PHP_INT_MAX ); list( $kind, $batch ) = self::$context; self::$context = array( '', '' ); /** * Fires when a test data step ended (also after one that failed). * * @since 1.0.0 * @param string $kind `generate` or `cleanup`. * @param string $batch Batch ID. */ do_action( 'apwc_test_data_quiet_end', $kind, $batch ); } /** * `pre_wp_mail`: no email leaves this request. * * @since 1.0.0 * @return bool */ public static function no_mail() { return false; } /** * Returns false: the Guard's own callback for the filters it switches off (never WordPress's * shared `__return_false`, so removing it can't remove another plugin's). * * @since 1.0.0 * @return bool */ public static function off() { return false; } /** * `woocommerce_webhook_should_deliver`: no delivery (and so nothing queued). * * @since 1.0.0 * @param mixed $deliver Deliver. * @return bool */ public static function no_webhook( $deliver ) { return self::$webhooks ? (bool) $deliver : false; } /** * `woocommerce_can_reduce_order_stock` / `…_restore_…`: never for a test order. * * @since 1.0.0 * @param mixed $can Allowed. * @param mixed $order Order. * @return mixed */ public static function no_stock_change( $can, $order = null ) { return $order instanceof \WC_Abstract_Order && '' !== (string) $order->get_meta( Marker::META ) ? false : $can; } /** * `woocommerce_prevent_adjust_line_item_product_stock`: never for an item of a test order. * * @since 1.0.0 * @param mixed $prevent Prevent. * @param mixed $item Order item. * @return mixed */ public static function prevent_item_stock( $prevent, $item = null ) { if ( $item instanceof \WC_Order_Item ) { $order = $item->get_order(); if ( $order instanceof \WC_Abstract_Order && '' !== (string) $order->get_meta( Marker::META ) ) { return true; } } return $prevent; } /** * `woocommerce_email` (and at start when the mailer exists): every email of this mailer reads * as disabled for the rest of the step. Uses the mailer it's given, never WC()->mailer(). * * @since 1.0.0 * @param mixed $mailer WC_Emails. * @return void */ public static function disable_emails( $mailer ): void { if ( self::$depth <= 0 || ! $mailer instanceof \WC_Emails ) { return; } foreach ( (array) $mailer->get_emails() as $email ) { if ( $email instanceof \WC_Email && '' !== (string) $email->id ) { $hook = 'woocommerce_email_enabled_' . $email->id; if ( ! in_array( $hook, self::$email_hooks, true ) ) { add_filter( $hook, array( __CLASS__, 'off' ), PHP_INT_MAX ); self::$email_hooks[] = $hook; } } } } /** * Remove every WC_Emails::send_transactional_email / queue_transactional_email callback (string * or array form, any priority), remembering them for end(). * * @return void */ private static function unhook_emails(): void { global $wp_filter; $targets = array( 'send_transactional_email', 'queue_transactional_email' ); foreach ( (array) $wp_filter as $hook => $wp_hook ) { if ( ! $wp_hook instanceof \WP_Hook ) { continue; } foreach ( $wp_hook->callbacks as $priority => $callbacks ) { foreach ( $callbacks as $callback ) { $function = $callback['function'] ?? null; $class = ''; $method = ''; if ( is_string( $function ) && false !== strpos( $function, '::' ) ) { list( $class, $method ) = explode( '::', $function, 2 ); } elseif ( is_array( $function ) && 2 === count( $function ) && is_string( $function[1] ) ) { $class = is_object( $function[0] ) ? get_class( $function[0] ) : ( is_string( $function[0] ) ? $function[0] : '' ); $method = $function[1]; } if ( '' === $class || ! in_array( strtolower( $method ), $targets, true ) ) { continue; } $class = ltrim( $class, '\\' ); if ( 0 !== strcasecmp( $class, 'WC_Emails' ) && ! is_subclass_of( $class, 'WC_Emails' ) ) { continue; } self::$removed[] = array( (string) $hook, $function, (int) $priority, (int) ( $callback['accepted_args'] ?? 1 ) ); } } } foreach ( self::$removed as $entry ) { remove_action( $entry[0], $entry[1], $entry[2] ); } } }