post_type ) { return; } if ( $new_status === $old_status || 'auto-draft' === $new_status || 'auto-draft' === $old_status ) { return; } self::stamp_modified( (int) $post->ID ); } /** * Trashed products need nothing here: they reach the backend as modified products with status "trash". * * @param int|string $post_id Post id. * @param \WP_Post|null $post The post (WordPress 5.5+). */ public function on_before_delete_post( $post_id, $post = null ): void { if ( ! Options::is_connected() ) { return; } $post = $post instanceof \WP_Post ? $post : get_post( (int) $post_id ); if ( $post instanceof \WP_Post && 'product' === $post->post_type ) { Options::add_deleted_product_id( (int) $post->ID ); } if ( $post instanceof \WP_Post && 'product_variation' === $post->post_type ) { $this->touch_parent( (int) $post->ID ); } } /** * A variation that is created, saved or deleted on its own does not change its parent's modified date, so the * sync would never see it. Stamp the parent's modified date, exactly as WooCommerce does when it saves a product * with no other changes: the next run re-sends the parent with all its variations. * * @param int|string $variation_id Variation id. */ public function touch_parent( $variation_id ): void { if ( ! Options::is_connected() ) { return; } $parent = wp_get_post_parent_id( (int) $variation_id ); if ( $parent <= 0 ) { return; } self::stamp_modified( $parent ); } /** * Sets a post's modified date to now without a save. */ private static function stamp_modified( int $post_id ): void { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- the same direct update WooCommerce's product data store makes; wp_update_post() would fire every save hook again. $wpdb->update( $wpdb->posts, array( 'post_modified' => current_time( 'mysql' ), 'post_modified_gmt' => current_time( 'mysql', true ), ), array( 'ID' => $post_id ) ); clean_post_cache( $post_id ); } /** * Schedules the recurring push unless it already is, and runs one push straight away. */ public static function schedule(): void { if ( ! function_exists( 'as_schedule_recurring_action' ) ) { return; } // Checked before the immediate run is queued, which would otherwise count as "already scheduled". Null // args means "any args" to Action Scheduler (see OrderEvents::unschedule_all()). if ( ! as_has_scheduled_action( self::HOOK, null, self::GROUP ) ) { as_schedule_recurring_action( time() + self::INTERVAL, self::INTERVAL, self::HOOK, array(), self::GROUP ); } as_enqueue_async_action( self::HOOK, array(), self::GROUP ); } /** * Starts the next run in a new PHP process, the way WordPress starts WP-Cron: a non-blocking POST to the plugin's * own run route, carrying a single-use token that expires in two minutes. When the host refuses loopback * requests, the 5-minute schedule continues the sync instead. */ public static function chain(): void { if ( ! Options::is_connected() ) { return; // A disconnect during the run cleared the token; do not save a new one. } $token = wp_generate_password( 32, false ); Options::save_run_token( $token, time() + self::RUN_TOKEN_TTL ); $response = wp_remote_post( rest_url( RestModule::NAMESPACE . '/sync/run' ), array( 'blocking' => false, 'timeout' => 0.01, // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress core's own filter, as wp-cron's spawn uses it. 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), 'headers' => array( 'X-Cortex-Run-Token' => $token ), ) ); if ( is_wp_error( $response ) ) { Log::info( 'Product sync could not start the next run itself; the 5-minute schedule will continue it.' ); } } public static function unschedule(): void { if ( function_exists( 'as_unschedule_all_actions' ) ) { // @phpstan-ignore-next-line argument.type -- null means "any args" to Action Scheduler (see OrderEvents::unschedule_all()). as_unschedule_all_actions( self::HOOK, null, self::GROUP ); } } }