Skip to content

Add Azure GCC High as CDN Engine Option - #1372

Open
gwhitson wants to merge 5 commits into
BoldGrid:masterfrom
gwhitson:master
Open

Add Azure GCC High as CDN Engine Option#1372
gwhitson wants to merge 5 commits into
BoldGrid:masterfrom
gwhitson:master

Conversation

@gwhitson

@gwhitson gwhitson commented Jul 30, 2026

Copy link
Copy Markdown

The Azure CDNEngine does not allow use with GCC High endpoints. Adding option for GCC High Azure environments.

In my current production environment, I have to adjust a few lines of the Azure CDNEngine php file to change the URL that it attempts to use.


Note

Medium Risk
New CDN path handles account keys and changes where assets are uploaded/served; misconfiguration could break CDN or point uploads at the wrong cloud, though existing commercial Azure behavior is unchanged.

Overview
Introduces Azure GCC High as a selectable CDN engine so sites in Azure Government can push static assets to blob storage without patching the commercial Azure engine.

CdnEngine now maps engine id azure_gcc_high to a new CdnEngine_Azure_GCC_High class. That engine mirrors the standard Azure blob workflow (upload, delete, connectivity test, container creation, URL/domain helpers) but wires the storage client and default hostnames to core.usgovcloudapi.net (e.g. {account}.blob.core.usgovcloudapi.net) instead of the public cloud endpoint suffix.

A new CDN settings partial at inc/options/cdn/azure_gcc_high.php exposes the same account/container/SSL/CNAME fields as Azure, with UI copy reflecting the GCC High blob hostname.

Reviewed by Cursor Bugbot for commit 174a040. Bugbot is set up for automated code reviews on this repo. Configure here.

@gwhitson
gwhitson requested a review from a team July 30, 2026 19:46
Var changed since I last looked at adding this

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 9 potential issues.

Fix All in Cursor

Done

Create PR

Or push these changes by commenting:

@cursor push 1dd3054594
Preview (1dd3054594)
diff --git a/Cache.php b/Cache.php
--- a/Cache.php
+++ b/Cache.php
@@ -181,6 +181,10 @@
 				$engine_name = 'Microsoft Azure Storage';
 				break;
 
+			case 'azure_gcc_high':
+				$engine_name = 'Microsoft Azure Storage (GCC High)';
+				break;
+
 			case 'azuremi':
 				$engine_name = 'Microsoft Azure Storage (Managed Identity)';
 				break;

diff --git a/CdnEngine.php b/CdnEngine.php
--- a/CdnEngine.php
+++ b/CdnEngine.php
@@ -30,6 +30,10 @@
 				case 'azure':
 					$instances[ $instance_key ] = new CdnEngine_Azure( $w3tc_config );
 					break;
+				
+				case 'azure_gcc_high':
+					$instances[ $instance_key ] = new CdnEngine_Azure_GCC_High( $w3tc_config );
+					break;
 
 				case 'azuremi':
 					$instances[ $instance_key ] = new CdnEngine_Azure_MI( $w3tc_config );

diff --git a/CdnEngine_Azure_GCC_High.php b/CdnEngine_Azure_GCC_High.php
new file mode 100644
--- /dev/null
+++ b/CdnEngine_Azure_GCC_High.php
@@ -1,0 +1,420 @@
+<?php
+/**
+ * File: CdnEngine_Azure.php
+ *
+ * @package W3TC
+ */
+
+namespace W3TC;
+
+use MicrosoftAzure\Storage\Blob\BlobRestProxy;
+use MicrosoftAzure\Storage\Common\ServiceException;
+
+/**
+ * Class: CdnEngine_Azure
+ *
+ * Windows Azure Storage CDN engine
+ */
+class CdnEngine_Azure_GCC_High extends CdnEngine_Base {
+	/**
+	 * Storage client object
+	 *
+	 * @var \MicrosoftAzure\Storage\Blob\BlobRestProxy
+	 */
+	var $_client = null; // phpcs:ignore PSR2.Classes.PropertyDeclaration
+
+	/**
+	 * Constructor.
+	 *
+	 * @param array $config Configuration.
+	 */
+	public function __construct( $config = array() ) {
+		$config = array_merge(
+			array(
+				'user' => '',
+				'key' => '',
+				'container' => '',
+				'cname' => array(),
+			),
+			$config
+		);
+
+		parent::__construct( $config );
+
+		// Load the Composer autoloader.
+		require_once W3TC_DIR . '/vendor/autoload.php';
+	}
+
+	/**
+	 * Inits storage client object
+	 *
+	 * @param string $error Error message.
+	 * @return bool
+	 */
+	public function _init( &$error ) {
+		if ( empty( $this->_config['user'] ) ) {
+			$error = 'Empty account name.';
+			return false;
+		}
+
+		if ( empty( $this->_config['key'] ) ) {
+			$error = 'Empty account key.';
+
+			return false;
+		}
+
+		if ( empty( $this->_config['container'] ) ) {
+			$error = 'Empty container name.';
+
+			return false;
+		}
+
+		try {
+			$this->_client = BlobRestProxy::createBlobService(
+				'DefaultEndpointsProtocol=https;AccountName=' . $this->_config['user'] . ';AccountKey=' . $this->_config['key'] . ';EndpointSuffix=core.usgovcloudapi.net'
+			);
+		} catch ( \Exception $ex ) {
+			$error = $ex->getMessage();
+			return false;
+		}
+
+		return true;
+	}
+
+	/**
+	 * Uploads files to S3
+	 *
+	 * @param array   $files
+	 * @param array   $results
+	 * @param boolean $force_rewrite
+	 * @return boolean
+	 */
+	function upload( $files, &$results, $force_rewrite = false,
+		$timeout_time = NULL ) {
+		$error = null;
+
+		if ( !$this->_init( $error ) ) {
+			$results = $this->_get_results( $files, W3TC_CDN_RESULT_HALT, $error );
+
+			return false;
+		}
+
+		foreach ( $files as $file ) {
+			$remote_path = $file['remote_path'];
+			$local_path = $file['local_path'];
+
+			// process at least one item before timeout so that progress goes on
+			if ( !empty( $results ) ) {
+				if ( !is_null( $timeout_time ) && time() > $timeout_time ) {
+					return 'timeout';
+				}
+			}
+
+			$results[] = $this->_upload( $file, $force_rewrite );
+		}
+
+		return !$this->_is_error( $results );
+	}
+
+	/**
+	 * Uploads file
+	 *
+	 * @param string  $local_path
+	 * @param string  $remote_path
+	 * @param bool    $force_rewrite
+	 * @return array
+	 */
+	function _upload( $file, $force_rewrite = false ) {
+		$local_path = $file['local_path'];
+		$remote_path = $file['remote_path'];
+
+		if ( !file_exists( $local_path ) ) {
+			return $this->_get_result( $local_path, $remote_path,
+				W3TC_CDN_RESULT_ERROR, 'Source file not found.', $file );
+		}
+
+		$contents = @file_get_contents( $local_path );
+		$md5 = md5( $contents );   // @md5_file( $local_path );
+		$content_md5 = $this->_get_content_md5( $md5 );
+
+		if ( !$force_rewrite ) {
+			try {
+				$propertiesResult = $this->_client->getBlobProperties( $this->_config['container'], $remote_path );
+				$p = $propertiesResult->getProperties();
+
+				$local_size = @filesize( $local_path );
+
+				if ( $local_size == $p->getContentLength() && $content_md5 === $p->getContentMD5() ) {
+					return $this->_get_result( $local_path, $remote_path,
+						W3TC_CDN_RESULT_OK, 'File up-to-date.', $file );
+				}
+			} catch ( \Exception $exception ) {
+			}
+		}
+
+		$headers = $this->get_headers_for_file( $file );
+
+		try {
+			// $headers
+			$options = new \MicrosoftAzure\Storage\Blob\Models\CreateBlockBlobOptions();
+			$options->setContentMD5( $content_md5 );
+			if ( isset( $headers['Content-Type'] ) )
+				$options->setContentType( $headers['Content-Type'] );
+			if ( isset( $headers['Cache-Control'] ) )
+				$options->setCacheControl( $headers['Cache-Control'] );
+
+			$this->_client->createBlockBlob( $this->_config['container'],
+				$remote_path, $contents, $options );
+		} catch ( \Exception $exception ) {
+			return $this->_get_result( $local_path, $remote_path,
+				W3TC_CDN_RESULT_ERROR,
+				sprintf( 'Unable to put blob (%s).', $exception->getMessage() ),
+				$file );
+		}
+
+		return $this->_get_result( $local_path, $remote_path, W3TC_CDN_RESULT_OK,
+			'OK', $file );
+	}
+
+	/**
+	 * Deletes files from storage
+	 *
+	 * @param array   $files
+	 * @param array   $results
+	 * @return boolean
+	 */
+	function delete( $files, &$results ) {
+		$error = null;
+
+		if ( !$this->_init( $error ) ) {
+			$results = $this->_get_results( $files, W3TC_CDN_RESULT_HALT, $error );
+
+			return false;
+		}
+
+		foreach ( $files as $file ) {
+			$local_path = $file['local_path'];
+			$remote_path = $file['remote_path'];
+
+			try {
+				$r = $this->_client->deleteBlob( $this->_config['container'], $remote_path );
+				$results[] = $this->_get_result( $local_path, $remote_path,
+					W3TC_CDN_RESULT_OK, 'OK', $file );
+			} catch ( \Exception $exception ) {
+				$results[] = $this->_get_result( $local_path, $remote_path,
+					W3TC_CDN_RESULT_ERROR,
+					sprintf( 'Unable to delete blob (%s).', $exception->getMessage() ),
+					$file );
+			}
+		}
+
+		return !$this->_is_error( $results );
+	}
+
+	/**
+	 * Tests S3
+	 *
+	 * @param string  $error
+	 * @return boolean
+	 */
+	function test( &$error ) {
+		if ( !parent::test( $error ) ) {
+			return false;
+		}
+
+		$string = 'test_azure_' . md5( time() );
+
+		if ( !$this->_init( $error ) ) {
+			return false;
+		}
+
+		try {
+			$containers = $this->_client->listContainers();
+		} catch ( \Exception $exception ) {
+			$error = sprintf( 'Unable to list containers (%s).', $exception->getMessage() );
+
+			return false;
+		}
+
+		$container = null;
+
+		foreach ( $containers->getContainers() as $_container ) {
+			if ( $_container->getName() == $this->_config['container'] ) {
+				$container = $_container;
+				break;
+			}
+		}
+
+		if ( !$container ) {
+			$error = sprintf( 'Container doesn\'t exist: %s.', $this->_config['container'] );
+
+			return false;
+		}
+
+		try {
+			$this->_client->createBlockBlob( $this->_config['container'], $string, $string );
+		} catch ( \Exception $exception ) {
+			$error = sprintf( 'Unable to create blob (%s).', $exception->getMessage() );
+			return false;
+		}
+
+		try {
+			$propertiesResult = $this->_client->getBlobProperties( $this->_config['container'], $string );
+			$p = $propertiesResult->getProperties();
+			$size = $p->getContentLength();
+			$md5 = $p->getContentMD5();
+		} catch ( \Exception $exception ) {
+			$error = sprintf( 'Unable to get blob properties (%s).', $exception->getMessage() );
+			return false;
+		}
+
+		if ( $size != strlen( $string ) || $this->_get_content_md5( md5( $string ) ) != $md5 ) {
+			try {
+				$this->_client->deleteBlob( $this->_config['container'], $string );
+			} catch ( \Exception $exception ) {
+			}
+
+			$error = 'Blob data properties are not equal.';
+			return false;
+		}
+
+		try {
+			$getBlob = $this->_client->getBlob( $this->_config['container'], $string );
+			$dataStream = $getBlob->getContentStream();
+			$data = stream_get_contents( $dataStream );
+		} catch ( \Exception $exception ) {
+			$error = sprintf( 'Unable to get blob data (%s).', $exception->getMessage() );
+			return false;
+		}
+
+
+		if ( $data != $string ) {
+			try {
+				$this->_client->deleteBlob( $this->_config['container'], $string );
+			} catch ( \Exception $exception ) {
+			}
+
+			$error = 'Blob datas are not equal.';
+			return false;
+		}
+
+		try {
+			$this->_client->deleteBlob( $this->_config['container'], $string );
+		} catch ( \Exception $exception ) {
+			$error = sprintf( 'Unable to delete blob (%s).', $exception->getMessage() );
+
+			return false;
+		}
+
+		return true;
+	}
+
+	/**
+	 * Returns CDN domain
+	 *
+	 * @return array
+	 */
+	function get_domains() {
+		if ( !empty( $this->_config['cname'] ) ) {
+			return (array) $this->_config['cname'];
+		} elseif ( !empty( $this->_config['user'] ) ) {
+			$domain = sprintf( '%s.blob.core.usgovcloudapi.net', $this->_config['user'] );
+
+			return array(
+				$domain
+			);
+		}
+
+		return array();
+	}
+
+	/**
+	 * Returns via string
+	 *
+	 * @return string
+	 */
+	function get_via() {
+		return sprintf( 'Windows Azure Storage: %s', parent::get_via() );
+	}
+
+	/**
+	 * Creates bucket
+	 *
+	 * @param string  $error
+	 * @return boolean
+	 */
+	function create_container() {
+		if ( !$this->_init( $error ) ) {
+			throw new \Exception( $error );
+		}
+
+		try {
+			$containers = $this->_client->listContainers();
+		} catch ( \Exception $exception ) {
+			$error = sprintf( 'Unable to list containers (%s).', $exception->getMessage() );
+			throw new \Exception( $error );
+		}
+
+		foreach ( $containers->getContainers() as $_container ) {
+			if ( $_container->getName() === $this->_config['container'] ) {
+				$error = sprintf( 'Container already exists: %s.', $this->_config['container'] );
+				throw new \Exception( $error );
+			}
+		}
+
+		try {
+			$createContainerOptions = new \MicrosoftAzure\Storage\Blob\Models\CreateContainerOptions();
+			$createContainerOptions->setPublicAccess(
+				\MicrosoftAzure\Storage\Blob\Models\PublicAccessType::CONTAINER_AND_BLOBS );
+
+			$this->_client->createContainer( $this->_config['container'], $createContainerOptions );
+		} catch ( \Exception $exception ) {
+			$error = sprintf( 'Unable to create container: %s (%s)', $this->_config['container'], $exception->getMessage() );
+			throw new \Exception( $error );
+		}
+	}
+
+	/**
+	 * Returns Content-MD5 header value
+	 *
+	 * @param string  $string
+	 * @return string
+	 */
+	function _get_content_md5( $md5 ) {
+		return base64_encode( pack( 'H*', $md5 ) );
+	}
+
+	/**
+	 * Formats object URL
+	 *
+	 * @param string  $path
+	 * @return string
+	 */
+	function _format_url( $path ) {
+		$domain = $this->get_domain( $path );
+
+		if ( $domain && !empty( $this->_config['container'] ) ) {
+			$scheme = $this->_get_scheme();
+			$url = sprintf( '%s://%s/%s/%s', $scheme, $domain, $this->_config['container'], $path );
+
+			return $url;
+		}
+
+		return false;
+	}
+
+	/**
+	 * How and if headers should be set
+	 *
+	 * @return string W3TC_CDN_HEADER_NONE, W3TC_CDN_HEADER_UPLOADABLE, W3TC_CDN_HEADER_MIRRORING
+	 */
+	function headers_support() {
+		return W3TC_CDN_HEADER_UPLOADABLE;
+	}
+
+	function get_prepend_path( $path ) {
+		$path = parent::get_prepend_path( $path );
+		$path = $this->_config['container'] ? trim( $path, '/' ) . '/' . trim( $this->_config['container'], '/' ): $path;
+		return $path;
+	}
+}
\ No newline at end of file

diff --git a/Cdn_AdminActions.php b/Cdn_AdminActions.php
--- a/Cdn_AdminActions.php
+++ b/Cdn_AdminActions.php
@@ -634,6 +634,7 @@
 			case 'rscf':
 				return array( 'key' => 'cdn.rscf.key' );
 			case 'azure':
+			case 'azure_gcc_high':
 				return array( 'key' => 'cdn.azure.key' );
 			default:
 				return array();
@@ -764,6 +765,7 @@
 			case 'cf':
 			case 'cf2':
 			case 'azure':
+			case 'azure_gcc_high':
 			case 'azuremi':
 				$w3_cdn = CdnEngine::instance( $w3tc_engine, $w3tc_config );
 

diff --git a/Cdn_AdminNotes.php b/Cdn_AdminNotes.php
--- a/Cdn_AdminNotes.php
+++ b/Cdn_AdminNotes.php
@@ -285,7 +285,7 @@
 				$error = __( 'The <strong>"Username", "API key", "Container" and "Replace default hostname with"</strong> fields cannot be empty.', 'w3-total-cache' );
 				break;
 
-			case ( 'azure' === $w3tc_cdn_engine && ( empty( $w3tc_c->get_string( 'cdn.azure.user' ) ) || empty( $w3tc_c->get_string( 'cdn.azure.key' ) ) || empty( $w3tc_c->get_string( 'cdn.azure.container' ) ) ) ):
+			case ( ( 'azure' === $w3tc_cdn_engine || 'azure_gcc_high' === $w3tc_cdn_engine ) && ( empty( $w3tc_c->get_string( 'cdn.azure.user' ) ) || empty( $w3tc_c->get_string( 'cdn.azure.key' ) ) || empty( $w3tc_c->get_string( 'cdn.azure.container' ) ) ) ):
 				$error = __( 'The <strong>"Account name", "Account key" and "Container"</strong> fields cannot be empty.', 'w3-total-cache' );
 				break;
 

diff --git a/Cdn_Core.php b/Cdn_Core.php
--- a/Cdn_Core.php
+++ b/Cdn_Core.php
@@ -384,6 +384,7 @@
 
 			switch ( $w3tc_engine ) {
 				case 'azure':
+				case 'azure_gcc_high':
 					$engine_config = array(
 						'user'        => $w3tc_c->get_string( 'cdn.azure.user' ),
 						'key'         => $w3tc_c->get_string( 'cdn.azure.key' ),
@@ -835,6 +836,7 @@
 	public function is_cdn_authorized() {
 		switch ( $this->_config->get_string( 'cdn.engine' ) ) {
 			case 'azure':
+			case 'azure_gcc_high':
 				$is_cdn_authorized = ! empty( $this->_config->get_string( 'cdn.azure.user' ) ) &&
 					! empty( $this->_config->get_string( 'cdn.azure.key' ) ) &&
 					! empty( $this->_config->get_string( 'cdn.azure.container' ) ) &&

diff --git a/Cdn_Core_Admin.php b/Cdn_Core_Admin.php
--- a/Cdn_Core_Admin.php
+++ b/Cdn_Core_Admin.php
@@ -919,7 +919,7 @@
 				break;
 
 			case (
-				'azure' === $w3tc_cdn_engine &&
+				( 'azure' === $w3tc_cdn_engine || 'azure_gcc_high' === $w3tc_cdn_engine ) &&
 				(
 					'' === $this->_config->get_string( 'cdn.azure.user' ) ||
 					'' === $this->_config->get_string( 'cdn.azure.key' ) ||

diff --git a/Cdn_Plugin_Admin.php b/Cdn_Plugin_Admin.php
--- a/Cdn_Plugin_Admin.php
+++ b/Cdn_Plugin_Admin.php
@@ -151,6 +151,11 @@
 			'optgroup' => $optgroup_push,
 		);
 
+		$engine_values['azure_gcc_high'] = array(
+			'label'    => \__( 'Microsoft Azure Storage (GCC High)', 'w3-total-cache' ),
+			'optgroup' => $optgroup_push,
+		);
+
 		$engine_values['azuremi'] = array(
 			'disabled' => empty( getenv( 'APPSETTING_WEBSITE_SITE_NAME' ) ),
 			'label'    => \__( 'Microsoft Azure Storage (Managed Identity)', 'w3-total-cache' ),

diff --git a/Cdn_Util.php b/Cdn_Util.php
--- a/Cdn_Util.php
+++ b/Cdn_Util.php
@@ -25,6 +25,7 @@
 			$w3tc_engine,
 			array(
 				'azure',
+				'azure_gcc_high',
 				'azuremi',
 				'cf',
 				'cf2',
@@ -102,6 +103,7 @@
 			$w3tc_engine,
 			array(
 				'azure',
+				'azure_gcc_high',
 				'azuremi',
 				'cf',
 				'cf2',

diff --git a/ConfigKeys.php b/ConfigKeys.php
--- a/ConfigKeys.php
+++ b/ConfigKeys.php
@@ -1218,6 +1218,7 @@
 		'enum'    => array(
 			'',
 			'azure',
+			'azure_gcc_high',
 			'azuremi',
 			'bunnycdn',
 			'cf',

diff --git a/Generic_AdminActions_Default.php b/Generic_AdminActions_Default.php
--- a/Generic_AdminActions_Default.php
+++ b/Generic_AdminActions_Default.php
@@ -622,6 +622,7 @@
 
 			switch ( $this->_config->get_string( 'cdn.engine' ) ) {
 				case 'azure':
+				case 'azure_gcc_high':
 					$w3tc_config->set( 'cdn.azure.cname', $cdn_domains );
 					break;
 

diff --git a/inc/options/cdn/azure_gcc_high.php b/inc/options/cdn/azure_gcc_high.php
new file mode 100644
--- /dev/null
+++ b/inc/options/cdn/azure_gcc_high.php
@@ -1,0 +1,138 @@
+<?php
+/**
+ * File: azure_gcc_high.php
+ *
+ * @package W3TC
+ */
+
+namespace W3TC;
+
+defined( 'ABSPATH' ) || exit;
+if ( ! defined( 'W3TC' ) ) {
+	die();
+}
+?>
+<tr>
+	<th style="width: 300px;"><label for="cdn_azure_user"><?php esc_html_e( 'Account name:', 'w3-total-cache' ); ?></label></th>
+	<td>
+		<input id="cdn_azure_user" class="w3tc-ignore-change" type="text"
+			<?php Util_Ui::sealing_disabled( 'cdn.' ); ?> name="cdn__azure__user" value="<?php echo esc_attr( $this->_config->get_string( 'cdn.azure.user' ) ); ?>" size="30" />
+	</td>
+</tr>
+<tr>
+	<th><label for="cdn_azure_key"><?php esc_html_e( 'Account key:', 'w3-total-cache' ); ?></label></th>
+	<td>
+		<?php
+		Util_Ui::secret_input(
+			array(
+				'id'          => 'cdn_azure_key',
+				'name'        => 'cdn__azure__key',
+				'has_value'   => '' !== $this->_config->get_string( 'cdn.azure.key' ),
+				'size'        => 60,
+				'sealing_key' => 'cdn.',
+			)
+		);
+		?>
+	</td>
+</tr>
+<tr>
+	<th><label for="cdn_azure_container"><?php esc_html_e( 'Container:', 'w3-total-cache' ); ?></label></th>
+	<td>
+		<input id="cdn_azure_container" type="text"
+			<?php Util_Ui::sealing_disabled( 'cdn.' ); ?> name="cdn__azure__container" value="<?php echo esc_attr( $this->_config->get_string( 'cdn.azure.container' ) ); ?>" size="30" />
+		<input id="cdn_create_container" <?php Util_Ui::sealing_disabled( 'cdn.' ); ?> class="button {type: 'azure_gcc_high', nonce: '<?php echo esc_attr( Util_Nonce::create_admin( 'w3tc_cdn_create_container' ) ); ?>'}" type="button" value="<?php esc_attr_e( 'Create container', 'w3-total-cache' ); ?>" />
+		<span id="cdn_create_container_status" class="w3tc-status w3tc-process"></span>
+	</td>
+</tr>
+<tr>
+	<th>
+		<label for="cdn_azure_ssl">
+			<?php
+			echo wp_kses(
+				sprintf(
+					// translators: 1 opening HTML acronym tag, 2 closing HTML acronym tag.
+					__(
+						'%1$sSSL%2$s support:',
+						'w3-total-cache'
+					),
+					'<acronym title="' . __( 'Secure Sockets Layer', 'w3-total-cache' ) . '">',
+					'</acronym>'
+				),
+				array(
+					'acronym' => array(
+						'title' => array(),
+					),
+				)
+			);
+			?>
+		</label>
+	</th>
+	<td>
+		<select id="cdn_azure_ssl" name="cdn__azure__ssl" <?php Util_Ui::sealing_disabled( 'cdn.' ); ?>>
+			<option value="auto"<?php selected( $this->_config->get_string( 'cdn.azure.ssl' ), 'auto' ); ?>><?php esc_html_e( 'Auto (determine connection type automatically)', 'w3-total-cache' ); ?></option>
+			<option value="enabled"<?php selected( $this->_config->get_string( 'cdn.azure.ssl' ), 'enabled' ); ?>><?php esc_html_e( 'Enabled (always use SSL)', 'w3-total-cache' ); ?></option>
+			<option value="disabled"<?php selected( $this->_config->get_string( 'cdn.azure.ssl' ), 'disabled' ); ?>><?php esc_html_e( 'Disabled (always use HTTP)', 'w3-total-cache' ); ?></option>
+		</select>
+		<p class="description">
+			<?php
+			echo wp_kses(
+				sprintf(
+					// translators: 1 opening HTML acronym tag, 2 closing HTML acronym tag,
+					// translators: 3 opening HTML acronym tag, 4 closing HTML acronym tag.
+					__(
+						'Some %1$sCDN%2$s providers may or may not support %3$sSSL%4$s, contact your vendor for more information.',
+						'w3-total-cache'
+					),
+					'<acronym title="' . __( 'Content Delivery Network', 'w3-total-cache' ) . '">',
+					'</acronym>',
+					'<acronym title="' . __( 'Secure Sockets Layer', 'w3-total-cache' ) . '">',
+					'</acronym>'
+				),
+				array(
+					'acronym' => array(
+						'title' => array(),
+					),
+				)
+			);
+			?>
+		</p>
+	</td>
+</tr>
+<tr>
+	<th><?php esc_html_e( 'Replace site\'s hostname with:', 'w3-total-cache' ); ?></th>
+	<td>
+		<?php
+		$w3tc_cdn_azure_user = $this->_config->get_string( 'cdn.azure.user' );
+		if ( '' !== $w3tc_cdn_azure_user ) {
+			echo esc_attr( $w3tc_cdn_azure_user ) . '.blob.core.usgovcloudapi.net';
+		} else {
+			echo '&lt;account name&gt;.blob.core.usgovcloudapi.net';
+		}
+
+		echo wp_kses(
+			sprintf(
+				// translators: 1 opening HTML acronym tag, 2 closing HTML acronym tag.
+				__(
+					' or %1$sCNAME%2$s:',
+					'w3-total-cache'
+				),
+				'<acronym title="' . __( 'Canonical Name', 'w3-total-cache' ) . '">',
+				'</acronym>'
+			),
+			array(
+				'acronym' => array(
+					'title' => array(),
+				),
+			)
+		);
+
+		$w3tc_cnames = $this->_config->get_array( 'cdn.azure.cname' );
+		require W3TC_INC_DIR . '/options/cdn/common/cnames.php';
+		?>
+	</td>
+</tr>
+<tr>
+	<th colspan="2">
+		<input id="cdn_test" class="button {type: 'azure_gcc_high', nonce: '<?php echo esc_attr( Util_Nonce::create_admin( 'w3tc_cdn_test' ) ); ?>'}" type="button" value="<?php esc_attr_e( 'Test Microsoft Azure Storage upload', 'w3-total-cache' ); ?>" /> <span id="cdn_test_status" class="w3tc-status w3tc-process"></span>
+	</th>
+</tr>

diff --git a/pub/js/options.js b/pub/js/options.js
--- a/pub/js/options.js
+++ b/pub/js/options.js
@@ -1283,6 +1283,19 @@
         }
         break;
 
+      case "azure_gcc_high":
+        jQuery.extend(params, {
+          engine: "azure_gcc_high",
+          "config[user]": jQuery("#cdn_azure_user").val(),
+          "config[key]": jQuery("#cdn_azure_key").val(),
+          "config[container]": jQuery("#cdn_azure_container").val(),
+        });
+
+        if (cnames.length) {
+          params["config[cname][]"] = cnames;
+        }
+        break;
+
       case "azuremi":
         jQuery.extend(params, {
           engine: "azuremi",
@@ -1426,6 +1439,19 @@
         }
         break;
 
+      case "azure_gcc_high":
+        jQuery.extend(params, {
+          engine: "azure_gcc_high",
+          "config[user]": jQuery("#cdn_azure_user").val(),
+          "config[key]": jQuery("#cdn_azure_key").val(),
+          "config[container]": jQuery("#cdn_azure_container").val(),
+        });
+
+        if (cnames.length) {
+          params["config[cname][]"] = cnames;
+        }
+        break;
+
       case "azuremi":
         jQuery.extend(params, {
           engine: "azuremi",

You can send follow-ups to the cloud agent here.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 174a040. Configure here.

Comment thread CdnEngine.php

case 'azure_gcc_high':
$instances[ $instance_key ] = new CdnEngine_Azure_GCC_High( $w3tc_config );
break;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete GCC High engine wiring

High Severity

The azure_gcc_high CDN engine was added to CdnEngine::instance() but isn't fully integrated into the CDN stack. It's missing from key areas like Cdn_Util::is_engine(), the engine dropdown, Cdn_Core::get_cdn() config mapping, can_purge(), create-container handling, and authorization checks. This prevents UI selection, settings partial loading, and providing a valid config for runtime uploads.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 174a040. Configure here.

<td>
<input id="cdn_azure_container" type="text"
<?php Util_Ui::sealing_disabled( 'cdn.' ); ?> name="cdn__azure__container" value="<?php echo esc_attr( $this->_config->get_string( 'cdn.azure.container' ) ); ?>" size="30" />
<input id="cdn_create_container" <?php Util_Ui::sealing_disabled( 'cdn.' ); ?> class="button {type: 'azure', nonce: '<?php echo esc_attr( wp_create_nonce( 'w3tc' ) ); ?>'}" type="button" value="<?php esc_attr_e( 'Create container', 'w3-total-cache' ); ?>" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Admin actions hit commercial Azure

High Severity

The Create container and Test buttons in the Azure GCC High settings are hardcoded to type: 'azure'. This directs requests to the commercial Azure CDN engine and endpoint, causing GCC High credentials to be used against the wrong cloud. This results in incorrect validation or container creation.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 174a040. Configure here.

<td>
<input id="cdn_azure_container" type="text"
<?php Util_Ui::sealing_disabled( 'cdn.' ); ?> name="cdn__azure__container" value="<?php echo esc_attr( $this->_config->get_string( 'cdn.azure.container' ) ); ?>" size="30" />
<input id="cdn_create_container" <?php Util_Ui::sealing_disabled( 'cdn.' ); ?> class="button {type: 'azure', nonce: '<?php echo esc_attr( wp_create_nonce( 'w3tc' ) ); ?>'}" type="button" value="<?php esc_attr_e( 'Create container', 'w3-total-cache' ); ?>" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid nonces on admin buttons

High Severity

The "Create container" and "Test" buttons in the Azure CDN settings generate a generic w3tc nonce. The admin dispatcher's nonce verification expects specific, per-action nonces, causing these AJAX actions to fail.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 174a040. Configure here.

<th><label for="cdn_azure_key"><?php esc_html_e( 'Account key:', 'w3-total-cache' ); ?></label></th>
<td>
<input id="cdn_azure_key" class="w3tc-ignore-change"
<?php Util_Ui::sealing_disabled( 'cdn.' ); ?> type="password" name="cdn__azure__key" value="<?php echo esc_attr( $this->_config->get_string( 'cdn.azure.key' ) ); ?>" size="60" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Account key exposed in HTML

Medium Severity

The Azure account key is directly rendered in the input's value attribute, exposing the sensitive secret in the page source. This deviates from the Util_Ui::secret_input() pattern used for other secrets.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 174a040. Configure here.

if ( '' !== $cdn_azure_user ) {
echo esc_attr( $cdn_azure_user ) . '.blob.core.usgovcloudapi.net';
} else {
echo '<account name>.blob.core.windows.net';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong default hostname placeholder

Low Severity

On the Azure GCC High CDN settings page, the hostname hint defaults to the commercial Azure endpoint (.blob.core.windows.net) when the account name is empty. This is misleading, as the correct GCC High endpoint (.blob.core.usgovcloudapi.net) is only displayed after an account name is provided.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 174a040. Configure here.

Comment thread CdnEngine.php

case 'azure_gcc_high':
$instances[ $instance_key ] = new CdnEngine_Azure_GCC_High( $w3tc_config );
break;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Engine slug rejected by config enum

High Severity

cdn.engine in ConfigKeys.php has an allowlist that omits azure_gcc_high. Config::enforce_enum() rejects that value on save and keeps the prior or default engine. Even with UI wiring fixed, the GCC High selection cannot persist.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 174a040. Configure here.

if ( in_array( $this->_config['container'], (array) $containers ) ) {
$error = sprintf( 'Container already exists: %s.', $this->_config['container'] );
throw new \Exception( $error );
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Broken container existence check

Medium Severity

create_container() casts the listContainers() result object with (array) and runs in_array on it. That never matches container names. The same class’s test() method correctly iterates getContainers(). Duplicate creates are not detected before calling the Azure API.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 174a040. Configure here.

);

$cnames = $this->_config->get_array( 'cdn.azure.cname' );
require W3TC_INC_DIR . '/options/cdn/common/cnames.php';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong CNAME variable breaks options

High Severity

The options partial assigns CNAMEs to $cnames, but common/cnames.php reads $w3tc_cnames. That leaves the expected variable undefined, so the CNAME UI errors (and can TypeError on PHP 8 count()) instead of rendering saved hostnames.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 174a040. Configure here.

if ( '' !== $cdn_azure_user ) {
echo esc_attr( $cdn_azure_user ) . '.blob.core.usgovcloudapi.net';
} else {
echo '<account name>.blob.core.windows.net';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Placeholder parsed as HTML tag

Low Severity

The empty-account hostname hint echoes raw &lt;account name&gt; angle brackets, so the browser treats it as an HTML tag and the intended placeholder text does not display. Sibling Azure options escape it as &amp;lt;account name&amp;gt;.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 174a040. Configure here.

@cursor

cursor Bot commented Aug 3, 2026

Copy link
Copy Markdown

The push command requires write access to the repository.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant