'dispatch cut-off: before the cut-off', 'tomorrow' => 'dispatch cut-off: shipped tomorrow', 'later' => 'dispatch cut-off: shipped on a later day', ); private static $booted = false; private static $settings = null; private static $words = array(); public static function settings() { if ( null !== self::$settings ) { return self::$settings; } $s = array_key_exists( self::OPTION, wp_load_alloptions() ) || has_filter( 'pre_option_' . self::OPTION ) ? get_option( self::OPTION, array() ) : array(); $s = is_array( $s ) ? $s : array(); $days = isset( $s['days'] ) && is_array( $s['days'] ) ? self::days( $s['days'] ) : self::WEEK_DAYS; self::$settings = array( 'cutoff' => self::time( isset( $s['cutoff'] ) ? (string) $s['cutoff'] : '' ), 'days' => $days, 'today' => isset( $s['today'] ) ? trim( (string) $s['today'] ) : '', 'tomorrow' => isset( $s['tomorrow'] ) ? trim( (string) $s['tomorrow'] ) : '', 'later' => isset( $s['later'] ) ? trim( (string) $s['later'] ) : '', ); return self::$settings; } public static function update( array $changes ) { $s = self::settings(); if ( array_key_exists( 'cutoff', $changes ) ) { $s['cutoff'] = self::time( (string) $changes['cutoff'] ); } if ( array_key_exists( 'days', $changes ) ) { $s['days'] = self::days( (array) $changes['days'] ); } foreach ( array( 'today', 'tomorrow', 'later' ) as $key ) { if ( array_key_exists( $key, $changes ) ) { $s[ $key ] = trim( (string) $changes[ $key ] ); } } update_option( self::OPTION, $s, true ); self::flush(); return self::settings(); } public static function flush() { self::$settings = null; self::$words = array(); } public static function time( $value ) { return preg_match( '/^([01]?\d|2[0-3]):([0-5]\d)(?::[0-5]\d)?$/', trim( (string) $value ), $m ) ? sprintf( '%02d:%s', (int) $m[1], $m[2] ) : ''; } public static function days( array $days ) { $days = array_values( array_unique( array_filter( array_map( 'intval', $days ), static function ( $d ) { return $d >= 1 && $d <= 7; } ) ) ); sort( $days ); return $days; } public static function on() { $s = self::settings(); return '' !== $s['cutoff'] && array() !== $s['days']; } private static function now() { return (int) apply_filters( 'aieo_dispatch_cutoff_now', time() ); } private static function cutoff_minutes() { $parts = explode( ':', self::settings()['cutoff'] ); return (int) $parts[0] * 60 + (int) ( $parts[1] ?? 0 ); } public static function moment( $now = null ) { $days = self::settings()['days']; $local = ( new DateTimeImmutable( '@' . (int) ( null === $now ? self::now() : $now ) ) )->setTimezone( wp_timezone() ); $day = (int) $local->format( 'N' ); if ( in_array( $day, $days, true ) && (int) $local->format( 'G' ) * 60 + (int) $local->format( 'i' ) < self::cutoff_minutes() ) { return array( 'today', 0 ); } for ( $i = 1; $i <= 7; $i++ ) { $next = ( $day - 1 + $i ) % 7 + 1; if ( in_array( $next, $days, true ) ) { break; } } return 1 === $i ? array( 'tomorrow', 0 ) : array( 'later', $next ); } public static function words( $moment, $day = 0 ) { $key = determine_locale() . '|' . $moment . '|' . (int) $day; if ( isset( self::$words[ $key ] ) ) { return self::$words[ $key ]; } $s = self::settings(); $own = isset( $s[ $moment ] ) ? (string) $s[ $moment ] : ''; if ( 'today' === $moment ) { $time = self::time_text(); $words = '' !== $own ? str_replace( '{time}', $time, self::translated( $own, 'today' ) ) : sprintf( __( 'ordered before %s, shipped today', 'ai-eshop-optimizer' ), $time ); } elseif ( 'tomorrow' === $moment ) { $words = '' !== $own ? self::translated( $own, 'tomorrow' ) : __( 'shipped tomorrow', 'ai-eshop-optimizer' ); } else { $words = '' !== $own ? str_replace( '{day}', self::day_name( $day ), self::translated( $own, 'later' ) ) : self::later_words( $day ); } self::$words[ $key ] = trim( (string) $words ); return self::$words[ $key ]; } private static function later_words( $day ) { switch ( (int) $day ) { case 1: return __( 'shipped on Monday', 'ai-eshop-optimizer' ); case 2: return __( 'shipped on Tuesday', 'ai-eshop-optimizer' ); case 3: return __( 'shipped on Wednesday', 'ai-eshop-optimizer' ); case 4: return __( 'shipped on Thursday', 'ai-eshop-optimizer' ); case 5: return __( 'shipped on Friday', 'ai-eshop-optimizer' ); case 6: return __( 'shipped on Saturday', 'ai-eshop-optimizer' ); default: return __( 'shipped on Sunday', 'ai-eshop-optimizer' ); } } private static function day_name( $day ) { global $wp_locale; $index = (int) $day % 7; return $wp_locale instanceof WP_Locale ? (string) $wp_locale->get_weekday( $index ) : gmdate( 'l', ( 3 + $index ) * DAY_IN_SECONDS ); } public static function time_text() { $format = (string) get_option( 'time_format' ); return wp_date( '' !== $format ? $format : 'H:i', self::cutoff_minutes() * MINUTE_IN_SECONDS, new DateTimeZone( 'UTC' ) ); } private static function translated( $text, $moment ) { if ( has_filter( 'wpml_translate_single_string' ) ) { return (string) apply_filters( 'wpml_translate_single_string', $text, self::STRINGS_DOMAIN, self::STRING_NAMES[ $moment ] ); } return function_exists( 'pll__' ) ? (string) pll__( $text ) : (string) $text; } public static function all_words() { $days = self::settings()['days']; $words = array( self::words( 'today' ), self::words( 'tomorrow' ), '', '', '', '', '', '', '' ); for ( $day = 1; $day <= 7; $day++ ) { for ( $i = 1; $i <= 7; $i++ ) { $next = ( $day - 1 + $i ) % 7 + 1; if ( in_array( $next, $days, true ) ) { break; } } if ( $i > 1 ) { $words[ 2 + $next % 7 ] = self::words( 'later', $next ); } } return $words; } public static function data( $now = null ) { $now = (int) ( null === $now ? self::now() : $now ); $zone = wp_timezone(); $offsets = array( (int) $zone->getOffset( new DateTimeImmutable( '@' . $now ) ) ); $changes = $zone->getTransitions( $now, $now + 2 * YEAR_IN_SECONDS ); if ( is_array( $changes ) ) { foreach ( array_slice( $changes, 1 ) as $change ) { $offsets[] = (int) $change['ts']; $offsets[] = (int) $change['offset']; } } $bits = 0; foreach ( self::settings()['days'] as $day ) { $bits |= 1 << ( $day % 7 ); } return array( 'o' => $offsets, 'c' => self::cutoff_minutes(), 'd' => $bits, 'p' => self::all_words(), ); } public static function script( $target = 'previousElementSibling', $now = null ) { static $source = null; if ( null === $source ) { $file = dirname( __DIR__ ) . '/assets/js/aieo-dispatch-cutoff.min.js'; $source = is_readable( $file ) ? trim( (string) file_get_contents( $file ) ) : ''; } if ( '' === $source ) { return ''; } $js = str_replace( array( 'document.currentScript.previousElementSibling', 'AIEO_CUTOFF' ), array( 'document.currentScript.' . ( 'parentNode' === $target ? 'parentNode' : 'previousElementSibling' ), (string) wp_json_encode( self::data( $now ), JSON_HEX_TAG | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE ) ), $source ); return wp_get_inline_script_tag( $js, array( 'data-cfasync' => 'false' ) ); } public static function storefront() { if ( wp_doing_cron() ) { return false; } return ! is_admin() || wp_doing_ajax(); } public static function applies( $product ) { $applies = $product instanceof WC_Product && $product->is_in_stock() && ! $product->is_on_backorder( 1 ) && $product->needs_shipping() && $product->is_purchasable(); return (bool) apply_filters( 'aieo_dispatch_cutoff_applies', $applies, $product ); } public static function availability( $text, $product = null ) { if ( ! self::on() || ! self::storefront() || ! self::applies( $product ) ) { return $text; } $words = trim( (string) $text ); if ( '' !== $words && self::holds_words( $words ) ) { return $text; } if ( '' === $words ) { $words = _x( 'In stock', 'the stock line, before when an order is shipped', 'ai-eshop-optimizer' ); } return $words . self::SEPARATOR . self::words( ...self::moment() ); } private static function holds_words( $html ) { $text = html_entity_decode( wp_strip_all_tags( (string) $html ), ENT_QUOTES, 'UTF-8' ); foreach ( self::all_words() as $words ) { if ( '' !== $words && false !== strpos( $text, $words ) ) { return true; } } return false; } public static function block( $content ) { $content = (string) $content; if ( '' === $content || ! self::on() || ! self::storefront() ) { return $content; } if ( false === strpos( $content, 'data-wp-interactive' ) && ! self::holds_words( $content ) ) { return $content; } return $content . self::script(); } public static function stock_html( $html, $product = null ) { $html = (string) $html; if ( '' === $html || ! self::on() || ! self::storefront() || ( $product instanceof WC_Product && $product->is_type( 'variation' ) ) || ! self::holds_words( $html ) ) { return $html; } return $html . self::script(); } public static function variation_form() { global $product; if ( ! self::on() || ! self::storefront() || ! self::applies( $product ) ) { return; } echo self::script( 'parentNode' ); } public static function boot() { if ( self::$booted ) { return; } self::$booted = true; if ( self::on() ) { self::hooks(); } } public static function hooks() { add_filter( 'woocommerce_get_availability_text', array( __CLASS__, 'availability' ), 20, 2 ); add_filter( 'render_block_woocommerce/product-stock-indicator', array( __CLASS__, 'block' ), 20 ); add_filter( 'woocommerce_get_stock_html', array( __CLASS__, 'stock_html' ), 20, 2 ); add_action( 'woocommerce_after_variations_form', array( __CLASS__, 'variation_form' ) ); } } }