Add Azure GCC High as CDN Engine Option - #1372
Conversation
Var changed since I last looked at adding this
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 9 potential issues.
Done
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 '<account name>.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.
|
|
||
| case 'azure_gcc_high': | ||
| $instances[ $instance_key ] = new CdnEngine_Azure_GCC_High( $w3tc_config ); | ||
| break; |
There was a problem hiding this comment.
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.
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' ); ?>" /> |
There was a problem hiding this comment.
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.
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' ); ?>" /> |
There was a problem hiding this comment.
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.
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" /> |
There was a problem hiding this comment.
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.
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'; |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 174a040. Configure here.
|
|
||
| case 'azure_gcc_high': | ||
| $instances[ $instance_key ] = new CdnEngine_Azure_GCC_High( $w3tc_config ); | ||
| break; |
There was a problem hiding this comment.
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.
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 ); | ||
| } |
There was a problem hiding this comment.
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.
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'; |
There was a problem hiding this comment.
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.
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'; |
There was a problem hiding this comment.
Placeholder parsed as HTML tag
Low Severity
The empty-account hostname hint echoes raw <account name> 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 &lt;account name&gt;.
Reviewed by Cursor Bugbot for commit 174a040. Configure here.
|
The push command requires write access to the repository. |



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.
CdnEnginenow maps engine idazure_gcc_highto a newCdnEngine_Azure_GCC_Highclass. 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 tocore.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.phpexposes 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.