=' ) ) { return sprintf( /* translators: 1: installed version, 2: minimum version. */ __( 'xCloud\'s purge plugin %1$s cannot take purges from xSpeed. Update it to %2$s or later from xCloud.', 'xspeed' ), $version, self::MIN_VERSION ); } return __( 'xCloud\'s purge plugin is installed but not accepting purges', 'xspeed' ); } /** * Mirror one purge-event context at the edge. * * @param mixed $context Purge-event context. See the * `xspeed_after_purge` docblock in Cache. */ public static function forward( $context ): void { if ( ! is_array( $context ) || true !== self::status() ) { return; } // A purge run that includes the `xcloud` target (`wp xspeed purge`, // Purge All) hands the whole domain over itself and reports it on its // own line. Sending the page-cache event's purge as well made one // Purge All two whole-domain purges in WP-CLI and cron, where nothing // latches. Scoped to the run, not the request, so a later purge in // the same long run is still handed over. if ( Purge_Runner::covers( 'xcloud' ) ) { return; } $scope = isset( $context['scope'] ) && is_string( $context['scope'] ) ? $context['scope'] : ''; if ( 'none' === $scope ) { return; } if ( 'urls' === $scope ) { $urls = array(); foreach ( (array) ( $context['urls'] ?? array() ) as $url ) { // The endpoint refuses a URL on a host the domain does not // serve, and the refusal costs the whole request, xCloud's own // purges included. So only this domain's URLs go in. if ( is_string( $url ) && self::on_this_domain( $url ) ) { $urls[] = $url; } } self::queue_urls( $urls ); return; } if ( 'network' === $scope ) { self::queue_everything(); return; } // `site`, and anything unknown: a purge we cannot read is likelier to // need the edge than not. Only for this domain, though. A full purge // of another multisite site on a host of its own is not ours to send. $host = isset( $context['host'] ) && is_string( $context['host'] ) ? $context['host'] : ''; if ( '' === $host || '*' === $host || self::host_is_this_domain( $host ) ) { self::queue_everything(); } } /** * Purge the whole domain at the edge. The Purge_Runner `xcloud` target. * * @param string $cause Who asked. Unused: xCloud's request carries no cause. * @return array{ok?:bool,reason:string} */ public static function purge_everything( string $cause = 'manual' ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- Purge_Runner callback signature. $status = self::status(); if ( true !== $status ) { return array( 'ok' => false, 'reason' => (string) $status, ); } self::queue_everything(); return array( 'reason' => __( 'handed to xCloud\'s purge plugin, which sends it when this request ends', 'xspeed' ), ); } /** * Queue URLs with xCloud's plugin. * * @param array $urls Absolute URLs on this domain. */ private static function queue_urls( array $urls ): void { $urls = array_values( array_unique( $urls ) ); if ( array() === $urls || self::$everything_queued ) { return; } do_action( 'xcloud_cfe_purge_urls', $urls ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- xCloud's own action; xSpeed calls it, it does not define it. } /** Queue a whole-domain purge, once per page load. */ private static function queue_everything(): void { if ( self::$everything_queued ) { return; } self::$everything_queued = self::latches(); do_action( 'xcloud_cfe_purge_everything' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- xCloud's own action; xSpeed calls it, it does not define it. } /** * Whether a queued whole-domain purge may stand for the rest of this * process. * * In a page load, yes: the plugin sends once, at the end, so everything * after the first whole-domain purge is covered by it. WP-CLI and cron * can run for minutes and purge many times. When the plugin sends is * not visible from here, and a purge it has already sent cannot cover a * change made after it, so there every purge is handed over. The plugin * batches what it is given, so a repeat costs nothing if it has not sent * yet. */ private static function latches(): bool { if ( defined( 'WP_CLI' ) && WP_CLI ) { return false; } return ! ( function_exists( 'wp_doing_cron' ) && wp_doing_cron() ); } /** * Whether a URL is on the domain xCloud's plugin purges for. * * @param string $url Absolute URL. */ private static function on_this_domain( string $url ): bool { $parts = function_exists( 'wp_parse_url' ) ? wp_parse_url( $url ) : parse_url( $url ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- fallback for a bare test harness. if ( ! is_array( $parts ) || empty( $parts['host'] ) ) { return false; } $scheme = isset( $parts['scheme'] ) ? strtolower( (string) $parts['scheme'] ) : ''; if ( 'http' !== $scheme && 'https' !== $scheme ) { return false; } // A non-default port is another origin, and not one the edge serves. if ( isset( $parts['port'] ) ) { $port = (int) $parts['port']; if ( ! ( ( 'https' === $scheme && 443 === $port ) || ( 'http' === $scheme && 80 === $port ) ) ) { return false; } } return self::host_is_this_domain( (string) $parts['host'] ); } /** * Whether a host is this site's, allowing the apex and `www` forms of it. * * xCloud registers an apex with its `www` form, so a URL on either is one * the edge serves. Any other host, including another multisite site's own * domain, is not. * * @param string $host Host, optionally with a port. */ private static function host_is_this_domain( string $host ): bool { if ( ! function_exists( 'home_url' ) ) { return false; } $home = function_exists( 'wp_parse_url' ) ? wp_parse_url( (string) home_url( '/' ), PHP_URL_HOST ) : parse_url( (string) home_url( '/' ), PHP_URL_HOST ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- see above. $ours = self::without_www( strtolower( (string) $home ) ); $given = self::without_www( strtolower( (string) preg_replace( '/:\d+$/', '', $host ) ) ); return '' !== $ours && $ours === $given; } /** * Strip one leading `www.`. * * @param string $host Lowercase host. */ private static function without_www( string $host ): string { return 0 === strpos( $host, 'www.' ) ? substr( $host, 4 ) : $host; } /** Test seam: forget the per-request state. */ public static function reset(): void { self::$everything_queued = false; } }