# xspeed/1.0.4/includes/modules/Cdn/CdnModule.php

xSpeed Cache: AI-Powered Performance Hub with MCP, Caching &amp; CDN, version 1.0.4. 160 lines.

- Page: https://pluginprobe.com/plugins/xspeed/1.0.4/code/includes/modules/Cdn/CdnModule.php
- Raw: https://pluginprobe.com/plugins/xspeed/1.0.4/raw/includes/modules/Cdn/CdnModule.php
- Modified: 2026-06-14T07:01:26+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/xspeed/1.0.4/code/includes/modules/Cdn/CdnModule.php#L10-L20`.

```php
<?php
/**
 * CDN module — rewrites local asset URLs to a user-supplied pull-zone
 * CDN hostname (BunnyCDN, KeyCDN, Cloudflare R2, custom).
 *
 * Tier: Free per FEATURES.md "CDN Integration" §1-6 (LiteSpeed parity).
 *
 * @package XSpeed
 */

declare(strict_types=1);

namespace XSpeed\Modules\Cdn;

use XSpeed\Cdn_Rewriter;
use XSpeed\Module;

final class CdnModule extends Module {

	public const SLUG    = 'cdn';
	public const TIER    = self::TIER_FREE;
	public const VERSION = '1.0.0';

	public function ui_metadata(): array {
		return array(
			'label'       => 'CDN',
			'icon'        => 'Globe',
			'description' => 'Serve static assets (images, fonts, CSS, JS) from a pull-zone CDN host like BunnyCDN, KeyCDN, or your own.',
		);
	}

	public function settings_schema(): array {
		return array(
			'enabled' => array(
				'type'        => 'bool',
				'default'     => false,
				'label'       => 'Enable CDN',
				'description' => 'Rewrite static asset URLs to the CDN hostname below. Your CDN must be a pull-zone configured to fetch from this site.',
			),
			'cdn_url' => array(
				'type'        => 'string',
				'default'     => '',
				'label'       => 'CDN URL',
				'description' => 'CDN hostname, e.g. cdn.example.com. https:// and trailing slashes are stripped automatically.',
			),
			'included_extensions' => array(
				'type'        => 'list',
				'default'     => Cdn_Rewriter::DEFAULT_EXTENSIONS,
				'item_type'   => 'string',
				'label'       => 'Included File Extensions',
				'description' => 'Only URLs ending in these extensions are rewritten. Defaults cover images, fonts, CSS, JS, and common media.',
			),
			'excluded_patterns' => array(
				'type'        => 'list',
				'default'     => array(),
				'item_type'   => 'string',
				'label'       => 'Excluded Patterns',
				'description' => 'Glob patterns matched against the URL path. Matching URLs stay on the origin. Examples: /wp-admin/*, *.pdf, /private/*',
			),
		);
	}

	public function conflicts(): array {
		return array(
			array(
				'plugin'   => 'cdn-enabler/cdn-enabler.php',
				'feature'  => 'cdn.rewrite',
				'strategy' => \XSpeed\Conflict_Registry::STRATEGY_REFUSE,
				'reason'   => 'CDN Enabler rewrites the same URLs; running both will double-rewrite or produce broken hosts.',
			),
		);
	}

	public function boot(): void {
		// Always-on: normalize cdn_url on save (admin context too).
		add_filter( 'pre_update_option_xspeed_module_cdn', array( $this, 'normalize_on_save' ), 10, 1 );

		if ( is_admin() || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) || ( defined( 'DOING_CRON' ) && DOING_CRON ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
			return;
		}
		$opts = $this->get_settings();
		if ( empty( $opts['enabled'] ) || empty( $opts['cdn_url'] ) ) {
			return;
		}
		Cdn_Rewriter::reset_state();
		// Late filter — same convention as Lazy_Loader. Runs after
		// shortcodes / blocks / embeds finish injecting tags.
		add_filter( 'the_content',         array( Cdn_Rewriter::class, 'process_html' ), 1000 );
		add_filter( 'post_thumbnail_html', array( Cdn_Rewriter::class, 'process_html' ), 1000 );
		add_filter( 'widget_text_content', array( Cdn_Rewriter::class, 'process_html' ), 1000 );
		add_filter( 'wp_get_attachment_url', array( $this, 'rewrite_attachment_url' ), 1000 );
	}

	public function rewrite_attachment_url( $url ) {
		if ( ! is_string( $url ) || '' === $url ) {
			return $url;
		}
		return Cdn_Rewriter::rewrite_url( $url, $this->get_settings() );
	}

	/**
	 * pre_update_option filter — strips https:// + trailing slash from
	 * cdn_url before storage, so we always work against a bare host.
	 *
	 * @param mixed $value
	 * @return mixed
	 */
	public function normalize_on_save( $value ) {
		if ( ! is_array( $value ) ) {
			return $value;
		}
		if ( isset( $value['cdn_url'] ) ) {
			$value['cdn_url'] = Cdn_Rewriter::normalize_host( (string) $value['cdn_url'] );
		}
		return $value;
	}

	public function cli_commands(): array {
		return array(
			array(
				'name'      => 'xspeed cdn',
				'callback'  => array( $this, 'cli_handler' ),
				'shortdesc' => 'Show CDN settings + test rewriting a URL.',
				'synopsis'  => array(
					array(
						'type'     => 'positional',
						'name'     => 'action',
						'options'  => array( 'status', 'test' ),
						'optional' => true,
					),
					array(
						'type'     => 'assoc',
						'name'     => 'url',
						'optional' => true,
					),
				),
			),
		);
	}

	public function cli_handler( array $args, array $assoc ): void {
		$action = $args[0] ?? 'status';
		$opts   = $this->get_settings();
		if ( 'test' === $action ) {
			$url = isset( $assoc['url'] ) ? (string) $assoc['url'] : '';
			if ( '' === $url ) {
				\WP_CLI::error( 'Pass --url=<url> to test rewriting.' );
			}
			Cdn_Rewriter::reset_state();
			\WP_CLI::log( 'in:  ' . $url );
			\WP_CLI::log( 'out: ' . Cdn_Rewriter::rewrite_url( $url, $opts ) );
			return;
		}
		\WP_CLI::log( sprintf( '%-22s %s', 'enabled', ! empty( $opts['enabled'] ) ? 'on' : 'off' ) );
		\WP_CLI::log( sprintf( '%-22s %s', 'cdn_url', (string) ( $opts['cdn_url'] ?? '' ) ) );
		\WP_CLI::log( sprintf( '%-22s %s', 'included_extensions', implode( ',', (array) ( $opts['included_extensions'] ?? array() ) ) ) );
		\WP_CLI::log( sprintf( '%-22s %s', 'excluded_patterns', implode( ',', (array) ( $opts['excluded_patterns'] ?? array() ) ) ) );
	}
}

```
