Commit inicial - WordPress Análisis de Precios Unitarios

- WordPress core y plugins
- Tema Twenty Twenty-Four configurado
- Plugin allow-unfiltered-html.php simplificado
- .gitignore configurado para excluir wp-config.php y uploads

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-11-03 21:04:30 -06:00
commit a22573bf0b
24068 changed files with 4993111 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
# Contributor Guide
## Project Overview
This project is a WordPress plugin designed to enhance website performance through caching and other optimization techniques.
## Coding Standards
- Follow the coding standards defined in the ./phpcs.xml file.
- This is a WordPress plugin, so the coding standards must adhere to the WordPress coding standards.
- This plugin must be compatible with PHP 7.2.5 through 8.3, as defined in the main plugin file "w3-total-cache.php" and "readme.txt".
- This plugin must be compatible with WordPress 5.3 and up, as defined in the main plugin file "w3-total-cache.php" and "readme.txt".
- Do not use spaces for indentation; use 4-space tabs instead.
- Use single quotes for strings unless double quotes are necessary (e.g., when using variables inside the string).
- Do not make coding standards changes in changed files unless it is directly related to the functionality being modified.
- Opening parenthesis of a multi-line function call must be the last content on the line (PEAR.Functions.FunctionCallSignature.ContentAfterOpenBracket).
- Prefix all global namespace functions with a backslash.
## References
- WordPress Coding Standards: https://developer.wordpress.org/coding-standards/
- WordPress Coding Standards for PHP: https://developer.wordpress.org/coding-standards/php/
- WordPress Coding Standards for JavaScript: https://developer.wordpress.org/coding-standards/javascript/
- WordPress Coding Standards for HTML: https://developer.wordpress.org/coding-standards/html/
- WordPress Coding Standards for CSS: https://developer.wordpress.org/coding-standards/css/
- WordPress Coding Standards for Accessibility: https://developer.wordpress.org/coding-standards/accessibility
- WordPress Documentation Standards for PHP: https://developer.wordpress.org/coding-standards/inline-documentation-standards/php/
- WordPress Documentation Standards for JavaScript: https://developer.wordpress.org/coding-standards/inline-documentation-standards/javascript/
## Contribution Process
- Add `@since X.X.X` to all new doc blocks -- it's updated in our build process.
- Do not update POT files -- it's done in our build process.
- Do not change the `readme.txt` file -- it's done on release branches.
- Do not increment the plugin version number -- it's done in our build process.
- All changes must be submitted via pull requests.
- Public-facing work may originate from GitHub issues to ensure public visibility.
- Create a GitHub issue describing the change, implement the change in a branch, and open a pull request that references the GitHub issue.
- Internal work may originate from JIRA issues for internal tracking.
- Create a JIRA issue, implement the change in a branch, and open a pull request that references the JIRA issue.
- Ensure each pull request references its originating issue (GitHub or JIRA) and includes a clear description of the change.
## Dependency Management
- Use `yarn run upgrade:deps` to refresh JS packages and Composer libraries in one step; this enforces the PHP 7.2.58.3 constraint declared in `composer.json`.
- When running Composer directly, keep `composer update --with-all-dependencies` targeted at the repo root so the generated lock file honors the configured PHP platform (7.2.5).

View File

@@ -0,0 +1,300 @@
<?php
/**
* File: Base_Page_Settings.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class: Base_Page_Settings
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter
*/
class Base_Page_Settings {
/**
* Config
*
* @var Config
*/
protected $_config = null;
/**
* Notes
*
* @var array
*/
protected $_notes = array();
/**
* Errors
*
* @var array
*/
protected $_errors = array();
/**
* Used in PHPMailer init function
*
* @var string
*/
protected $_phpmailer_sender = '';
/**
* Master configuration
*
* @var Config
*/
protected $_config_master;
/**
* Page
*
* @var number
*/
protected $_page;
/**
* Constructor.
*
* @return void
*/
public function __construct() {
$this->_config = Dispatcher::config();
$this->_config_master = Dispatcher::config_master();
$this->_page = Util_Admin::get_current_page();
}
/**
* Render header.
*
* @return void
*/
public function options() {
$this->view();
}
/**
* Render footer.
*
* @return void
*/
public function render_footer() {
include W3TC_INC_OPTIONS_DIR . '/common/footer.php';
}
/**
* Returns true if config section is sealed.
*
* @param string $section Config section.
*
* @return boolean
*/
protected function is_sealed( $section ) {
return true;
}
/**
* Returns true if we edit master config.
*
* @return boolean
*/
protected function is_master() {
return $this->_config->is_master();
}
/**
* Prints checkbox with config option value.
*
* @param string $option_id Option ID.
* @param bool $disabled Disabled flag.
* @param string $class_prefix Class prefix.
* @param bool $label Label.
* @param bool $force_value Override value.
*
* @return void
*/
protected function checkbox( $option_id, $disabled = false, $class_prefix = '', $label = true, $force_value = null ) {
$disabled = $disabled || $this->_config->is_sealed( $option_id );
$name = Util_Ui::config_key_to_http_name( $option_id );
if ( ! $disabled ) {
echo '<input type="hidden" name="' . esc_attr( $name ) . '" value="0" />';
}
if ( $label ) {
echo '<label>';
}
echo '<input class="' . esc_attr( $class_prefix ) . 'enabled" type="checkbox" id="' . esc_attr( $name ) . '" name="' . esc_attr( $name ) . '" value="1" ';
if ( ! is_null( $force_value ) ) {
checked( $force_value, true );
} elseif ( 'cdn.flush_manually' === $option_id ) {
checked(
$this->_config->get_boolean(
$option_id,
Cdn_Util::get_flush_manually_default_override( $this->_config->get_string( 'cdn.engine' ) )
),
true
);
} else {
checked( $this->_config->get_boolean( $option_id ), true );
}
if ( $disabled ) {
echo 'disabled="disabled" ';
}
echo ' />';
}
/**
* Prints a radio button and if config value matches value
*
* @param string $option_id Option id.
* @param unknown $value Value.
* @param bool $disabled Disabled flag.
* @param string $class_prefix Class prefix.
*
* @return void
*/
protected function radio( $option_id, $value, $disabled = false, $class_prefix = '' ) {
if ( is_bool( $value ) ) {
$r_value = $value ? '1' : '0';
} else {
$r_value = $value;
}
$disabled = $disabled || $this->_config->is_sealed( $option_id );
$name = Util_Ui::config_key_to_http_name( $option_id );
echo '<label>';
echo '<input class="' . esc_attr( $class_prefix ) . 'enabled" type="radio" id="' . esc_attr( $name ) . '" name="' . esc_attr( $name ) . '" value="' . esc_attr( $r_value ) . '" ';
checked( $this->_config->get_boolean( $option_id ), $value );
if ( $disabled ) {
echo 'disabled="disabled" ';
}
echo ' />';
}
/**
* Prints checkbox for debug option.
*
* @param string $option_id Option ID.
*
* @return void
*/
protected function checkbox_debug( $option_id ) {
if ( is_array( $option_id ) ) {
$section = $option_id[0];
$section_enabled = $this->_config->is_extension_active_frontend( $section );
} else {
$section = substr( $option_id, 0, strrpos( $option_id, '.' ) );
$section_enabled = $this->_config->get_boolean( $section . '.enabled' );
}
$disabled = $this->_config->is_sealed( $option_id ) || ! $section_enabled;
$name = Util_Ui::config_key_to_http_name( $option_id );
if ( ! $disabled ) {
echo '<input type="hidden" name="' . esc_attr( $name ) . '" value="0" />';
}
echo '<label>';
echo '<input class="enabled" type="checkbox" id="' . esc_attr( $name ) . '" name="' . esc_attr( $name ) . '" value="1" ';
checked( $this->_config->get_boolean( $option_id ) && $section_enabled, true );
if ( $disabled ) {
echo 'disabled="disabled" ';
}
echo ' />';
}
/**
* Prints checkbox for debug option for pro.
*
* @param string $option_id Option ID.
* @param unknown $label Label.
* @param unknown $label_pro Pro label.
*
* @return void
*/
protected function checkbox_debug_pro( $option_id, $label, $label_pro ) {
if ( is_array( $option_id ) ) {
$section = $option_id[0];
$section_enabled = $this->_config->is_extension_active_frontend( $section );
} else {
$section = substr( $option_id, 0, strrpos( $option_id, '.' ) );
$section_enabled = $this->_config->get_boolean( $section . '.enabled' );
}
$is_pro = Util_Environment::is_w3tc_pro( $this->_config );
$disabled = $this->_config->is_sealed( $option_id ) || ! $section_enabled || ! $is_pro;
$name = Util_Ui::config_key_to_http_name( $option_id );
if ( ! $disabled ) {
echo '<input type="hidden" name="' . esc_attr( $name ) . '" value="0" />';
}
echo '<label>';
echo '<input class="enabled" type="checkbox" id="' . esc_attr( $name ) . '" name="' . esc_attr( $name ) . '" value="1" ';
checked( $this->_config->get_boolean( $option_id ) && $is_pro, true );
if ( $disabled ) {
echo 'disabled="disabled" ';
}
echo ' />';
echo esc_html( $label );
if ( $is_pro ) {
echo wp_kses(
$label_pro,
array(
'a' => array(
'href' => array(),
'id' => array(),
'class' => array(),
),
)
);
}
echo '</label>';
}
/**
* Prints checkbox for debug option for pro.
*
* @param string $option_id Option ID.
* @param bool $disabled Disabled flag.
* @param unknown $value_when_disabled Override value when disabled.
*
* @return void
*/
protected function value_with_disabled( $option_id, $disabled, $value_when_disabled ) {
if ( $disabled ) {
echo 'value="' . esc_attr( $value_when_disabled ) . '" disabled="disabled" ';
} else {
echo 'value="' . esc_attr( $this->_config->get_string( $option_id ) ) . '" ';
}
}
/**
* Render header.
*
* @return void
*/
protected function view() {
include W3TC_INC_DIR . '/options/common/header.php';
}
}

View File

@@ -0,0 +1,129 @@
<?php
/**
* File: BrowserCache_ConfigLabels.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class: BrowserCache_ConfigLabels
*/
class BrowserCache_ConfigLabels {
/**
* Get config labels
*
* @param array $config_labels Config labels.
*
* @return array
*/
public function config_labels( $config_labels ) {
return array_merge(
$config_labels,
array(
'browsercache.enabled' => __( 'Browser Cache:', 'w3-total-cache' ),
'browsercache.replace.exceptions' => __( 'Prevent caching exception list:', 'w3-total-cache' ),
'browsercache.no404wp' => __( 'Do not process 404 errors for static objects with WordPress', 'w3-total-cache' ),
'browsercache.no404wp.exceptions' => __( '404 error exception list:', 'w3-total-cache' ),
'browsercache.cssjs.last_modified' => __( 'Set Last-Modified header', 'w3-total-cache' ),
'browsercache.cssjs.expires' => __( 'Set expires header', 'w3-total-cache' ),
'browsercache.cssjs.lifetime' => __( 'Expires header lifetime:', 'w3-total-cache' ),
'browsercache.cssjs.cache.control' => __( 'Set cache control header', 'w3-total-cache' ),
'browsercache.cssjs.cache.policy' => __( 'Cache Control policy:', 'w3-total-cache' ),
'browsercache.cssjs.etag' => __( 'Set entity tag (eTag)', 'w3-total-cache' ),
'browsercache.cssjs.w3tc' => __( 'Set W3 Total Cache header', 'w3-total-cache' ),
'browsercache.cssjs.compression' => __( 'Enable <acronym title="Hypertext Transfer Protocol">HTTP</acronym> (gzip) compression', 'w3-total-cache' ),
'browsercache.cssjs.brotli' => __( 'Enable <acronym title="Hypertext Transfer Protocol">HTTP</acronym> (brotli) compression', 'w3-total-cache' ),
'browsercache.cssjs.replace' => __( 'Prevent caching of objects after settings change', 'w3-total-cache' ),
'browsercache.cssjs.nocookies' => __( 'Disable cookies for static files', 'w3-total-cache' ),
'browsercache.html.last_modified' => __( 'Set Last-Modified header', 'w3-total-cache' ),
'browsercache.html.expires' => __( 'Set expires header', 'w3-total-cache' ),
'browsercache.html.lifetime' => __( 'Expires header lifetime:', 'w3-total-cache' ),
'browsercache.html.cache.control' => __( 'Set cache control header', 'w3-total-cache' ),
'browsercache.html.cache.policy' => __( 'Cache Control policy:', 'w3-total-cache' ),
'browsercache.html.etag' => __( 'Set entity tag (ETag)', 'w3-total-cache' ),
'browsercache.html.w3tc' => __( 'Set W3 Total Cache header', 'w3-total-cache' ),
'browsercache.html.compression' => __( 'Enable <acronym title="Hypertext Transfer Protocol">HTTP</acronym> (gzip) compression', 'w3-total-cache' ),
'browsercache.html.brotli' => __( 'Enable <acronym title="Hypertext Transfer Protocol">HTTP</acronym> (brotli) compression', 'w3-total-cache' ),
'browsercache.other.last_modified' => __( 'Set Last-Modified header', 'w3-total-cache' ),
'browsercache.other.expires' => __( 'Set expires header', 'w3-total-cache' ),
'browsercache.other.lifetime' => __( 'Expires header lifetime:', 'w3-total-cache' ),
'browsercache.other.cache.control' => __( 'Set cache control header', 'w3-total-cache' ),
'browsercache.other.cache.policy' => __( 'Cache Control policy:', 'w3-total-cache' ),
'browsercache.other.etag' => __( 'Set entity tag (ETag)', 'w3-total-cache' ),
'browsercache.other.w3tc' => __( 'Set W3 Total Cache header', 'w3-total-cache' ),
'browsercache.other.compression' => __( 'Enable <acronym title="Hypertext Transfer Protocol">HTTP</acronym> (gzip) compression', 'w3-total-cache' ),
'browsercache.other.brotli' => __( 'Enable <acronym title="Hypertext Transfer Protocol">HTTP</acronym> (brotli) compression', 'w3-total-cache' ),
'browsercache.other.replace' => __( 'Prevent caching of objects after settings change', 'w3-total-cache' ),
'browsercache.other.nocookies' => __( 'Disable cookies for static files', 'w3-total-cache' ),
'browsercache.security.session.cookie_httponly' => __( 'Access session cookies through the <acronym title="Hypertext Transfer Protocol">HTTP</acronym> only:', 'w3-total-cache' ),
'browsercache.security.session.cookie_secure' => __( 'Send session cookies only to secure connections:', 'w3-total-cache' ),
'browsercache.security.session.use_only_cookies' => __( 'Use cookies to store session IDs:', 'w3-total-cache' ),
'browsercache.hsts' => __( '<acronym title="Hypertext Transfer Protocol">HTTP</acronym> Strict Transport Security policy', 'w3-total-cache' ),
'browsercache.security.hsts.directive' => __( 'Directive:', 'w3-total-cache' ),
'browsercache.security.xfo' => __( 'X-Frame-Options', 'w3-total-cache' ),
'browsercache.security.xfo.directive' => __( 'Directive:', 'w3-total-cache' ),
'browsercache.security.xss' => __( 'X-<acronym title="Cross-Site Scripting">XSS</acronym>-Protection', 'w3-total-cache' ),
'browsercache.security.xss.directive' => __( 'Directive:', 'w3-total-cache' ),
'browsercache.security.xcto' => __( 'X-Content-Type-Options', 'w3-total-cache' ),
'browsercache.security.pkp' => __( '<acronym title="Hypertext Transfer Protocol">HTTP</acronym> Public Key Pinning', 'w3-total-cache' ),
'browsercache.security.pkp.pin' => __( 'Public Key:', 'w3-total-cache' ),
'browsercache.security.pkp.pin.backup' => __( 'Public Key (Backup):', 'w3-total-cache' ),
'browsercache.security.pkp.extra' => __( 'Extra Parameters:', 'w3-total-cache' ),
'browsercache.security.pkp.report.url' => __( 'Report <acronym title="Uniform Resource Locator">URL</acronym>:', 'w3-total-cache' ),
'browsercache.security.pkp.report.only' => __( 'Report Mode Only:', 'w3-total-cache' ),
'browsercache.security.referrer.policy' => __( 'Referrer Policy', 'w3-total-cache' ),
'browsercache.security.referrer.policy.directive' => __( 'Directive:', 'w3-total-cache' ),
'browsercache.security.csp' => __( 'Content Security Policy', 'w3-total-cache' ),
'browsercache.security.csp.reporturi' => __( 'report-uri:', 'w3-total-cache' ),
'browsercache.security.csp.reportto' => __( 'report-to:', 'w3-total-cache' ),
'browsercache.security.csp.base' => __( 'base-uri:', 'w3-total-cache' ),
'browsercache.security.csp.frame' => __( 'frame-src:', 'w3-total-cache' ),
'browsercache.security.csp.connect' => __( 'connect-src:', 'w3-total-cache' ),
'browsercache.security.csp.font' => __( 'font-src:', 'w3-total-cache' ),
'browsercache.security.csp.script' => __( 'script-src:', 'w3-total-cache' ),
'browsercache.security.csp.style' => __( 'style-src:', 'w3-total-cache' ),
'browsercache.security.csp.img' => __( 'img-src:', 'w3-total-cache' ),
'browsercache.security.csp.media' => __( 'media-src:', 'w3-total-cache' ),
'browsercache.security.csp.object' => __( 'object-src:', 'w3-total-cache' ),
'browsercache.security.csp.plugin' => __( 'plugin-types:', 'w3-total-cache' ),
'browsercache.security.csp.form' => __( 'form-action:', 'w3-total-cache' ),
'browsercache.security.csp.frame.ancestors' => __( 'frame-ancestors:', 'w3-total-cache' ),
'browsercache.security.csp.sandbox' => __( 'sandbox:', 'w3-total-cache' ),
'browsercache.security.csp.child' => __( 'child-src:', 'w3-total-cache' ),
'browsercache.security.csp.manifest' => __( 'manifest-src:', 'w3-total-cache' ),
'browsercache.security.csp.scriptelem' => __( 'script-src-elem:', 'w3-total-cache' ),
'browsercache.security.csp.scriptattr' => __( 'script-src-attr:', 'w3-total-cache' ),
'browsercache.security.csp.styleelem' => __( 'style-src-elem:', 'w3-total-cache' ),
'browsercache.security.csp.styleattr' => __( 'style-src-attr:', 'w3-total-cache' ),
'browsercache.security.csp.worker' => __( 'worker-src:', 'w3-total-cache' ),
'browsercache.security.csp.default' => __( 'default-src:', 'w3-total-cache' ),
'browsercache.security.cspro' => __( 'Content Security Policy Report Only', 'w3-total-cache' ),
'browsercache.security.cspro.reporturi' => __( 'report-uri:', 'w3-total-cache' ),
'browsercache.security.cspro.reportto' => __( 'report-to:', 'w3-total-cache' ),
'browsercache.security.cspro.base' => __( 'base-uri:', 'w3-total-cache' ),
'browsercache.security.cspro.frame' => __( 'frame-src:', 'w3-total-cache' ),
'browsercache.security.cspro.connect' => __( 'connect-src:', 'w3-total-cache' ),
'browsercache.security.cspro.font' => __( 'font-src:', 'w3-total-cache' ),
'browsercache.security.cspro.script' => __( 'script-src:', 'w3-total-cache' ),
'browsercache.security.cspro.style' => __( 'style-src:', 'w3-total-cache' ),
'browsercache.security.cspro.img' => __( 'img-src:', 'w3-total-cache' ),
'browsercache.security.cspro.media' => __( 'media-src:', 'w3-total-cache' ),
'browsercache.security.cspro.object' => __( 'object-src:', 'w3-total-cache' ),
'browsercache.security.cspro.plugin' => __( 'plugin-types:', 'w3-total-cache' ),
'browsercache.security.cspro.form' => __( 'form-action:', 'w3-total-cache' ),
'browsercache.security.cspro.frame.ancestors' => __( 'frame-ancestors:', 'w3-total-cache' ),
'browsercache.security.cspro.sandbox' => __( 'sandbox:', 'w3-total-cache' ),
'browsercache.security.cspro.child' => __( 'child-src:', 'w3-total-cache' ),
'browsercache.security.cspro.manifest' => __( 'manifest-src:', 'w3-total-cache' ),
'browsercache.security.cspro.scriptelem' => __( 'script-src-elem:', 'w3-total-cache' ),
'browsercache.security.cspro.scriptattr' => __( 'script-src-attr:', 'w3-total-cache' ),
'browsercache.security.cspro.styleelem' => __( 'style-src-elem:', 'w3-total-cache' ),
'browsercache.security.cspro.styleattr' => __( 'style-src-attr:', 'w3-total-cache' ),
'browsercache.security.cspro.worker' => __( 'worker-src:', 'w3-total-cache' ),
'browsercache.security.cspro.default' => __( 'default-src:', 'w3-total-cache' ),
)
);
}
}

View File

@@ -0,0 +1,135 @@
<?php
/**
* File: BrowserCache_Core.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class BrowserCache_Core
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
*/
class BrowserCache_Core {
/**
* Returns replace extensions
*
* @param Config $config Config.
*
* @return array
*/
public function get_replace_extensions( $config ) {
$types = array();
$extensions = array();
if ( $config->get_boolean( 'browsercache.cssjs.replace' ) ) {
$types = array_merge( $types, array_keys( $this->_get_cssjs_types() ) );
}
if ( $config->get_boolean( 'browsercache.html.replace' ) ) {
$types = array_merge( $types, array_keys( $this->_get_html_types() ) );
}
if ( $config->get_boolean( 'browsercache.other.replace' ) ) {
$types = array_merge( $types, array_keys( $this->_get_other_types() ) );
}
foreach ( $types as $type ) {
$extensions = array_merge( $extensions, explode( '|', $type ) );
}
return $extensions;
}
/**
* Returns replace extensions
*
* @param Config $config Config.
*
* @return array
*/
public function get_replace_querystring_extensions( $config ) {
$extensions = array();
if ( $config->get_boolean( 'browsercache.cssjs.replace' ) ) {
$this->_fill_extensions( $extensions, $this->_get_cssjs_types(), 'replace' );
}
if ( $config->get_boolean( 'browsercache.html.replace' ) ) {
$this->_fill_extensions( $extensions, $this->_get_html_types(), 'replace' );
}
if ( $config->get_boolean( 'browsercache.other.replace' ) ) {
$this->_fill_extensions( $extensions, $this->_get_other_types(), 'replace' );
}
if ( $config->get_boolean( 'browsercache.cssjs.querystring' ) ) {
$this->_fill_extensions( $extensions, $this->_get_cssjs_types(), 'querystring' );
}
if ( $config->get_boolean( 'browsercache.html.querystring' ) ) {
$this->_fill_extensions( $extensions, $this->_get_html_types(), 'querystring' );
}
if ( $config->get_boolean( 'browsercache.other.querystring' ) ) {
$this->_fill_extensions( $extensions, $this->_get_other_types(), 'querystring' );
}
return $extensions;
}
/**
* Returns replace extensions
*
* @param array $extensions Extensions.
* @param array $types Types.
* @param string $operation Operation.
*
* @return void
*/
private function _fill_extensions( &$extensions, $types, $operation ) {
foreach ( array_keys( $types ) as $type ) {
$type_extensions = explode( '|', $type );
foreach ( $type_extensions as $ext ) {
if ( ! isset( $extensions[ $ext ] ) ) {
$extensions[ $ext ] = array();
}
$extensions[ $ext ][ $operation ] = true;
}
}
}
/**
* Returns CSS/JS mime types
*
* @return array
*/
private function _get_cssjs_types() {
$mime_types = include W3TC_INC_DIR . '/mime/cssjs.php';
return $mime_types;
}
/**
* Returns HTML mime types
*
* @return array
*/
private function _get_html_types() {
$mime_types = include W3TC_INC_DIR . '/mime/html.php';
return $mime_types;
}
/**
* Returns other mime types
*
* @return array
*/
private function _get_other_types() {
$mime_types = include W3TC_INC_DIR . '/mime/other.php';
return $mime_types;
}
}

View File

@@ -0,0 +1,864 @@
<?php
/**
* File: BrowserCache_Environment.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class BrowserCache_Environment
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable Squiz.Strings.DoubleQuoteUsage.NotRequired
*/
class BrowserCache_Environment {
/**
* Constructor
*
* @return void
*/
public function __construct() {
add_filter( 'w3tc_cdn_rules_section', array( $this, 'w3tc_cdn_rules_section' ), 10, 2 );
}
/**
* Fixes environment in each wp-admin request
*
* @param Config $config Config.
* @param bool $force_all_checks Force all checks flag.
*
* @throws Util_Environment_Exceptions Environment exceptions.
*/
public function fix_on_wpadmin_request( $config, $force_all_checks ) {
$exs = new Util_Environment_Exceptions();
if ( $config->get_boolean( 'config.check' ) || $force_all_checks ) {
if ( $config->get_boolean( 'browsercache.enabled' ) ) {
$this->rules_cache_add( $config, $exs );
} else {
$this->rules_cache_remove( $exs );
}
}
if ( count( $exs->exceptions() ) > 0 ) {
throw $exs;
}
}
/**
* Fixes environment once event occurs
*
* @param Config $config Config.
* @param string $event Event.
* @param Config $old_config Old config.
*
* @throws Util_Environment_Exceptions Environment Exceptions.
*/
public function fix_on_event( $config, $event, $old_config = null ) {
}
/**
* Fixes environment after plugin deactivation
*
* @throws Util_Environment_Exceptions Environment Exceptions.
*/
public function fix_after_deactivation() {
$exs = new Util_Environment_Exceptions();
$this->rules_cache_remove( $exs );
if ( count( $exs->exceptions() ) > 0 ) {
throw $exs;
}
}
/**
* Returns required rules for module
*
* @param Config $config Config.
*
* @return array
*/
public function get_required_rules( $config ) {
if ( ! $config->get_boolean( 'browsercache.enabled' ) ) {
return array();
}
$mime_types = $this->get_mime_types();
switch ( true ) {
case Util_Environment::is_apache():
$generator_apache = new BrowserCache_Environment_Apache( $config );
$rewrite_rules = array(
array(
'filename' => Util_Rule::get_apache_rules_path(),
'content' => W3TC_MARKER_BEGIN_BROWSERCACHE_CACHE . "\n" .
$this->rules_cache_generate_apache( $config ) .
$generator_apache->rules_no404wp( $mime_types ) .
W3TC_MARKER_END_BROWSERCACHE_CACHE . "\n",
),
);
break;
case Util_Environment::is_litespeed():
$generator_litespeed = new BrowserCache_Environment_LiteSpeed( $config );
$rewrite_rules = $generator_litespeed->get_required_rules( $mime_types );
break;
case Util_Environment::is_nginx():
$generator_nginx = new BrowserCache_Environment_Nginx( $config );
$rewrite_rules = $generator_nginx->get_required_rules( $mime_types );
break;
default:
$rewrite_rules = array();
}
return $rewrite_rules;
}
/**
* Returns mime types
*
* @return array
*/
public function get_mime_types() {
$a = Util_Mime::sections_to_mime_types_map();
$other_compression = $a['other'];
unset( $other_compression['asf|asx|wax|wmv|wmx'] );
unset( $other_compression['avi'] );
unset( $other_compression['avif'] );
unset( $other_compression['avifs'] );
unset( $other_compression['divx'] );
unset( $other_compression['gif'] );
unset( $other_compression['br'] );
unset( $other_compression['gz|gzip'] );
unset( $other_compression['jpg|jpeg|jpe'] );
unset( $other_compression['mid|midi'] );
unset( $other_compression['mov|qt'] );
unset( $other_compression['mp3|m4a'] );
unset( $other_compression['mp4|m4v'] );
unset( $other_compression['ogv'] );
unset( $other_compression['mpeg|mpg|mpe'] );
unset( $other_compression['png'] );
unset( $other_compression['ra|ram'] );
unset( $other_compression['tar'] );
unset( $other_compression['webp'] );
unset( $other_compression['wma'] );
unset( $other_compression['zip'] );
$a['other_compression'] = $other_compression;
return $a;
}
/**
* Generate rules for FTP upload
*
* @param Config $config Config.
*
* @return string
*/
public function rules_cache_generate_for_ftp( $config ) {
return $this->rules_cache_generate_apache( $config );
}
/**
* Writes cache rules
*
* @param Config $config Config.
* @param array $exs Extras.
*
* @throws Util_WpFile_FilesystemOperationException FilesystemOperation Exceptions.
* With S/FTP form if it can't get the required filesystem credentials.
*/
private function rules_cache_add( $config, $exs ) {
$rules = $this->get_required_rules( $config );
foreach ( $rules as $i ) {
Util_Rule::add_rules(
$exs,
$i['filename'],
$i['content'],
W3TC_MARKER_BEGIN_BROWSERCACHE_CACHE,
W3TC_MARKER_END_BROWSERCACHE_CACHE,
array(
W3TC_MARKER_BEGIN_MINIFY_CORE => 0,
W3TC_MARKER_BEGIN_PGCACHE_CORE => 0,
W3TC_MARKER_BEGIN_WORDPRESS => 0,
W3TC_MARKER_END_PGCACHE_CACHE => strlen( W3TC_MARKER_END_PGCACHE_CACHE ) + 1,
W3TC_MARKER_END_MINIFY_CACHE => strlen( W3TC_MARKER_END_MINIFY_CACHE ) + 1,
)
);
}
}
/**
* Removes cache directives
*
* @param array $exs Extras.
*
* @throws Util_WpFile_FilesystemOperationException FilesystemOperation Exceptions.
* With S/FTP form if it can't get the required filesystem credentials.
*/
private function rules_cache_remove( $exs ) {
$filenames = array();
switch ( true ) {
case Util_Environment::is_apache():
$filenames[] = Util_Rule::get_apache_rules_path();
break;
case Util_Environment::is_litespeed():
$filenames[] = Util_Rule::get_apache_rules_path();
$filenames[] = Util_Rule::get_litespeed_rules_path();
break;
case Util_Environment::is_nginx():
$filenames[] = Util_Rule::get_nginx_rules_path();
break;
}
foreach ( $filenames as $i ) {
Util_Rule::remove_rules(
$exs,
$i,
W3TC_MARKER_BEGIN_BROWSERCACHE_CACHE,
W3TC_MARKER_END_BROWSERCACHE_CACHE
);
}
}
/**
* Returns cache rules.
*
* @param Config $config Configuration.
*
* @return string
*/
private function rules_cache_generate_apache( Config $config ): string {
$mime_types2 = $this->get_mime_types();
$cssjs_types = $mime_types2['cssjs'];
$cssjs_types = array_unique( $cssjs_types );
$html_types = $mime_types2['html'];
$other_types = $mime_types2['other'];
$other_compression_types = $mime_types2['other_compression'];
$cssjs_expires = $config->get_boolean( 'browsercache.cssjs.expires' );
$html_expires = $config->get_boolean( 'browsercache.html.expires' );
$other_expires = $config->get_boolean( 'browsercache.other.expires' );
$cssjs_lifetime = $config->get_integer( 'browsercache.cssjs.lifetime' );
$html_lifetime = $config->get_integer( 'browsercache.html.lifetime' );
$other_lifetime = $config->get_integer( 'browsercache.other.lifetime' );
$compatibility = $config->get_boolean( 'pgcache.compatibility' );
$mime_types = array();
$rules = '';
// For mod_mime and mod_expires.
if ( $cssjs_expires && $cssjs_lifetime ) {
$mime_types = array_merge( $mime_types, $cssjs_types );
}
if ( $html_expires && $html_lifetime ) {
$mime_types = array_merge( $mime_types, $html_types );
}
if ( $other_expires && $other_lifetime ) {
$mime_types = array_merge( $mime_types, $other_types );
}
// Rules for mod_mime.
if ( count( $mime_types ) ) {
$rules_mime = "<IfModule mod_mime.c>\n";
foreach ( $mime_types as $ext => $mime_type ) {
$extensions = explode( '|', $ext );
if ( ! is_array( $mime_type ) ) {
$mime_type = (array) $mime_type;
}
foreach ( $mime_type as $mime_type2 ) {
$rules_mime .= ' AddType ' . $mime_type2;
foreach ( $extensions as $extension ) {
$rules_mime .= ' .' . $extension;
}
$rules_mime .= "\n";
}
}
$rules_mime .= "</IfModule>\n";
// Rules for mod_expires.
$rules_mime .= "<IfModule mod_expires.c>\n";
$rules_mime .= " ExpiresActive On\n";
if ( $cssjs_expires && $cssjs_lifetime ) {
foreach ( $cssjs_types as $mime_type ) {
$rules_mime .= ' ExpiresByType ' . $mime_type . ' A' . $cssjs_lifetime . "\n";
}
}
if ( $html_expires && $html_lifetime ) {
foreach ( $html_types as $mime_type ) {
$rules_mime .= ' ExpiresByType ' . $mime_type . ' A' . $html_lifetime . "\n";
}
}
if ( $other_expires && $other_lifetime ) {
foreach ( $other_types as $mime_type ) {
if ( is_array( $mime_type ) ) {
foreach ( $mime_type as $mime_type2 ) {
$rules_mime .= ' ExpiresByType ' . $mime_type2 . ' A' . $other_lifetime . "\n";
}
} else {
$rules_mime .= ' ExpiresByType ' . $mime_type . ' A' . $other_lifetime . "\n";
}
}
}
$rules_mime .= "</IfModule>\n";
/**
* Filter: w3tc_browsercache_rules_apache_mime
*
* @since 2.8.0
*
* @param string $rules_mime Apache rules for MIME types.
* @return string
*/
$rules .= apply_filters( 'w3tc_browsercache_rules_apache_mime', $rules_mime );
unset( $rules_mime );
}
// For mod_brotli.
$cssjs_brotli = $config->get_boolean( 'browsercache.cssjs.brotli' );
$html_brotli = $config->get_boolean( 'browsercache.html.brotli' );
$other_brotli = $config->get_boolean( 'browsercache.other.brotli' );
if ( $cssjs_brotli || $html_brotli || $other_brotli ) {
$brotli_types = array();
if ( $cssjs_brotli ) {
$brotli_types = array_merge( $brotli_types, $cssjs_types );
}
if ( $html_brotli ) {
$brotli_types = array_merge( $brotli_types, $html_types );
}
if ( $other_brotli ) {
$brotli_types = array_merge( $brotli_types, $other_compression_types );
}
// Rules for mod_brotli.
$rules_brotli = "<IfModule mod_brotli.c>\n";
if ( version_compare( Util_Environment::get_server_version(), '2.3.7', '>=' ) ) {
$rules_brotli .= " <IfModule mod_filter.c>\n";
}
$rules_brotli .= " AddOutputFilterByType BROTLI_COMPRESS " . implode( ' ', $brotli_types ) . "\n";
$rules_brotli .= " <IfModule mod_mime.c>\n";
$rules_brotli .= " # BROTLI_COMPRESS by extension\n";
$rules_brotli .= " AddOutputFilter BROTLI_COMPRESS js css htm html xml\n";
$rules_brotli .= " </IfModule>\n";
if ( version_compare( Util_Environment::get_server_version(), '2.3.7', '>=' ) ) {
$rules_brotli .= " </IfModule>\n";
}
$rules_brotli .= "</IfModule>\n";
/**
* Filter: w3tc_browsercache_rules_apache_brotli
*
* @since 2.8.0
*
* @param string $rules_brotli Apache rules for mod_brotli.
* @return string
*/
$rules .= apply_filters( 'w3tc_browsercache_rules_apache_brotli', $rules_brotli );
unset( $rules_brotli );
}
// For mod_deflate.
$cssjs_compression = $config->get_boolean( 'browsercache.cssjs.compression' );
$html_compression = $config->get_boolean( 'browsercache.html.compression' );
$other_compression = $config->get_boolean( 'browsercache.other.compression' );
if ( $cssjs_compression || $html_compression || $other_compression ) {
$compression_types = array();
if ( $cssjs_compression ) {
$compression_types = array_merge( $compression_types, $cssjs_types );
}
if ( $html_compression ) {
$compression_types = array_merge( $compression_types, $html_types );
}
if ( $other_compression ) {
$compression_types = array_merge( $compression_types, $other_compression_types );
}
// Rules for mod_deflate.
$rules_deflate = "<IfModule mod_deflate.c>\n";
if ( $compatibility ) {
$rules_deflate .= " <IfModule mod_setenvif.c>\n";
$rules_deflate .= " BrowserMatch ^Mozilla/4 gzip-only-text/html\n";
$rules_deflate .= " BrowserMatch ^Mozilla/4\\.0[678] no-gzip\n";
$rules_deflate .= " BrowserMatch \\bMSIE !no-gzip !gzip-only-text/html\n";
$rules_deflate .= " BrowserMatch \\bMSI[E] !no-gzip !gzip-only-text/html\n";
$rules_deflate .= " </IfModule>\n";
}
if ( version_compare( Util_Environment::get_server_version(), '2.3.7', '>=' ) ) {
$rules_deflate .= " <IfModule mod_filter.c>\n";
}
$rules_deflate .= " AddOutputFilterByType DEFLATE " . implode( ' ', $compression_types ) . "\n";
$rules_deflate .= " <IfModule mod_mime.c>\n";
$rules_deflate .= " # DEFLATE by extension\n";
$rules_deflate .= " AddOutputFilter DEFLATE js css htm html xml\n";
$rules_deflate .= " </IfModule>\n";
if ( version_compare( Util_Environment::get_server_version(), '2.3.7', '>=' ) ) {
$rules_deflate .= " </IfModule>\n";
}
$rules_deflate .= "</IfModule>\n";
/**
* Filter: w3tc_browsercache_rules_apache_deflate
*
* @since 2.8.0
*
* @param string $rules_deflate Apache rules for mod_deflate.
* @return string
*/
$rules .= apply_filters( 'w3tc_browsercache_rules_apache_deflate', $rules_deflate );
unset( $rules_deflate );
}
/* Rules for MIME types CSS/JS, HTML, and other. */
/**
* Filter: w3tc_browsercache_rules_apache_cssjs
*
* @since 2.8.0
*
* @param string $this->_rules_cache_generate_apache_for_type( $config, $mime_types2['cssjs'], 'cssjs' ) Apache rules for CSS/JS MIME types.
* @return string
*/
$rules .= apply_filters( 'w3tc_browsercache_rules_apache_cssjs', $this->_rules_cache_generate_apache_for_type( $config, $mime_types2['cssjs'], 'cssjs' ) );
/**
* Filter: w3tc_browsercache_rules_apache_html
*
* @since 2.8.0
*
* @param string $this->_rules_cache_generate_apache_for_type( $config, $mime_types2['html'], 'html' ) Apache rules for HTML MIME types.
* @return string
*/
$rules .= apply_filters( 'w3tc_browsercache_rules_apache_html', $this->_rules_cache_generate_apache_for_type( $config, $mime_types2['html'], 'html' ) );
/**
* Filter: w3tc_browsercache_rules_apache_other
*
* @since 2.8.0
*
* @param string $this->_rules_cache_generate_apache_for_type( $config, $mime_types2['other'], 'other' ) Apache rules for other MIME types.
* @return string
*/
$rules .= apply_filters( 'w3tc_browsercache_rules_apache_other', $this->_rules_cache_generate_apache_for_type( $config, $mime_types2['other'], 'other' ) );
// For mod_headers.
if ( $config->get_boolean( 'browsercache.hsts' ) ||
$config->get_boolean( 'browsercache.security.xfo' ) ||
$config->get_boolean( 'browsercache.security.xss' ) ||
$config->get_boolean( 'browsercache.security.xcto' ) ||
$config->get_boolean( 'browsercache.security.pkp' ) ||
$config->get_boolean( 'browsercache.security.referrer.policy' ) ||
$config->get_boolean( 'browsercache.security.csp' ) ||
$config->get_boolean( 'browsercache.security.cspro' ) ||
$config->get_boolean( 'browsercache.security.fp' )
) {
$lifetime = $config->get_integer( 'browsercache.other.lifetime' );
// Rules for mod_headers.
$rules_headers = "<IfModule mod_headers.c>\n";
if ( $config->get_boolean( 'browsercache.hsts' ) ) {
$dir = $config->get_string( 'browsercache.security.hsts.directive' );
$rules_headers .= ' Header always set Strict-Transport-Security "max-age=' . $lifetime .
( strpos( $dir, 'inc' ) ? '; includeSubDomains' : '' ) . ( strpos( $dir, 'pre' ) ? '; preload' : '' ) . "\"\n";
}
if ( $config->get_boolean( 'browsercache.security.xfo' ) ) {
$dir = $config->get_string( 'browsercache.security.xfo.directive' );
$url = trim( $config->get_string( 'browsercache.security.xfo.allow' ) );
if ( empty( $url ) ) {
$url = Util_Environment::home_url_maybe_https();
}
$rules_headers .= ' Header always append X-Frame-Options "' .
( 'same' === $dir ? 'SAMEORIGIN' : ( 'deny' === $dir ? 'DENY' : 'ALLOW-FROM' . $url ) ) . "\"\n";
}
if ( $config->get_boolean( 'browsercache.security.xss' ) ) {
$dir = $config->get_string( 'browsercache.security.xss.directive' );
$rules_headers .= ' Header set X-XSS-Protection "' . ( 'block' === $dir ? '1; mode=block' : $dir ) . "\"\n";
}
if ( $config->get_boolean( 'browsercache.security.xcto' ) ) {
$rules_headers .= " Header set X-Content-Type-Options \"nosniff\"\n";
}
if ( $config->get_boolean( 'browsercache.security.pkp' ) ) {
$pin = trim( $config->get_string( 'browsercache.security.pkp.pin' ) );
$pinbak = trim( $config->get_string( 'browsercache.security.pkp.pin.backup' ) );
$extra = $config->get_string( 'browsercache.security.pkp.extra' );
$url = trim( $config->get_string( 'browsercache.security.pkp.report.url' ) );
$rep_only = '1' === $config->get_string( 'browsercache.security.pkp.report.only' ) ? true : false;
$rules_headers .= ' Header set ' . ( $rep_only ? 'Public-Key-Pins-Report-Only' : 'Public-Key-Pins' ) .
' "pin-sha256="$pin"; pin-sha256="$pinbak"; max-age=' . $lifetime . ( strpos( $extra, 'inc' ) ? '; includeSubDomains' : '' ) .
( ! empty( $url ) ? '; report-uri="$url"' : '' ) . "\"\n";
}
if ( $config->get_boolean( 'browsercache.security.referrer.policy' ) ) {
$dir = $config->get_string( 'browsercache.security.referrer.policy.directive' );
$rules_headers .= ' Header set Referrer-Policy "' . ( empty( $dir ) ? '' : $dir ) . "\"\n";
}
if ( $config->get_boolean( 'browsercache.security.csp' ) ) {
$base = trim( $config->get_string( 'browsercache.security.csp.base' ) );
$reporturi = trim( $config->get_string( 'browsercache.security.csp.reporturi' ) );
$reportto = trim( $config->get_string( 'browsercache.security.csp.reportto' ) );
$frame = trim( $config->get_string( 'browsercache.security.csp.frame' ) );
$connect = trim( $config->get_string( 'browsercache.security.csp.connect' ) );
$font = trim( $config->get_string( 'browsercache.security.csp.font' ) );
$script = trim( $config->get_string( 'browsercache.security.csp.script' ) );
$style = trim( $config->get_string( 'browsercache.security.csp.style' ) );
$img = trim( $config->get_string( 'browsercache.security.csp.img' ) );
$media = trim( $config->get_string( 'browsercache.security.csp.media' ) );
$object = trim( $config->get_string( 'browsercache.security.csp.object' ) );
$plugin = trim( $config->get_string( 'browsercache.security.csp.plugin' ) );
$form = trim( $config->get_string( 'browsercache.security.csp.form' ) );
$frame_ancestors = trim( $config->get_string( 'browsercache.security.csp.frame.ancestors' ) );
$sandbox = trim( $config->get_string( 'browsercache.security.csp.sandbox' ) );
$child = trim( $config->get_string( 'browsercache.security.csp.child' ) );
$manifest = trim( $config->get_string( 'browsercache.security.csp.manifest' ) );
$scriptelem = trim( $config->get_string( 'browsercache.security.csp.scriptelem' ) );
$scriptattr = trim( $config->get_string( 'browsercache.security.csp.scriptattr' ) );
$styleelem = trim( $config->get_string( 'browsercache.security.csp.styleelem' ) );
$styleattr = trim( $config->get_string( 'browsercache.security.csp.styleattr' ) );
$worker = trim( $config->get_string( 'browsercache.security.csp.worker' ) );
$default = trim( $config->get_string( 'browsercache.security.csp.default' ) );
$dir = rtrim(
( ! empty( $base ) ? "base-uri $base; " : '' ) .
( ! empty( $reporturi ) ? "report-uri $reporturi; " : '' ) .
( ! empty( $reportto ) ? "report-to $reportto; " : '' ) .
( ! empty( $frame ) ? "frame-src $frame; " : '' ) .
( ! empty( $connect ) ? "connect-src $connect; " : '' ) .
( ! empty( $font ) ? "font-src $font; " : '' ) .
( ! empty( $script ) ? "script-src $script; " : '' ) .
( ! empty( $style ) ? "style-src $style; " : '' ) .
( ! empty( $img ) ? "img-src $img; " : '' ) .
( ! empty( $media ) ? "media-src $media; " : '' ) .
( ! empty( $object ) ? "object-src $object; " : '' ) .
( ! empty( $plugin ) ? "plugin-types $plugin; " : '' ) .
( ! empty( $form ) ? "form-action $form; " : '' ) .
( ! empty( $frame_ancestors ) ? "frame-ancestors $frame_ancestors; " : '' ) .
( ! empty( $sandbox ) ? "sandbox $sandbox; " : '' ) .
( ! empty( $child ) ? "child-src $child; " : '' ) .
( ! empty( $manifest ) ? "manifest-src $manifest; " : '' ) .
( ! empty( $scriptelem ) ? "script-src-elem $scriptelem; " : '' ) .
( ! empty( $scriptattr ) ? "script-src-attr $scriptattr; " : '' ) .
( ! empty( $styleelem ) ? "style-src-elem $styleelem; " : '' ) .
( ! empty( $styleattr ) ? "style-src-attr $styleattr; " : '' ) .
( ! empty( $worker ) ? "worker-src $worker; " : '' ) .
( ! empty( $default ) ? "default-src $default;" : '' ),
'; '
);
if ( ! empty( $dir ) ) {
$rules_headers .= ' Header set Content-Security-Policy "' . $dir . "\"\n";
}
}
if ( $config->get_boolean( 'browsercache.security.cspro' ) && ( ! empty( $config->get_string( 'browsercache.security.cspro.reporturi' ) ) || ! empty( $config->get_string( 'browsercache.security.cspro.reportto' ) ) ) ) {
$base = trim( $config->get_string( 'browsercache.security.cspro.base' ) );
$reporturi = trim( $config->get_string( 'browsercache.security.cspro.reporturi' ) );
$reportto = trim( $config->get_string( 'browsercache.security.cspro.reportto' ) );
$frame = trim( $config->get_string( 'browsercache.security.cspro.frame' ) );
$connect = trim( $config->get_string( 'browsercache.security.cspro.connect' ) );
$font = trim( $config->get_string( 'browsercache.security.cspro.font' ) );
$script = trim( $config->get_string( 'browsercache.security.cspro.script' ) );
$style = trim( $config->get_string( 'browsercache.security.cspro.style' ) );
$img = trim( $config->get_string( 'browsercache.security.cspro.img' ) );
$media = trim( $config->get_string( 'browsercache.security.cspro.media' ) );
$object = trim( $config->get_string( 'browsercache.security.cspro.object' ) );
$plugin = trim( $config->get_string( 'browsercache.security.cspro.plugin' ) );
$form = trim( $config->get_string( 'browsercache.security.cspro.form' ) );
$frame_ancestors = trim( $config->get_string( 'browsercache.security.cspro.frame.ancestors' ) );
$sandbox = trim( $config->get_string( 'browsercache.security.cspro.sandbox' ) );
$child = trim( $config->get_string( 'browsercache.security.cspro.child' ) );
$manifest = trim( $config->get_string( 'browsercache.security.cspro.manifest' ) );
$scriptelem = trim( $config->get_string( 'browsercache.security.cspro.scriptelem' ) );
$scriptattr = trim( $config->get_string( 'browsercache.security.cspro.scriptattr' ) );
$styleelem = trim( $config->get_string( 'browsercache.security.cspro.styleelem' ) );
$scriptelem = trim( $config->get_string( 'browsercache.security.cspro.styleattr' ) );
$worker = trim( $config->get_string( 'browsercache.security.cspro.worker' ) );
$default = trim( $config->get_string( 'browsercache.security.cspro.default' ) );
$dir = rtrim(
( ! empty( $base ) ? "base-uri $base; " : '' ) .
( ! empty( $reporturi ) ? "report-uri $reporturi; " : '' ) .
( ! empty( $reportto ) ? "report-to $reportto; " : '' ) .
( ! empty( $frame ) ? "frame-src $frame; " : '' ) .
( ! empty( $connect ) ? "connect-src $connect; " : '' ) .
( ! empty( $font ) ? "font-src $font; " : '' ) .
( ! empty( $script ) ? "script-src $script; " : '' ) .
( ! empty( $style ) ? "style-src $style; " : '' ) .
( ! empty( $img ) ? "img-src $img; " : '' ) .
( ! empty( $media ) ? "media-src $media; " : '' ) .
( ! empty( $object ) ? "object-src $object; " : '' ) .
( ! empty( $plugin ) ? "plugin-types $plugin; " : '' ) .
( ! empty( $form ) ? "form-action $form; " : '' ) .
( ! empty( $frame_ancestors ) ? "frame-ancestors $frame_ancestors; " : '' ) .
( ! empty( $sandbox ) ? "sandbox $sandbox; " : '' ) .
( ! empty( $child ) ? "child-src $child; " : '' ) .
( ! empty( $manifest ) ? "manifest-src $manifest; " : '' ) .
( ! empty( $scriptelem ) ? "script-src-elem $scriptelem; " : '' ) .
( ! empty( $scriptattr ) ? "script-src-attr $scriptattr; " : '' ) .
( ! empty( $styleelem ) ? "style-src-elem $styleelem; " : '' ) .
( ! empty( $styleattr ) ? "style-src-attr $styleattr; " : '' ) .
( ! empty( $worker ) ? "worker-src $worker; " : '' ) .
( ! empty( $default ) ? "default-src $default;" : '' ),
'; '
);
if ( ! empty( $dir ) ) {
$rules_headers .= ' Header set Content-Security-Policy-Report-Only "' . $dir . "\"\n";
}
}
if ( $config->get_boolean( 'browsercache.security.fp' ) ) {
$fp_values = $config->get_array( 'browsercache.security.fp.values' );
$feature_v = array();
$permission_v = array();
foreach ( $fp_values as $key => $value ) {
if ( ! empty( $value ) ) {
$value = str_replace( array( '"', "'" ), '', $value );
$feature_v[] = "$key '$value'";
$permission_v[] = "$key=($value)";
}
}
if ( ! empty( $feature_v ) ) {
$rules_headers .= ' Header set Feature-Policy "' . implode( ';', $feature_v ) . "\"\n";
}
if ( ! empty( $permission_v ) ) {
$rules_headers .= ' Header set Permissions-Policy "' . implode( ',', $permission_v ) . "\"\n";
}
}
$rules_headers .= "</IfModule>\n";
/**
* Filter: w3tc_browsercache_rules_apache_headers
*
* @since 2.8.0
*
* @param string $rules_mime Apache rules for mod_headers.
* @return string
*/
$rules .= apply_filters( 'w3tc_browsercache_rules_apache_headers', $rules_headers );
unset( $rules_headers );
}
$g = new BrowserCache_Environment_Apache( $config );
/**
* Filter: w3tc_browsercache_rules_apache_rewrite
*
* @since 2.8.0
*
* @param string $rules_mime Apache rules for mod_rewrite.
* @return string
*/
$rules .= apply_filters( 'w3tc_browsercache_rules_apache_rewrite', $g->rules_rewrite() );
return apply_filters( 'w3tc_browsercache_rules_apache', $rules );
}
/**
* Writes cache rules
*
* @param Config $config Config.
* @param array $mime_types Mime types.
* @param string $section Section.
*
* @return string
*/
private function _rules_cache_generate_apache_for_type( $config, $mime_types, $section ) {
$is_disc_enhanced = $config->get_boolean( 'pgcache.enabled' ) && 'file_generic' === $config->get_string( 'pgcache.engine' );
$cache_control = $config->get_boolean( 'browsercache.' . $section . '.cache.control' );
$etag = $config->get_boolean( 'browsercache.' . $section . '.etag' );
$w3tc = $config->get_boolean( 'browsercache.' . $section . '.w3tc' );
$unset_setcookie = $config->get_boolean( 'browsercache.' . $section . '.nocookies' );
$set_last_modified = $config->get_boolean( 'browsercache.' . $section . '.last_modified' );
$compatibility = $config->get_boolean( 'pgcache.compatibility' );
$mime_types2 = apply_filters( 'w3tc_browsercache_rules_section_extensions', $mime_types, $config, $section );
$extensions = array_keys( $mime_types2 );
// Remove ext from filesmatch if its the same as permalink extension.
$pext = strtolower( pathinfo( get_option( 'permalink_structure' ), PATHINFO_EXTENSION ) );
if ( $pext ) {
$extensions = Util_Rule::remove_extension_from_list( $extensions, $pext );
}
$extensions_lowercase = array_map( 'strtolower', $extensions );
$extensions_uppercase = array_map( 'strtoupper', $extensions );
$rules = '';
$headers_rules = '';
if ( $cache_control ) {
$cache_policy = $config->get_string( 'browsercache.' . $section . '.cache.policy' );
switch ( $cache_policy ) {
case 'cache':
$headers_rules .= " Header set Pragma \"public\"\n";
$headers_rules .= " Header set Cache-Control \"public\"\n";
break;
case 'cache_public_maxage':
$expires = $config->get_boolean( 'browsercache.' . $section . '.expires' );
$lifetime = $config->get_integer( 'browsercache.' . $section . '.lifetime' );
$headers_rules .= " Header set Pragma \"public\"\n";
if ( $expires ) {
$headers_rules .= " Header append Cache-Control \"public\"\n";
} else {
$headers_rules .= " Header set Cache-Control \"max-age=" . $lifetime . ", public\"\n";
}
break;
case 'cache_validation':
$headers_rules .= " Header set Pragma \"public\"\n";
$headers_rules .= " Header set Cache-Control \"public, must-revalidate, proxy-revalidate\"\n";
break;
case 'cache_noproxy':
$headers_rules .= " Header set Pragma \"public\"\n";
$headers_rules .= " Header set Cache-Control \"private, must-revalidate\"\n";
break;
case 'cache_maxage':
$expires = $config->get_boolean( 'browsercache.' . $section . '.expires' );
$lifetime = $config->get_integer( 'browsercache.' . $section . '.lifetime' );
$headers_rules .= " Header set Pragma \"public\"\n";
if ( $expires ) {
$headers_rules .= " Header append Cache-Control \"public, must-revalidate, proxy-revalidate\"\n";
} else {
$headers_rules .= " Header set Cache-Control \"max-age=" . $lifetime . ", public, must-revalidate, proxy-revalidate\"\n";
}
break;
case 'no_cache':
$headers_rules .= " Header set Pragma \"no-cache\"\n";
$headers_rules .= " Header set Cache-Control \"private, no-cache\"\n";
break;
case 'no_store':
$headers_rules .= " Header set Pragma \"no-store\"\n";
$headers_rules .= " Header set Cache-Control \"no-store\"\n";
break;
case 'cache_immutable':
$lifetime = $config->get_integer( 'browsercache.' . $section . '.lifetime' );
$headers_rules .= " Header set Pragma \"public\"\n";
$headers_rules .= " Header set Cache-Control \"public, max-age=" . $lifetime . ", immutable\"\n";
break;
case 'cache_immutable_nomaxage':
$headers_rules .= " Header set Pragma \"public\"\n";
$headers_rules .= " Header set Cache-Control \"public, immutable\"\n";
break;
}
}
if ( $etag ) {
$rules .= " FileETag MTime Size\n";
} elseif ( $compatibility ) {
$rules .= " FileETag None\n";
$headers_rules .= " Header unset ETag\n";
}
if ( $unset_setcookie ) {
$headers_rules .= " Header unset Set-Cookie\n";
}
if ( ! $set_last_modified ) {
$headers_rules .= " Header unset Last-Modified\n";
}
if ( $w3tc ) {
$headers_rules .= " Header set X-Powered-By \"" . Util_Environment::w3tc_header() . "\"\n";
}
if ( strlen( $headers_rules ) > 0 ) {
$rules .= " <IfModule mod_headers.c>\n";
$rules .= $headers_rules;
$rules .= " </IfModule>\n";
}
if ( strlen( $rules ) > 0 ) {
$rules = "<FilesMatch \"\\.(" . implode( '|', array_merge( $extensions_lowercase, $extensions_uppercase ) ) . ")$\">\n" . $rules;
$rules .= "</FilesMatch>\n";
}
return $rules;
}
/**
* Return CDN rules section
*
* @param array $section_rules Section rules.
* @param Config $config Config.
*
* @return array
*/
public function w3tc_cdn_rules_section( $section_rules, $config ) {
if ( Util_Environment::is_litespeed() ) {
$o = new BrowserCache_Environment_LiteSpeed( $config );
$section_rules = $o->w3tc_cdn_rules_section( $section_rules );
}
return $section_rules;
}
}

View File

@@ -0,0 +1,124 @@
<?php
/**
* File: BrowserCache_Environment_Apache.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class BrowserCache_Environment_Apache
*
* phpcs:disable Squiz.Strings.DoubleQuoteUsage.NotRequired
*
* Environment (rules) generation for apache
* TODO: move all apache-specific code here from BrowserCache_Environment
*/
class BrowserCache_Environment_Apache {
/**
* Config
*
* @var Config
*/
private $c;
/**
* Constructor
*
* @param Config $config Config.
*
* @return void
*/
public function __construct( $config ) {
$this->c = $config;
}
/**
* Rules rewrite
*
* @return string
*/
public function rules_rewrite() {
if ( ! $this->c->get_boolean( 'browsercache.rewrite' ) ) {
return '';
}
$core = Dispatcher::component( 'BrowserCache_Core' );
$extensions = $core->get_replace_extensions( $this->c );
$rules = array();
$rules[] = '<IfModule mod_rewrite.c>';
$rules[] = ' RewriteCond %{REQUEST_FILENAME} !-f';
$rules[] = ' RewriteRule ^(.+)\.(x[0-9]{5})\.(' . implode( '|', $extensions ) . ')$ $1.$3 [L]';
$rules[] = '</IfModule>';
$rules[] = '';
return implode( "\n", $rules );
}
/**
* Generate rules related to prevent for media 404 error by WP
*
* @param array $mime_types Mime types.
*
* @return string
*/
public function rules_no404wp( $mime_types ) {
if ( ! $this->c->get_boolean( 'browsercache.no404wp' ) ) {
return '';
}
$cssjs_types = $mime_types['cssjs'];
$html_types = $mime_types['html'];
$other_types = $mime_types['other'];
$extensions = array_merge( array_keys( $cssjs_types ), array_keys( $html_types ), array_keys( $other_types ) );
$permalink_structure = get_option( 'permalink_structure' );
$permalink_structure_ext = ltrim( strrchr( $permalink_structure, '.' ), '.' );
if ( '' !== $permalink_structure_ext ) {
foreach ( $extensions as $index => $extension ) {
if ( strstr( $extension, $permalink_structure_ext ) !== false ) {
$extensions[ $index ] = preg_replace( '~\|?' . Util_Environment::preg_quote( $permalink_structure_ext ) . '\|?~', '', $extension );
}
}
}
$exceptions = $this->c->get_array( 'browsercache.no404wp.exceptions' );
$wp_uri = network_home_url( '', 'relative' );
$wp_uri = rtrim( $wp_uri, '/' );
$rules = '';
$rules .= "<IfModule mod_rewrite.c>\n";
$rules .= " RewriteEngine On\n";
// in subdir - rewrite theme files and similar to upper folder if file exists.
if ( Util_Environment::is_wpmu() && ! Util_Environment::is_wpmu_subdomain() ) {
$document_root = Util_Rule::apache_docroot_variable();
$rules .= " RewriteCond %{REQUEST_FILENAME} !-f\n";
$rules .= " RewriteCond %{REQUEST_FILENAME} !-d\n";
$rules .= " RewriteCond %{REQUEST_URI} ^$wp_uri/([_0-9a-zA-Z-]+/)(.*\.)(" . implode( '|', $extensions ) . ")$ [NC]\n";
$rules .= ' RewriteCond "' . $document_root . $wp_uri . '/%2%3" -f' . "\n";
$rules .= " RewriteRule .* $wp_uri/%2%3 [L]\n\n";
}
$rules .= " RewriteCond %{REQUEST_FILENAME} !-f\n";
$rules .= " RewriteCond %{REQUEST_FILENAME} !-d\n";
$imploded = implode( '|', $exceptions );
if ( ! empty( $imploded ) ) {
$rules .= " RewriteCond %{REQUEST_URI} !(" . $imploded . ")\n";
}
$rules .= " RewriteCond %{REQUEST_URI} \\.(" . implode( '|', $extensions ) . ")$ [NC]\n";
$rules .= " RewriteRule .* - [L]\n";
$rules .= "</IfModule>\n";
return $rules;
}
}

View File

@@ -0,0 +1,289 @@
<?php
/**
* File: BrowserCache_Environment_LiteSpeed.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class BrowserCache_Environment_LiteSpeed
*
* phpcs:disable Squiz.Strings.DoubleQuoteUsage.NotRequired
*
* Rules generation for OpenLiteSpeed
*
* phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter
*/
class BrowserCache_Environment_LiteSpeed {
/**
* Config
*
* @var Config
*/
private $c;
/**
* Constructor
*
* @param Config $config Config.
*
* @return void
*/
public function __construct( $config ) {
$this->c = $config;
}
/**
* Get required rules
*
* @param array $mime_types Mime types.
*
* @return string
*/
public function get_required_rules( $mime_types ) {
$rewrite_rules = array();
$rewrite_rules[] = array(
'filename' => Util_Rule::get_litespeed_rules_path(),
'content' => $this->generate( $mime_types ),
);
if ( $this->c->get_boolean( 'browsercache.rewrite' ) || $this->c->get_boolean( 'browsercache.no404wp' ) ) {
$g = new BrowserCache_Environment_Apache( $this->c );
$rewrite_rules[] = array(
'filename' => Util_Rule::get_apache_rules_path(),
'content' =>
W3TC_MARKER_BEGIN_BROWSERCACHE_CACHE . "\n" .
$g->rules_rewrite() .
$g->rules_no404wp( $mime_types ) .
W3TC_MARKER_END_BROWSERCACHE_CACHE . "\n",
);
}
return $rewrite_rules;
}
/**
* Generate cache rules
*
* @param array $mime_types Mime types.
* @param bool $cdnftp CDN FTP flag.
*
* @return array
*/
public function generate( $mime_types, $cdnftp = false ) {
$cssjs_types = $mime_types['cssjs'];
$cssjs_types = array_unique( $cssjs_types );
$html_types = $mime_types['html'];
$other_types = $mime_types['other'];
$other_compression_types = $mime_types['other_compression'];
$rules = '';
$rules .= W3TC_MARKER_BEGIN_BROWSERCACHE_CACHE . "\n";
$this->generate_section( $rules, $mime_types['cssjs'], 'cssjs' );
$this->generate_section( $rules, $mime_types['html'], 'html' );
$this->generate_section( $rules, $mime_types['other'], 'other' );
if ( $this->c->get_boolean( 'browsercache.rewrite' ) ) {
$core = Dispatcher::component( 'BrowserCache_Core' );
$extensions = $core->get_replace_extensions( $this->c );
$rules .= "<IfModule mod_rewrite.c>\n";
$rules .= " RewriteCond %{REQUEST_FILENAME} !-f\n";
$rules .= ' RewriteRule ^(.+)\.(x[0-9]{5})\.(' . implode( '|', $extensions ) . ')$ $1.$3 [L]' . "\n";
$rules .= "</IfModule>\n";
}
$rules .= W3TC_MARKER_END_BROWSERCACHE_CACHE . "\n";
return $rules;
}
/**
* Adds cache rules for type to &$rules.
*
* @param string $rules Rules.
* @param array $mime_types MIME types.
* @param string $section Section.
*
* @return void
*/
private function generate_section( &$rules, $mime_types, $section ) {
$expires = $this->c->get_boolean( 'browsercache.' . $section . '.expires' );
$cache_control = $this->c->get_boolean( 'browsercache.' . $section . '.cache.control' );
$w3tc = $this->c->get_boolean( 'browsercache.' . $section . '.w3tc' );
$last_modified = $this->c->get_boolean( 'browsercache.' . $section . '.last_modified' );
if ( $expires || $cache_control || $w3tc || ! $last_modified ) {
$mime_types2 = apply_filters(
'w3tc_browsercache_rules_section_extensions',
$mime_types,
$this->c,
$section
);
$extensions = array_keys( $mime_types2 );
// Remove ext from filesmatch if its the same as permalink extension.
$pext = strtolower( pathinfo( get_option( 'permalink_structure' ), PATHINFO_EXTENSION ) );
if ( $pext ) {
$extensions = Util_Rule::remove_extension_from_list( $extensions, $pext );
}
$extensions_string = implode( '|', $extensions );
$section_rules = self::section_rules( $section );
$section_rules = apply_filters( 'w3tc_browsercache_rules_section', $section_rules, $this->c, $section );
$context_rules = $section_rules['other'];
if ( ! empty( $section_rules['add_header'] ) ) {
$context_rules[] = " extraHeaders <<<END_extraHeaders";
foreach ( $section_rules['add_header'] as $line ) {
$context_rules[] = ' ' . $line;
}
$context_rules[] = " END_extraHeaders";
}
if ( $section_rules['rewrite'] ) {
$context_rules[] = ' rewrite {';
$context_rules[] = ' RewriteFile .htaccess';
$context_rules[] = ' }';
}
$rules .= "context exp:^.*($extensions_string)\$ {\n";
$rules .= " location \$DOC_ROOT/\$0\n";
$rules .= " allowBrowse 1\n";
$rules .= implode( "\n", $context_rules ) . "\n";
$rules .= "}\n";
}
}
/**
* Returns directives plugin applies to files of specific section * Without location
*
* @param string $section Section.
*
* @return array
*/
public function section_rules( $section ) {
$rules = array();
$expires = $this->c->get_boolean( "browsercache.$section.expires" );
$lifetime = $this->c->get_integer( "browsercache.$section.lifetime" );
if ( $expires ) {
$rules[] = ' enableExpires 1';
$rules[] = " expiresDefault A$lifetime";
$rules[] = " ExpiresByType */*=A$lifetime";
} else {
$rules[] = ' enableExpires 0';
}
/*
Lastmod support not implemented
if ( $this->c->get_boolean( "browsercache.$section.last_modified" ) )
*/
$add_header_rules = array();
if ( $this->c->get_boolean( "browsercache.$section.cache.control" ) ) {
$cache_policy = $this->c->get_string( "browsercache.$section.cache.policy" );
switch ( $cache_policy ) {
case 'cache':
$add_header_rules[] = 'unset Pragma';
$add_header_rules[] = 'set Pragma public';
$add_header_rules[] = 'set Cache-Control public';
break;
case 'cache_public_maxage':
$add_header_rules[] = 'unset Pragma';
$add_header_rules[] = 'set Pragma public';
break;
case 'cache_validation':
$add_header_rules[] = 'unset Pragma';
$add_header_rules[] = 'set Pragma public';
$add_header_rules[] = 'unset Cache-Control';
$add_header_rules[] = 'set Cache-Control "public, must-revalidate, proxy-revalidate"';
break;
case 'cache_noproxy':
$add_header_rules[] = 'unset Pragma';
$add_header_rules[] = 'set Pragma public';
$add_header_rules[] = 'unset Cache-Control';
$add_header_rules[] = 'set Cache-Control "private, must-revalidate"';
break;
case 'cache_maxage':
$add_header_rules[] = 'unset Pragma';
$add_header_rules[] = 'set Pragma "public"';
$add_header_rules[] = 'unset Cache-Control';
if ( $expires ) {
$add_header_rules[] = 'set Cache-Control "public, must-revalidate, proxy-revalidate"';
} else {
$add_header_rules[] = "set Cache-Control \"max-age=$lifetime, public, must-revalidate, proxy-revalidate\"";
}
break;
case 'no_cache':
$add_header_rules[] = 'unset Pragma';
$add_header_rules[] = 'set Pragma "no-cache"';
$add_header_rules[] = 'unset Cache-Control';
$add_header_rules[] = 'set Cache-Control "private, no-cache"';
break;
case 'no_store':
$add_header_rules[] = 'unset Pragma';
$add_header_rules[] = 'set Pragma "no-store"';
$add_header_rules[] = 'unset Cache-Control';
$add_header_rules[] = 'set Cache-Control "no-store"';
break;
case 'cache_immutable':
$add_header_rules[] = 'unset Pragma';
$add_header_rules[] = 'set Pragma public';
$add_header_rules[] = 'unset Cache-Control';
$add_header_rules[] = "set Cache-Control \"public, max-age=$lifetime, immutable\"";
break;
case 'cache_immutable_nomaxage':
$add_header_rules[] = 'unset Pragma';
$add_header_rules[] = 'set Pragma public';
$add_header_rules[] = 'unset Cache-Control';
$add_header_rules[] = 'set Cache-Control "public, immutable"';
break;
}
}
// Need htaccess for rewrites.
$rewrite = $this->c->get_boolean( 'browsercache.rewrite' );
return array(
'add_header' => $add_header_rules,
'other' => $rules,
'rewrite' => $rewrite,
);
}
/**
* Returns CDN rules section
*
* @param array $section_rules Section rules.
*
* @return array
*/
public function w3tc_cdn_rules_section( $section_rules ) {
$section_rules_bc = $this->section_rules( 'other' );
$section_rules['other'] = array_merge( $section_rules['other'], $section_rules_bc['other'] );
$section_rules['add_header'] = array_merge( $section_rules['add_header'], $section_rules_bc['add_header'] );
return $section_rules;
}
}

View File

@@ -0,0 +1,551 @@
<?php
/**
* File: BrowserCache_Environment_Nginx.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class BrowserCache_Environment_Nginx
*
* Rules generation for Nginx
*
* phpcs:disable Squiz.Strings.DoubleQuoteUsage.NotRequired
* phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter
*/
class BrowserCache_Environment_Nginx {
/**
* Config
*
* @var Config
*/
private $c;
/**
* Constructor
*
* @param Config $config Config.
*
* @return void
*/
public function __construct( $config ) {
$this->c = $config;
}
/**
* Returns required rules
*
* @param array $mime_types Mime types.
*
* @return array
*/
public function get_required_rules( $mime_types ) {
return array(
array(
'filename' => Util_Rule::get_nginx_rules_path(),
'content' => $this->generate( $mime_types ),
),
);
}
/**
* Returns cache rules
*
* @param array $mime_types Mime types.
* @param bool $cdnftp CDN FTP flag.
*
* @return string
*/
public function generate( $mime_types, $cdnftp = false ) {
$cssjs_types = $mime_types['cssjs'];
$cssjs_types = array_unique( $cssjs_types );
$html_types = $mime_types['html'];
$other_types = $mime_types['other'];
$other_compression_types = $mime_types['other_compression'];
$rules = '';
$rules .= W3TC_MARKER_BEGIN_BROWSERCACHE_CACHE . "\n";
if ( $this->c->get_boolean( 'browsercache.rewrite' ) ) {
$core = Dispatcher::component( 'BrowserCache_Core' );
$extensions = $core->get_replace_extensions( $this->c );
$exts = implode( '|', $extensions );
$rules .= "set \$w3tcbc_rewrite_filename '';\n";
$rules .= "set \$w3tcbc_rewrite_uri '';\n";
$rules .= "if (\$uri ~ '^(?<w3tcbc_base>.+)\.(x[0-9]{5})(?<w3tcbc_ext>\.($exts))$') {\n";
$rules .= " set \$w3tcbc_rewrite_filename \$document_root\$w3tcbc_base\$w3tcbc_ext;\n";
$rules .= " set \$w3tcbc_rewrite_uri \$w3tcbc_base\$w3tcbc_ext;\n";
$rules .= "}\n";
if ( Util_Environment::is_wpmu() && ! Util_Environment::is_wpmu_subdomain() ) {
// WPMU subdir extra rewrite.
if ( defined( 'W3TC_HOME_URI' ) ) {
$home_uri = W3TC_HOME_URI;
} else {
$primary_blog_id = get_network()->site_id;
$home_uri = wp_parse_url( get_home_url( $primary_blog_id ), PHP_URL_PATH );
$home_uri = rtrim( $home_uri, '/' );
}
$rules .= "if (\$uri ~ '^$home_uri/[_0-9a-zA-Z-]+(?<w3tcbc_base>/wp-.+)\.(x[0-9]{5})(?<w3tcbc_ext>\.($exts))$') {\n";
$rules .= " set \$w3tcbc_rewrite_filename \$document_root$home_uri\$w3tcbc_base\$w3tcbc_ext;\n";
$rules .= " set \$w3tcbc_rewrite_uri $home_uri\$w3tcbc_base\$w3tcbc_ext;\n";
$rules .= "}\n";
}
$rules .= "if (-f \$w3tcbc_rewrite_filename) {\n";
$rules .= " rewrite .* \$w3tcbc_rewrite_uri;\n";
$rules .= "}\n";
}
$cssjs_brotli = $this->c->get_boolean( 'browsercache.cssjs.brotli' );
$html_brotli = $this->c->get_boolean( 'browsercache.html.brotli' );
$other_brotli = $this->c->get_boolean( 'browsercache.other.brotli' );
if ( $cssjs_brotli || $html_brotli || $other_brotli ) {
$brotli_types = array();
if ( $cssjs_brotli ) {
$brotli_types = array_merge( $brotli_types, $cssjs_types );
}
if ( $html_brotli ) {
$brotli_types = array_merge( $brotli_types, $html_types );
}
if ( $other_brotli ) {
$brotli_types = array_merge( $brotli_types, $other_compression_types );
}
unset( $brotli_types['html|htm'] );
// some nginx cant handle values longer than 47 chars.
unset( $brotli_types['odp'] );
$rules .= "brotli on;\n";
$rules .= 'brotli_types ' . implode( ' ', array_unique( $brotli_types ) ) . ";\n";
}
$cssjs_compression = $this->c->get_boolean( 'browsercache.cssjs.compression' );
$html_compression = $this->c->get_boolean( 'browsercache.html.compression' );
$other_compression = $this->c->get_boolean( 'browsercache.other.compression' );
if ( $cssjs_compression || $html_compression || $other_compression ) {
$compression_types = array();
if ( $cssjs_compression ) {
$compression_types = array_merge( $compression_types, $cssjs_types );
}
if ( $html_compression ) {
$compression_types = array_merge( $compression_types, $html_types );
}
if ( $other_compression ) {
$compression_types = array_merge( $compression_types, $other_compression_types );
}
unset( $compression_types['html|htm'] );
// some nginx cant handle values longer than 47 chars.
unset( $compression_types['odp'] );
$rules .= "gzip on;\n";
$rules .= "gzip_types " . implode( ' ', array_unique( $compression_types ) ) . ";\n";
}
if ( $this->c->get_boolean( 'browsercache.no404wp' ) ) {
$exceptions = $this->c->get_array( 'browsercache.no404wp.exceptions' );
$impoloded = implode( '|', $exceptions );
if ( ! empty( $impoloded ) ) {
$wp_uri = network_home_url( '', 'relative' );
$wp_uri = rtrim( $wp_uri, '/' );
$rules .= "location ~ (" . $impoloded . ") {\n";
$rules .= ' try_files $uri $uri/ ' . $wp_uri . '/index.php?$args;' . "\n";
$rules .= "}\n";
}
}
$this->generate_section( $rules, $mime_types['cssjs'], 'cssjs' );
$this->generate_section( $rules, $mime_types['html'], 'html' );
$this->generate_section( $rules, $mime_types['other'], 'other' );
$rules .= implode( "\n", $this->security_rules() ) . "\n";
$rules .= W3TC_MARKER_END_BROWSERCACHE_CACHE . "\n";
return $rules;
}
/**
* Returns security header directives
*
* @return array
*/
private function security_rules() {
$rules = array();
if ( $this->c->get_boolean( 'browsercache.hsts' ) ||
$this->c->get_boolean( 'browsercache.security.xfo' ) ||
$this->c->get_boolean( 'browsercache.security.xss' ) ||
$this->c->get_boolean( 'browsercache.security.xcto' ) ||
$this->c->get_boolean( 'browsercache.security.pkp' ) ||
$this->c->get_boolean( 'browsercache.security.referrer.policy' ) ||
$this->c->get_boolean( 'browsercache.security.csp' ) ||
$this->c->get_boolean( 'browsercache.security.cspro' ) ||
$this->c->get_boolean( 'browsercache.security.fp' )
) {
$lifetime = $this->c->get_integer( 'browsercache.other.lifetime' );
if ( $this->c->get_boolean( 'browsercache.hsts' ) ) {
$dir = $this->c->get_string( 'browsercache.security.hsts.directive' );
$rules[] = "add_header Strict-Transport-Security \"max-age=$lifetime" . ( strpos( $dir, "inc" ) ? "; includeSubDomains" : "" ) . ( strpos( $dir, "pre" ) ? "; preload" : "" ) . "\";";
}
if ( $this->c->get_boolean( 'browsercache.security.xfo' ) ) {
$dir = $this->c->get_string( 'browsercache.security.xfo.directive' );
$url = trim( $this->c->get_string( 'browsercache.security.xfo.allow' ) );
if ( empty( $url ) ) {
$url = Util_Environment::home_url_maybe_https();
}
$rules[] = "add_header X-Frame-Options \"" . ( 'same' === $dir ? "SAMEORIGIN" : ( 'deny' === $dir ? "DENY" : "ALLOW-FROM $url" ) ) . "\";";
}
if ( $this->c->get_boolean( 'browsercache.security.xss' ) ) {
$dir = $this->c->get_string( 'browsercache.security.xss.directive' );
$rules[] = "add_header X-XSS-Protection \"" . ( 'block' === $dir ? "1; mode=block" : $dir ) . "\";";
}
if ( $this->c->get_boolean( 'browsercache.security.xcto' ) ) {
$rules[] = "add_header X-Content-Type-Options \"nosniff\";";
}
if ( $this->c->get_boolean( 'browsercache.security.pkp' ) ) {
$pin = trim( $this->c->get_string( 'browsercache.security.pkp.pin' ) );
$pinbak = trim( $this->c->get_string( 'browsercache.security.pkp.pin.backup' ) );
$extra = $this->c->get_string( 'browsercache.security.pkp.extra' );
$url = trim( $this->c->get_string( 'browsercache.security.pkp.report.url' ) );
$rep_only = '1' === $this->c->get_string( 'browsercache.security.pkp.report.only' ) ? true : false;
$rules[] = "add_header " . ( $rep_only ? "Public-Key-Pins-Report-Only" : "Public-Key-Pins" ) . " 'pin-sha256=\"$pin\"; pin-sha256=\"$pinbak\"; max-age=$lifetime" . ( strpos( $extra, "inc" ) ? "; includeSubDomains" : "" ) . ( ! empty( $url ) ? "; report-uri=\"$url\"" : "" ) . "';";
}
if ( $this->c->get_boolean( 'browsercache.security.referrer.policy' ) ) {
$dir = $this->c->get_string( 'browsercache.security.referrer.policy.directive' );
$rules[] = "add_header Referrer-Policy \"" . ( '0' === $dir ? "" : $dir ) . "\";";
}
if ( $this->c->get_boolean( 'browsercache.security.csp' ) ) {
$base = trim( $this->c->get_string( 'browsercache.security.csp.base' ) );
$frame = trim( $this->c->get_string( 'browsercache.security.csp.frame' ) );
$connect = trim( $this->c->get_string( 'browsercache.security.csp.connect' ) );
$font = trim( $this->c->get_string( 'browsercache.security.csp.font' ) );
$script = trim( $this->c->get_string( 'browsercache.security.csp.script' ) );
$style = trim( $this->c->get_string( 'browsercache.security.csp.style' ) );
$img = trim( $this->c->get_string( 'browsercache.security.csp.img' ) );
$media = trim( $this->c->get_string( 'browsercache.security.csp.media' ) );
$object = trim( $this->c->get_string( 'browsercache.security.csp.object' ) );
$plugin = trim( $this->c->get_string( 'browsercache.security.csp.plugin' ) );
$form = trim( $this->c->get_string( 'browsercache.security.csp.form' ) );
$frame_ancestors = trim( $this->c->get_string( 'browsercache.security.csp.frame.ancestors' ) );
$sandbox = trim( $this->c->get_string( 'browsercache.security.csp.sandbox' ) );
$child = trim( $this->c->get_string( 'browsercache.security.csp.child' ) );
$manifest = trim( $this->c->get_string( 'browsercache.security.csp.manifest' ) );
$scriptelem = trim( $this->c->get_string( 'browsercache.security.csp.scriptelem' ) );
$scriptattr = trim( $this->c->get_string( 'browsercache.security.csp.scriptattr' ) );
$styleelem = trim( $this->c->get_string( 'browsercache.security.csp.styleelem' ) );
$styleattr = trim( $this->c->get_string( 'browsercache.security.csp.styleattr' ) );
$worker = trim( $this->c->get_string( 'browsercache.security.csp.worker' ) );
$default = trim( $this->c->get_string( 'browsercache.security.csp.default' ) );
$dir = rtrim(
( ! empty( $base ) ? "base-uri $base; " : '' ) .
( ! empty( $frame ) ? "frame-src $frame; " : '' ) .
( ! empty( $connect ) ? "connect-src $connect; " : '' ) .
( ! empty( $font ) ? "font-src $font; " : '' ) .
( ! empty( $script ) ? "script-src $script; " : '' ) .
( ! empty( $style ) ? "style-src $style; " : '' ) .
( ! empty( $img ) ? "img-src $img; " : '' ) .
( ! empty( $media ) ? "media-src $media; " : '' ) .
( ! empty( $object ) ? "object-src $object; " : '' ) .
( ! empty( $plugin ) ? "plugin-types $plugin; " : '' ) .
( ! empty( $form ) ? "form-action $form; " : '' ) .
( ! empty( $frame_ancestors ) ? "frame-ancestors $frame_ancestors; " : '' ) .
( ! empty( $sandbox ) ? "sandbox $sandbox; " : '' ) .
( ! empty( $child ) ? "child-src $child; " : '' ) .
( ! empty( $manifest ) ? "manifest-src $manifest; " : '' ) .
( ! empty( $scriptelem ) ? "script-src-elem $scriptelem; " : '' ) .
( ! empty( $scriptattr ) ? "script-src-attr $scriptattr; " : '' ) .
( ! empty( $styleelem ) ? "style-src-elem $styleelem; " : '' ) .
( ! empty( $styleattr ) ? "style-src-attr $styleattr; " : '' ) .
( ! empty( $worker ) ? "worker-src $worker; " : '' ) .
( ! empty( $default ) ? "default-src $default;" : '' ),
'; '
);
if ( ! empty( $dir ) ) {
$rules[] = "add_header Content-Security-Policy \"$dir\";";
}
}
if ( $this->c->get_boolean( 'browsercache.security.cspro' ) && ( ! empty( $this->c->get_string( 'browsercache.security.cspro.reporturi' ) ) || ! empty( $this->c->get_string( 'browsercache.security.cspro.reportto' ) ) ) ) {
$base = trim( $this->c->get_string( 'browsercache.security.cspro.base' ) );
$reporturi = trim( $this->c->get_string( 'browsercache.security.cspro.reporturi' ) );
$reportto = trim( $this->c->get_string( 'browsercache.security.cspro.reportto' ) );
$frame = trim( $this->c->get_string( 'browsercache.security.cspro.frame' ) );
$connect = trim( $this->c->get_string( 'browsercache.security.cspro.connect' ) );
$font = trim( $this->c->get_string( 'browsercache.security.cspro.font' ) );
$script = trim( $this->c->get_string( 'browsercache.security.cspro.script' ) );
$style = trim( $this->c->get_string( 'browsercache.security.cspro.style' ) );
$img = trim( $this->c->get_string( 'browsercache.security.cspro.img' ) );
$media = trim( $this->c->get_string( 'browsercache.security.cspro.media' ) );
$object = trim( $this->c->get_string( 'browsercache.security.cspro.object' ) );
$plugin = trim( $this->c->get_string( 'browsercache.security.cspro.plugin' ) );
$form = trim( $this->c->get_string( 'browsercache.security.cspro.form' ) );
$frame_ancestors = trim( $this->c->get_string( 'browsercache.security.cspro.frame.ancestors' ) );
$sandbox = trim( $this->c->get_string( 'browsercache.security.cspro.sandbox' ) );
$child = trim( $this->c->get_string( 'browsercache.security.csp.child' ) );
$manifest = trim( $this->c->get_string( 'browsercache.security.csp.manifest' ) );
$scriptelem = trim( $this->c->get_string( 'browsercache.security.csp.scriptelem' ) );
$scriptattr = trim( $this->c->get_string( 'browsercache.security.csp.scriptattr' ) );
$styleelem = trim( $this->c->get_string( 'browsercache.security.csp.styleelem' ) );
$styleattr = trim( $this->c->get_string( 'browsercache.security.csp.styleattr' ) );
$worker = trim( $this->c->get_string( 'browsercache.security.csp.worker' ) );
$default = trim( $this->c->get_string( 'browsercache.security.cspro.default' ) );
$dir = rtrim(
( ! empty( $base ) ? "base-uri $base; " : '' ) .
( ! empty( $reporturi ) ? "report-uri $reporturi; " : '' ) .
( ! empty( $reportto ) ? "report-to $reportto; " : '' ) .
( ! empty( $frame ) ? "frame-src $frame; " : '' ) .
( ! empty( $connect ) ? "connect-src $connect; " : '' ) .
( ! empty( $font ) ? "font-src $font; " : '' ) .
( ! empty( $script ) ? "script-src $script; " : '' ) .
( ! empty( $style ) ? "style-src $style; " : '' ) .
( ! empty( $img ) ? "img-src $img; " : '' ) .
( ! empty( $media ) ? "media-src $media; " : '' ) .
( ! empty( $object ) ? "object-src $object; " : '' ) .
( ! empty( $plugin ) ? "plugin-types $plugin; " : '' ) .
( ! empty( $form ) ? "form-action $form; " : '' ) .
( ! empty( $frame_ancestors ) ? "frame-ancestors $frame_ancestors; " : '' ) .
( ! empty( $sandbox ) ? "sandbox $sandbox; " : '' ) .
( ! empty( $child ) ? "child-src $child; " : '' ) .
( ! empty( $manifest ) ? "manifest-src $manifest; " : '' ) .
( ! empty( $scriptelem ) ? "script-src-elem $scriptelem; " : '' ) .
( ! empty( $scriptattr ) ? "script-src-attr $scriptattr; " : '' ) .
( ! empty( $styleelem ) ? "style-src-elem $styleelem; " : '' ) .
( ! empty( $styleattr ) ? "style-src-attr $styleattr; " : '' ) .
( ! empty( $worker ) ? "worker-src $worker; " : '' ) .
( ! empty( $default ) ? "default-src $default;" : '' ),
'; '
);
if ( ! empty( $dir ) ) {
$rules[] = "add_header Content-Security-Policy-Report-Only \"$dir\";";
}
}
if ( $this->c->get_boolean( 'browsercache.security.fp' ) ) {
$fp_values = $this->c->get_array( 'browsercache.security.fp.values' );
$feature_v = array();
$permission_v = array();
foreach ( $fp_values as $key => $value ) {
if ( ! empty( $value ) ) {
$value = str_replace( array( '"', "'" ), '', $value );
$feature_v[] = "$key '$value'";
$permission_v[] = "$key=($value)";
}
}
if ( ! empty( $feature_v ) ) {
$rules[] = 'add_header Feature-Policy "' . implode( ';', $feature_v ) . "\";\n";
}
if ( ! empty( $permission_v ) ) {
$rules[] = 'add_header Permissions-Policy "' . implode( ',', $permission_v ) . "\";\n";
}
}
}
return $rules;
}
/**
* Adds cache rules for type to &$rules.
*
* @param string $rules Rules.
* @param array $mime_types MIME types.
* @param string $section Section.
*
* @return void
*/
private function generate_section( &$rules, $mime_types, $section ) {
$expires = $this->c->get_boolean( 'browsercache.' . $section . '.expires' );
$etag = $this->c->get_boolean( 'browsercache.' . $section . '.etag' );
$cache_control = $this->c->get_boolean( 'browsercache.' . $section . '.cache.control' );
$w3tc = $this->c->get_boolean( 'browsercache.' . $section . '.w3tc' );
$last_modified = $this->c->get_boolean( 'browsercache.' . $section . '.last_modified' );
if ( $etag || $expires || $cache_control || $w3tc || ! $last_modified ) {
$mime_types2 = apply_filters(
'w3tc_browsercache_rules_section_extensions',
$mime_types,
$this->c,
$section
);
$extensions = array_keys( $mime_types2 );
// Remove ext from filesmatch if its the same as permalink extension.
$pext = strtolower( pathinfo( get_option( 'permalink_structure' ), PATHINFO_EXTENSION ) );
if ( $pext ) {
$extensions = Util_Rule::remove_extension_from_list( $extensions, $pext );
}
$rules .= 'location ~ \\.(' . implode( '|', $extensions ) . ')$ {' . "\n";
$subrules = Dispatcher::nginx_rules_for_browsercache_section( $this->c, $section );
$rules .= ' ' . implode( "\n ", $subrules ) . "\n";
// Add rules for the Image Service extension, if active.
if ( 'other' === $section && array_key_exists( 'imageservice', $this->c->get_array( 'extensions.active' ) ) ) {
$rules .= "\n" . ' location ~* ^(?<path>.+)\.(jpe?g|png|gif)$ {' . "\n" .
' if ( $http_accept !~* "webp|\*/\*" ) {' . "\n" .
' break;' . "\n" .
' }' . "\n\n" .
' ' . implode( "\n ", Dispatcher::nginx_rules_for_browsercache_section( $this->c, $section, true ) ) . "\n" .
' add_header Vary Accept;' . "\n";
if ( $this->c->get_boolean( 'browsercache.no404wp' ) ) {
$rules .= ' try_files ${path}.webp $uri =404;';
} else {
$rules .= ' try_files ${path}.webp $uri /index.php?$args;';
}
$rules .= "\n" . ' }' . "\n\n";
}
if ( ! $this->c->get_boolean( 'browsercache.no404wp' ) ) {
$wp_uri = network_home_url( '', 'relative' );
$wp_uri = rtrim( $wp_uri, '/' );
$rules .= ' try_files $uri $uri/ ' . $wp_uri . '/index.php?$args;' . "\n";
}
$rules .= '}' . "\n";
}
}
/**
* Returns directives plugin applies to files of specific section without location
*
* $extra_add_headers_set specifies if other add_header directives will be added to location block generated
*
* @param string $section Section.
* @param bool $extra_add_headers_set Extra add headers flag.
*
* @return array
*/
public function section_rules( $section, $extra_add_headers_set = false ) {
$rules = array();
$expires = $this->c->get_boolean( "browsercache.$section.expires" );
$lifetime = $this->c->get_integer( "browsercache.$section.lifetime" );
if ( $expires ) {
$rules[] = 'expires ' . $lifetime . 's;';
}
if ( version_compare( Util_Environment::get_server_version(), '1.3.3', '>=' ) ) {
if ( $this->c->get_boolean( "browsercache.$section.etag" ) ) {
$rules[] = 'etag on;';
} else {
$rules[] = 'etag off;';
}
}
if ( $this->c->get_boolean( "browsercache.$section.last_modified" ) ) {
$rules[] = 'if_modified_since exact;';
} else {
$rules[] = 'if_modified_since off;';
}
$add_header_rules = array();
if ( $this->c->get_boolean( "browsercache.$section.cache.control" ) ) {
$cache_policy = $this->c->get_string( "browsercache.$section.cache.policy" );
switch ( $cache_policy ) {
case 'cache':
$add_header_rules[] = 'add_header Pragma "public";';
$add_header_rules[] = 'add_header Cache-Control "public";';
break;
case 'cache_public_maxage':
$add_header_rules[] = 'add_header Pragma "public";';
if ( $expires ) {
$add_header_rules[] = 'add_header Cache-Control "public";';
} else {
$add_header_rules[] = "add_header Cache-Control \"max-age=$lifetime, public\";";
}
break;
case 'cache_validation':
$add_header_rules[] = 'add_header Pragma "public";';
$add_header_rules[] = 'add_header Cache-Control "public, must-revalidate, proxy-revalidate";';
break;
case 'cache_noproxy':
$add_header_rules[] = 'add_header Pragma "public";';
$add_header_rules[] = 'add_header Cache-Control "private, must-revalidate";';
break;
case 'cache_maxage':
$add_header_rules[] = 'add_header Pragma "public";';
if ( $expires ) {
$add_header_rules[] = 'add_header Cache-Control "public, must-revalidate, proxy-revalidate";';
} else {
$add_header_rules[] = "add_header Cache-Control \"max-age=$lifetime, public, must-revalidate, proxy-revalidate\";";
}
break;
case 'no_cache':
$add_header_rules[] = 'add_header Pragma "no-cache";';
$add_header_rules[] = 'add_header Cache-Control "private, no-cache";';
break;
case 'no_store':
$add_header_rules[] = 'add_header Pragma "no-store";';
$add_header_rules[] = 'add_header Cache-Control "no-store";';
break;
case 'cache_immutable':
$add_header_rules[] = 'add_header Pragma "public";';
$add_header_rules[] = "add_header Cache-Control \"public, max-age=$lifetime, immutable\";";
break;
case 'cache_immutable_nomaxage':
$add_header_rules[] = 'add_header Pragma "public";';
$add_header_rules[] = 'add_header Cache-Control "public, immutable";';
break;
}
}
if ( $this->c->get_boolean( "browsercache.$section.w3tc" ) ) {
$add_header_rules[] = 'add_header X-Powered-By "' . Util_Environment::w3tc_header() . '";';
}
if ( ! empty( $add_header_rules ) || $extra_add_headers_set ) {
$add_header_rules = array_merge( $add_header_rules, $this->security_rules() );
}
return array(
'add_header' => $add_header_rules,
'other' => $rules,
);
}
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* File: BrowserCache_Page.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class BrowserCache_Environment
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
*/
class BrowserCache_Page extends Base_Page_Settings {
/**
* Page
*
* @var string
*/
protected $_page = 'w3tc_browsercache';
/**
* Quick reference AJAX
*
* @return void
*/
public static function w3tc_ajax() {
add_action( 'w3tc_ajax_browsercache_quick_reference', array( '\W3TC\BrowserCache_Page', 'w3tc_ajax_browsercache_quick_reference' ) );
}
/**
* Quick reference include
*
* @return void
*/
public static function w3tc_ajax_browsercache_quick_reference() {
include W3TC_DIR . '/BrowserCache_Page_View_QuickReference.php';
exit();
}
/**
* View browser cache options
*
* @return void
*/
public function view() {
$browsercache_enabled = $this->_config->get_boolean( 'browsercache.enabled' );
$browsercache_last_modified = ( $this->_config->get_boolean( 'browsercache.cssjs.last_modified' ) && $this->_config->get_boolean( 'browsercache.html.last_modified' ) && $this->_config->get_boolean( 'browsercache.other.last_modified' ) );
$browsercache_expires = ( $this->_config->get_boolean( 'browsercache.cssjs.expires' ) && $this->_config->get_boolean( 'browsercache.html.expires' ) && $this->_config->get_boolean( 'browsercache.other.expires' ) );
$browsercache_cache_control = ( $this->_config->get_boolean( 'browsercache.cssjs.cache.control' ) && $this->_config->get_boolean( 'browsercache.html.cache.control' ) && $this->_config->get_boolean( 'browsercache.other.cache.control' ) );
$browsercache_etag = ( $this->_config->get_boolean( 'browsercache.cssjs.etag' ) && $this->_config->get_boolean( 'browsercache.html.etag' ) && $this->_config->get_boolean( 'browsercache.other.etag' ) );
$browsercache_w3tc = ( $this->_config->get_boolean( 'browsercache.cssjs.w3tc' ) && $this->_config->get_boolean( 'browsercache.html.w3tc' ) && $this->_config->get_boolean( 'browsercache.other.w3tc' ) );
$browsercache_compression = ( $this->_config->get_boolean( 'browsercache.cssjs.compression' ) && $this->_config->get_boolean( 'browsercache.html.compression' ) && $this->_config->get_boolean( 'browsercache.other.compression' ) );
$browsercache_brotli = ( $this->_config->get_boolean( 'browsercache.cssjs.brotli' ) && $this->_config->get_boolean( 'browsercache.html.brotli' ) && $this->_config->get_boolean( 'browsercache.other.brotli' ) );
$browsercache_replace = ( $this->_config->get_boolean( 'browsercache.cssjs.replace' ) && $this->_config->get_boolean( 'browsercache.other.replace' ) );
$browsercache_querystring = ( $this->_config->get_boolean( 'browsercache.cssjs.querystring' ) && $this->_config->get_boolean( 'browsercache.other.querystring' ) );
$browsercache_update_media_qs = ( $this->_config->get_boolean( 'browsercache.cssjs.replace' ) || $this->_config->get_boolean( 'browsercache.other.replace' ) );
$browsercache_nocookies = ( $this->_config->get_boolean( 'browsercache.cssjs.nocookies' ) && $this->_config->get_boolean( 'browsercache.other.nocookies' ) );
$is_nginx = Util_Environment::is_nginx();
include W3TC_INC_DIR . '/options/browsercache.php';
}
}

View File

@@ -0,0 +1,84 @@
<?php
/**
* File: BrowserCache_Page_View_QuickReference.php
*
* @package W3TC
*/
namespace W3TC;
if ( ! defined( 'W3TC' ) ) {
die();
}
?>
<div class="lightbox-content-padded">
<h3><?php esc_html_e( 'Security Headers: Quick Reference', 'w3-total-cache' ); ?></h3>
<fieldset>
<legend><?php esc_html_e( 'Legend', 'w3-total-cache' ); ?></legend>
<p>
All of the directives that end with -src support similar values known as
a source list. Multiple source list values can be space separated with the exception of
'none' which should be the only value.
</p>
</fieldset>
<table class="w3tcbc_qrf">
<tr>
<th>Source Value</th>
<th>Example</th>
<th>Description</th>
</tr>
<tr>
<td><code>*</code></td>
<td><code>img-src *</code></td>
<td>Wildcard, allows any URL except data: blob: filesystem: schemes</td>
</tr>
<tr>
<td><code>'none'</code></td>
<td><code>object-src 'none'</code></td>
<td>Prevents loading resources from any source</td>
</tr>
<tr>
<td><code>'self'</code></td>
<td><code>script-src 'self'</code></td>
<td>Allows loading resources from the same origin (same scheme, host and port)</td>
</tr>
<tr>
<td><code>data:</code></td>
<td><code>img-src 'self' data:</code></td>
<td>Allows loading resources via the data scheme (e.g. Base64 encoded images)</td>
</tr>
<tr>
<td><code>domain.example.com</code></td>
<td><code>img-src domain.example.com</code></td>
<td>Allows loading resources from the specified domain name</td>
</tr>
<tr>
<td><code>*.example.com</code></td>
<td><code>img-src *.example.com</code></td>
<td>Allows loading resources from any subdomain under example.com</td>
</tr>
<tr>
<td><code>https://cdn.com</code></td>
<td><code>img-src https://cdn.com</code></td>
<td>Allows loading resources only over <acronym title="HyperText Transfer Protocol over SSL">HTTPS</acronym> matching the given domain</td>
</tr>
<tr>
<td><code>https:</code></td>
<td><code>img-src https:</code></td>
<td>Allows loading resources only over <acronym title="HyperText Transfer Protocol over SSL">HTTPS</acronym> on any domain</td>
</tr>
<tr>
<td><code>'unsafe-inline'</code></td>
<td><code>script-src 'unsafe-inline'</code></td>
<td>Allows use of inline source elements such as style attribute, onclick, or script tag bodies (depends on the context of the source it is applied to)</td>
</tr>
<tr>
<td><code>'unsafe-eval'</code></td>
<td><code>script-src 'unsafe-eval'</code></td>
<td>Allows unsafe dynamic code evaluation such as Javascript eval()</td>
</tr>
</table>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,612 @@
<?php
/**
* File: BrowserCache_Plugin.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class BrowserCache_Plugin
*
* W3 ObjectCache plugin
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable WordPress.PHP.IniSet.Risky
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
*/
class BrowserCache_Plugin {
/**
* Config
*
* @var Config
*/
private $_config = null;
/**
* Browsercache rewrite
*
* @var bool
*/
private $browsercache_rewrite;
/**
* Constructor
*
* @return void
*/
public function __construct() {
$this->_config = Dispatcher::config();
}
/**
* Runs plugin
*
* @return void
*/
public function run() {
add_filter( 'w3tc_admin_bar_menu', array( $this, 'w3tc_admin_bar_menu' ) );
if ( $this->_config->get_boolean( 'browsercache.html.w3tc' ) ) {
add_action( 'send_headers', array( $this, 'send_headers' ) );
}
if ( ! $this->_config->get_boolean( 'browsercache.html.etag' ) ) {
add_filter( 'wp_headers', array( $this, 'filter_wp_headers' ), 0, 2 );
}
$url_uniqualize_enabled = $this->url_uniqualize_enabled();
if ( $this->url_clean_enabled() || $url_uniqualize_enabled ) {
$this->browsercache_rewrite = $this->_config->get_boolean( 'browsercache.rewrite' );
// modify CDN urls.
add_filter( 'w3tc_cdn_url', array( $this, 'w3tc_cdn_url' ), 0, 3 );
if ( $url_uniqualize_enabled ) {
add_action( 'w3tc_flush_all', array( $this, 'w3tc_flush_all' ), 1050, 1 );
}
if ( $this->can_ob() ) {
Util_Bus::add_ob_callback( 'browsercache', array( $this, 'ob_callback' ) );
}
}
$v = $this->_config->get_string( 'browsercache.security.session.cookie_httponly' );
if ( ! empty( $v ) ) {
@ini_set( 'session.cookie_httponly', 'on' === $v ? '1' : '0' ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
}
$v = $this->_config->get_string( 'browsercache.security.session.cookie_secure' );
if ( ! empty( $v ) ) {
@ini_set( 'session.cookie_secure', 'on' === $v ? '1' : '0' ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
}
$v = $this->_config->get_string( 'browsercache.security.session.use_only_cookies' );
if ( ! empty( $v ) ) {
@ini_set( 'session.use_only_cookies', 'on' === $v ? '1' : '0' ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
}
add_filter( 'w3tc_minify_http2_preload_url', array( $this, 'w3tc_minify_http2_preload_url' ), 4000 );
add_filter( 'w3tc_cdn_config_headers', array( $this, 'w3tc_cdn_config_headers' ) );
if ( Util_Admin::is_w3tc_admin_page() ) {
add_action( 'admin_notices', array( $this, 'admin_notices' ) );
}
}
/**
* Check if URL clean is enabled
*
* @return bool
*/
private function url_clean_enabled() {
return $this->_config->get_boolean( 'browsercache.cssjs.querystring' ) ||
$this->_config->get_boolean( 'browsercache.html.querystring' ) ||
$this->_config->get_boolean( 'browsercache.other.querystring' );
}
/**
* Check if URL uniqualize is enabled
*
* @return bool
*/
private function url_uniqualize_enabled() {
return $this->_config->get_boolean( 'browsercache.cssjs.replace' ) ||
$this->_config->get_boolean( 'browsercache.html.replace' ) ||
$this->_config->get_boolean( 'browsercache.other.replace' );
}
/**
* Flush all
*
* @param array $extras Extras.
*
* @return void
*/
public function w3tc_flush_all( $extras = array() ) {
if ( isset( $extras['only'] ) && 'browsercache' !== $extras['only'] ) {
return;
}
update_option( 'w3tc_browsercache_flush_timestamp', wp_rand( 10000, 99999 ) . '' );
}
/**
* Check if we can start OB
*
* @return boolean
*/
public function can_ob() {
/**
* Skip if admin
*/
if ( defined( 'WP_ADMIN' ) ) {
return false;
}
/**
* Skip if doing AJAX
*/
if ( defined( 'DOING_AJAX' ) ) {
return false;
}
/**
* Skip if doing cron
*/
if ( defined( 'DOING_CRON' ) ) {
return false;
}
/**
* Skip if APP request
*/
if ( defined( 'APP_REQUEST' ) ) {
return false;
}
/**
* Skip if XMLRPC request
*/
if ( defined( 'XMLRPC_REQUEST' ) ) {
return false;
}
/**
* Check for WPMU's and WP's 3.0 short init
*/
if ( defined( 'SHORTINIT' ) && SHORTINIT ) {
return false;
}
/**
* Check User Agent
*/
$http_user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '';
if ( stristr( $http_user_agent, W3TC_POWERED_BY ) !== false ) {
return false;
}
return true;
}
/**
* Output buffer callback
*
* @param string $buffer Buffer.
*
* @return mixed
*/
public function ob_callback( $buffer ) {
if ( '' !== $buffer && Util_Content::is_html_xml( $buffer ) ) {
$domain_url_regexp = Util_Environment::home_domain_root_url_regexp();
$buffer = preg_replace_callback(
'~(href|src|action|extsrc|asyncsrc|w3tc_load_js\()=?([\'"])((' . $domain_url_regexp . ')?(/[^\'"/][^\'"]*\.([a-z-_]+)([\?#][^\'"]*)?))[\'"]~Ui',
array( $this, 'link_replace_callback' ),
$buffer
);
// without quotes.
$buffer = preg_replace_callback(
'~(href|src|action|extsrc|asyncsrc)=((' . $domain_url_regexp . ')?(/[^\\s>][^\\s>]*\.([a-z-_]+)([\?#][^\\s>]*)?))([\\s>])~Ui',
array( $this, 'link_replace_callback_noquote' ),
$buffer
);
}
return $buffer;
}
/**
* Link replace callback
*
* @param string $matches Matches.
*
* @return string
*/
public function link_replace_callback( $matches ) {
list ( $match, $attr, $quote, $url, , , , , $extension ) = $matches;
$ops = $this->_get_url_mutation_operations( $url, $extension );
if ( is_null( $ops ) ) {
return $match;
}
$url = $this->mutate_url( $url, $ops, ! $this->browsercache_rewrite );
if ( 'w3tc_load_js(' !== $attr ) {
return $attr . '=' . $quote . $url . $quote;
}
return sprintf( '%s\'%s\'', $attr, $url );
}
/**
* Link replace callback when no quote arount attribute value
*
* @param string $matches Matches.
*
* @return string
*/
public function link_replace_callback_noquote( $matches ) {
list ( $match, $attr, $url, , , , , $extension, , $delimiter ) = $matches;
$ops = $this->_get_url_mutation_operations( $url, $extension );
if ( is_null( $ops ) ) {
return $match;
}
$url = $this->mutate_url( $url, $ops, ! $this->browsercache_rewrite );
return $attr . '=' . $url . $delimiter;
}
/**
* Mutate http/2 header links
*
* @param array $data Data.
*
* @return array
*/
public function w3tc_minify_http2_preload_url( $data ) {
if ( isset( $data['browsercache_processed'] ) ) {
return $data;
}
$data['browsercache_processed'] = '*';
$url = $data['result_link'];
// decouple extension.
$matches = array();
if ( ! preg_match( '/\.([a-zA-Z0-9]+)($|[\?])/', $url, $matches ) ) {
return $data;
}
$extension = $matches[1];
$ops = $this->_get_url_mutation_operations( $url, $extension );
if ( is_null( $ops ) ) {
return $data;
}
$mutate_by_querystring = ! $this->browsercache_rewrite;
$url = $this->mutate_url( $url, $ops, $mutate_by_querystring );
$data['result_link'] = $url;
return $data;
}
/**
* Link replace for CDN url
*
* @param string $url URL.
* @param string $original_url Original URL.
* @param bool $is_cdn_mirror Is CDN mirror.
*
* @return string
*/
public function w3tc_cdn_url( $url, $original_url, $is_cdn_mirror ) {
// decouple extension.
$matches = array();
if ( ! preg_match( '/\.([a-zA-Z0-9]+)($|[\?])/', $original_url, $matches ) ) {
return $url;
}
$extension = $matches[1];
$ops = $this->_get_url_mutation_operations( $original_url, $extension );
if ( is_null( $ops ) ) {
return $url;
}
// for push cdns each flush would require manual reupload of files.
$mutate_by_querystring = ! $this->browsercache_rewrite || ! $is_cdn_mirror;
$url = $this->mutate_url( $url, $ops, $mutate_by_querystring );
return $url;
}
/**
* Mutate url
*
* @param string $url URL.
* @param array $ops Operations data.
* @param bool $mutate_by_querystring Mutate by querystring flag.
*
* @return string
*/
private function mutate_url( $url, $ops, $mutate_by_querystring ) {
$query_pos = strpos( $url, '?' );
if ( isset( $ops['querystring'] ) && false !== $query_pos ) {
$url = substr( $url, 0, $query_pos );
$query_pos = false;
}
if ( isset( $ops['replace'] ) ) {
$id = $this->get_filename_uniqualizator();
if ( $mutate_by_querystring ) {
if ( false !== $query_pos ) {
$url = substr( $url, 0, $query_pos + 1 ) . $id . '&amp;' . substr( $url, $query_pos + 1 );
} else {
$tag_pos = strpos( $url, '#' );
if ( false === $tag_pos ) {
$url .= '?' . $id;
} else {
$url = substr( $url, 0, $tag_pos ) . '?' . $id . substr( $url, $tag_pos );
}
}
} else {
// add $id to url before extension.
$url_query = '';
if ( false !== $query_pos ) {
$url_query = substr( $url, $query_pos );
$url = substr( $url, 0, $query_pos );
}
$ext_pos = strrpos( $url, '.' );
$extension = substr( $url, $ext_pos );
$url = substr( $url, 0, strlen( $url ) - strlen( $extension ) ) .
'.' . $id . $extension . $url_query;
}
}
return $url;
}
/**
* Get mutatation url operations
*
* @param string $url URL.
* @param string $extension Operations data.
*
* @return string
*/
public function _get_url_mutation_operations( $url, $extension ) {
static $extensions = null;
if ( null === $extensions ) {
$core = Dispatcher::component( 'BrowserCache_Core' );
$extensions = $core->get_replace_querystring_extensions( $this->_config );
}
static $exceptions = null;
if ( null === $exceptions ) {
$exceptions = $this->_config->get_array( 'browsercache.replace.exceptions' );
}
if ( ! isset( $extensions[ $extension ] ) ) {
return null;
}
$test_url = Util_Environment::remove_query( $url );
foreach ( $exceptions as $exception ) {
$escaped = str_replace( '~', '\~', $exception );
if ( trim( $exception ) && preg_match( '~' . $escaped . '~', $test_url ) ) {
return null;
}
}
return $extensions[ $extension ];
}
/**
* Returns replace ID
*
* @return string
*/
public function get_filename_uniqualizator() {
static $cache_id = null;
if ( null === $cache_id ) {
$value = get_option( 'w3tc_browsercache_flush_timestamp' );
if ( empty( $value ) ) {
$value = wp_rand( 10000, 99999 ) . '';
update_option( 'w3tc_browsercache_flush_timestamp', $value );
}
$cache_id = substr( $value, 0, 5 );
}
return 'x' . $cache_id;
}
/**
* Admin bar menu
*
* @param array $menu_items Menu items.
*
* @return array
*/
public function w3tc_admin_bar_menu( $menu_items ) {
$browsercache_update_media_qs = (
$this->_config->get_boolean( 'browsercache.cssjs.replace' ) ||
$this->_config->get_boolean( 'browsercache.other.replace' )
);
if ( $browsercache_update_media_qs ) {
$current_page = Util_Request::get_string( 'page', 'w3tc_dashboard' );
$menu_items['20190.browsercache'] = array(
'id' => 'w3tc_flush_browsercache',
'parent' => 'w3tc_flush',
'title' => __( 'Browser Cache', 'w3-total-cache' ),
'href' => wp_nonce_url(
admin_url(
'admin.php?page=' . $current_page . '&amp;w3tc_flush_browser_cache'
),
'w3tc'
),
);
}
return $menu_items;
}
/**
* Send headers
*
* @return void
*/
public function send_headers() {
@header( 'X-Powered-By: ' . Util_Environment::w3tc_header() );
}
/**
* Returns headers config for CDN
*
* @param Config $config Config.
*
* @return Config
*/
public function w3tc_cdn_config_headers( $config ) {
$sections = Util_Mime::sections_to_mime_types_map();
foreach ( $sections as $section => $v ) {
$config[ $section ] = $this->w3tc_cdn_config_headers_section( $section );
}
return $config;
}
/**
* Gets CDN config headers section
*
* @param string $section Section.
*
* @return Config
*/
private function w3tc_cdn_config_headers_section( $section ) {
$c = $this->_config;
$prefix = 'browsercache.' . $section;
$lifetime = $c->get_integer( $prefix . '.lifetime' );
$headers = array();
if ( $c->get_boolean( $prefix . '.w3tc' ) ) {
$headers['X-Powered-By'] = Util_Environment::w3tc_header();
}
if ( $c->get_boolean( $prefix . '.cache.control' ) ) {
switch ( $c->get_string( $prefix . '.cache.policy' ) ) {
case 'cache':
$headers['Pragma'] = 'public';
$headers['Cache-Control'] = 'public';
break;
case 'cache_public_maxage':
$headers['Pragma'] = 'public';
$headers['Cache-Control'] = "max-age=$lifetime, public";
break;
case 'cache_validation':
$headers['Pragma'] = 'public';
$headers['Cache-Control'] = 'public, must-revalidate, proxy-revalidate';
break;
case 'cache_noproxy':
$headers['Pragma'] = 'public';
$headers['Cache-Control'] = 'private, must-revalidate';
break;
case 'cache_maxage':
$headers['Pragma'] = 'public';
$headers['Cache-Control'] = "max-age=$lifetime, public, must-revalidate, proxy-revalidate";
break;
case 'no_cache':
$headers['Pragma'] = 'no-cache';
$headers['Cache-Control'] = 'private, no-cache';
break;
case 'no_store':
$headers['Pragma'] = 'no-store';
$headers['Cache-Control'] = 'no-store';
break;
case 'cache_immutable':
$headers['Pragma'] = 'public';
$headers['Cache-Control'] = "max-age=$lifetime, public, immutable";
break;
case 'cache_immutable_nomaxage':
$headers['Pragma'] = 'public';
$headers['Cache-Control'] = 'public, immutable';
break;
}
}
return array(
'etag' => $c->get_boolean( $prefix . 'etag' ),
'expires' => $c->get_boolean( $prefix . '.expires' ),
'lifetime' => $lifetime,
'static' => $headers,
);
}
/**
* Filters headers set by WordPress
*
* @param array $headers Headers.
* @param object $wp WP object.
*
* @return array
*/
public function filter_wp_headers( $headers, $wp ) {
if ( ! empty( $wp->query_vars['feed'] ) ) {
unset( $headers['ETag'] );
}
return $headers;
}
/**
* Admin notice for Content-Security-Policy-Report-Only that displays if the feature is enabled and the report-uri/to isn't defined.
*
* @since 2.2.13
*/
public function admin_notices() {
// Check if the current user is a contributor or higher.
if (
\user_can( \get_current_user_id(), 'manage_options' ) &&
$this->_config->get_boolean( 'browsercache.security.cspro' ) &&
empty( $this->_config->get_string( 'browsercache.security.cspro.reporturi' ) ) &&
empty( $this->_config->get_string( 'browsercache.security.cspro.reportto' ) )
) {
$message = '<p>' . sprintf(
// translators: 1 opening HTML a tag to Browser Cache CSP-Report-Only settings, 2 closing HTML a tag.
esc_html__(
'The Content Security Policy - Report Only requires the "report-uri" and/or "report-to" directives. Please define one or both of these directives %1$shere%2$s.',
'w3-total-cache'
),
'<a href="' . Util_Ui::admin_url( 'admin.php?page=w3tc_browsercache#browsercache__security__cspro' ) . '" target="_blank" alt="' . esc_attr__( 'Browser Cache Content-Security-Policy-Report-Only Settings', 'w3-total-cache' ) . '">',
'</a>'
);
Util_Ui::error_box( $message );
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* File: BrowserCache_Plugin_Admin.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class BrowserCache_Environment
*
* phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter
*/
class BrowserCache_Plugin_Admin {
/**
* Run
*
* @return void
*/
public function run() {
$config_labels = new BrowserCache_ConfigLabels();
add_filter( 'w3tc_config_labels', array( $config_labels, 'config_labels' ) );
add_action( 'w3tc_ajax', array( '\W3TC\BrowserCache_Page', 'w3tc_ajax' ) );
add_action( 'w3tc_config_ui_save-w3tc_browsercache', array( $this, 'w3tc_config_ui_save_w3tc_browsercache' ), 10, 2 );
}
/**
* Config UI save
*
* @param Config $config Config.
* @param Config $old_config Config.
*
* @return void
*/
public function w3tc_config_ui_save_w3tc_browsercache( $config, $old_config ) {
$prefix = 'browsercache__security__fp__values__keyvalues__';
$prefixl = strlen( $prefix );
$fp_values = array();
foreach ( $_REQUEST as $key => $value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$value = Util_Request::get_string( $key );
if ( substr( $key, 0, $prefixl ) === $prefix ) {
$k = substr( $key, $prefixl );
if ( ! empty( $value ) ) {
$fp_values[ $k ] = $value;
}
}
}
$config->set( 'browsercache.security.fp.values', $fp_values );
}
}

View File

@@ -0,0 +1,215 @@
<?php
/**
* File: Cache.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cache
*
* phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
*/
class Cache {
/**
* Returns cache engine instance
*
* @param string $engine Engine key code.
* @param array $config Configuration.
*
* @return W3_Cache_Base
*/
public static function instance( $engine, $config = array() ) {
static $instances = array();
// Common configuration data.
if ( ! isset( $config['blog_id'] ) ) {
$config['blog_id'] = Util_Environment::blog_id();
}
$instance_key = sprintf( '%s_%s', $engine, md5( serialize( $config ) ) );
if ( ! isset( $instances[ $instance_key ] ) ) {
switch ( $engine ) {
case 'apc':
if ( function_exists( 'apcu_store' ) ) {
$instances[ $instance_key ] = new Cache_Apcu( $config );
} elseif ( function_exists( 'apc_store' ) ) {
$instances[ $instance_key ] = new Cache_Apc( $config );
}
break;
case 'eaccelerator':
$instances[ $instance_key ] = new Cache_Eaccelerator( $config );
break;
case 'file':
$instances[ $instance_key ] = new Cache_File( $config );
break;
case 'file_generic':
$instances[ $instance_key ] = new Cache_File_Generic( $config );
break;
case 'memcached':
if ( class_exists( '\Memcached' ) ) {
$instances[ $instance_key ] = new Cache_Memcached( $config );
} elseif ( class_exists( '\Memcache' ) ) {
$instances[ $instance_key ] = new Cache_Memcache( $config );
}
break;
case 'nginx_memcached':
$instances[ $instance_key ] = new Cache_Nginx_Memcached( $config );
break;
case 'redis':
$instances[ $instance_key ] = new Cache_Redis( $config );
break;
case 'wincache':
$instances[ $instance_key ] = new Cache_Wincache( $config );
break;
case 'xcache':
$instances[ $instance_key ] = new Cache_Xcache( $config );
break;
default:
trigger_error( 'Incorrect cache engine ' . esc_html( $engine ), E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
$instances[ $instance_key ] = new Cache_Base( $config );
break;
}
if ( ! isset( $instances[ $instance_key ] ) || ! $instances[ $instance_key ]->available() ) {
$instances[ $instance_key ] = new Cache_Base( $config );
}
}
return $instances[ $instance_key ];
}
/**
* Returns caching engine name.
*
* @param string $engine Engine key code.
* @param string $module Module.
*
* @return string
*/
public static function engine_name( $engine, $module = '' ) {
switch ( $engine ) {
case 'memcached':
if ( class_exists( 'Memcached' ) ) {
$engine_name = 'Memcached';
} else {
$engine_name = 'Memcache';
}
break;
case 'nginx_memcached':
$engine_name = 'Nginx + Memcached';
break;
case 'apc':
$engine_name = 'APC';
break;
case 'eaccelerator':
$engine_name = 'EAccelerator';
break;
case 'redis':
$engine_name = 'Redis';
break;
case 'xcache':
$engine_name = 'XCache';
break;
case 'wincache':
$engine_name = 'WinCache';
break;
case 'file':
if ( 'pgcache' === $module ) {
$engine_name = 'Disk: Basic';
} else {
$engine_name = 'Disk';
}
break;
case 'file_generic':
$engine_name = 'Disk: Enhanced';
break;
case 'ftp':
$engine_name = 'Self-hosted / file transfer protocol upload';
break;
case 's3':
$engine_name = 'Amazon Simple Storage Service (S3)';
break;
case 's3_compatible':
$engine_name = 'S3 compatible';
break;
case 'cf':
$engine_name = 'Amazon CloudFront';
break;
case 'google_drive':
$engine_name = 'Google Drive';
break;
case 'cf2':
$engine_name = 'Amazon CloudFront';
break;
case 'cloudfront':
$engine_name = 'Amazon CloudFront';
break;
case 'rscf':
$engine_name = 'Rackspace Cloud Files';
break;
case 'azure':
$engine_name = 'Microsoft Azure Storage';
break;
case 'azuremi':
$engine_name = 'Microsoft Azure Storage (Managed Identity)';
break;
case 'edgecast':
$engine_name = 'Media Template ProCDN / EdgeCast';
break;
case 'att':
$engine_name = 'AT&amp;T';
break;
case 'rackspace_cdn':
$engine_name = 'Rackspace';
break;
case 'bunnycdn':
$engine_name = 'Bunny CDN';
break;
case '':
$engine_name = __( 'None', 'w3-total-cache' );
break;
default:
$engine_name = $engine;
break;
}
return $engine_name;
}
}

View File

@@ -0,0 +1,314 @@
<?php
/**
* File: CacheFlush.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class CacheFlush
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
*/
class CacheFlush {
/**
* Config
*
* @var Config $_config
*/
private $_config;
/**
* Executor
*
* @var Object $_executor
*/
private $_executor;
/**
* Initializes the cache flush executor and registers necessary hooks.
*
* This constructor sets up the cache flush mechanism based on configuration, using either
* a local executor or a message bus for distributed environments. It also registers
* WordPress actions and filters to handle delayed operations and cache flush events.
*
* @return void
*/
public function __construct() {
$this->_config = Dispatcher::config();
$sns = $this->_config->get_boolean( 'cluster.messagebus.enabled' );
if ( $sns ) {
$this->_executor = new Enterprise_CacheFlush_MakeSnsEvent();
} else {
$this->_executor = new CacheFlush_Locally();
}
if ( function_exists( 'add_action' ) ) {
add_action( 'w3tc_redirect', array( $this, 'execute_delayed_operations' ), 100000, 0 );
add_filter( 'wp_redirect', array( $this, 'execute_delayed_operations_filter' ), 100000, 1 );
add_action( 'w3tc_messagebus_message_processed', array( $this, 'execute_delayed_operations' ), 0 );
add_action( 'shutdown', array( $this, 'execute_delayed_operations' ), 100000, 0 );
}
}
/**
* Flushes the database cache if enabled.
*
* This method clears the database cache by delegating the operation to the executor
* if the database cache is enabled in the configuration.
*
* @return void
*/
public function dbcache_flush() {
if ( $this->_config->get_boolean( 'dbcache.enabled' ) ) {
$this->_executor->dbcache_flush();
}
}
/**
* Flushes the minification cache if enabled.
*
* This method clears the minification cache by delegating the operation to the executor
* if the minification feature is enabled in the configuration.
*
* @return void
*/
public function minifycache_flush() {
if ( $this->_config->get_boolean( 'minify.enabled' ) ) {
$this->_executor->minifycache_flush();
}
}
/**
* Flushes the object cache if enabled.
*
* This method clears the object cache by delegating the operation to the executor
* if the object cache feature is enabled in the configuration.
*
* @return void
*/
public function objectcache_flush() {
if ( $this->_config->getf_boolean( 'objectcache.enabled' ) ) {
$this->_executor->objectcache_flush();
}
}
/**
* Flushes the fragment cache.
*
* This method clears all stored fragment cache entries using the executor.
*
* @return void
*/
public function fragmentcache_flush() {
$this->_executor->fragmentcache_flush();
}
/**
* Flushes a specific fragment cache group.
*
* @param string $group The cache group to flush.
*
* @return void
*/
public function fragmentcache_flush_group( $group ) {
$this->_executor->fragmentcache_flush_group( $group );
}
/**
* Flushes the browser cache if enabled.
*
* This method clears the browser cache rules by delegating the operation to the executor
* if the browser cache feature is enabled in the configuration.
*
* @return void
*/
public function browsercache_flush() {
if ( $this->_config->get_boolean( 'browsercache.enabled' ) ) {
$this->_executor->browsercache_flush();
}
}
/**
* Purges all CDN cache files.
*
* This method clears all files stored in the CDN cache if the CDN feature is enabled.
*
* @param array $extras Additional options or context for the purge.
*
* @return bool True on success, false otherwise.
*/
public function cdn_purge_all( $extras = array() ) {
if ( $this->_config->get_boolean( 'cdn.enabled' ) || $this->_config->get_boolean( 'cdnfsd.enabled' ) ) {
return $this->_executor->cdn_purge_all( $extras );
}
return false;
}
/**
* Purges specific files from the CDN cache.
*
* @param array $purgefiles List of file paths to purge from the CDN cache.
*
* @return void
*/
public function cdn_purge_files( $purgefiles ) {
$this->_executor->cdn_purge_files( $purgefiles );
}
/**
* Flushes the OPcache.
*
* This method clears the PHP OPcache by delegating the operation to the executor.
*
* @return bool True on success, false otherwise.
*/
public function opcache_flush() {
return $this->_executor->opcache_flush();
}
/**
* Flushes the cache for a specific post.
*
* @param int $post_id The ID of the post to flush.
* @param mixed $extras Additional options or context for the flush.
*
* @return bool True on success, false otherwise.
*/
public function flush_post( $post_id, $extras = null ) {
return $this->_executor->flush_post( $post_id, $extras );
}
/**
* Retrieves a list of flushable posts.
*
* This method applies a filter to allow customization of which posts can be flushed.
*
* @param mixed $extras Additional options or context for filtering flushable posts.
*
* @return array|bool List of flushable posts or false if none are defined.
*/
public function flushable_posts( $extras = null ) {
$flushable_posts = apply_filters( 'w3tc_flushable_posts', false, $extras );
return $flushable_posts;
}
/**
* Flushes all eligible posts.
*
* This method clears the cache for all eligible posts as determined by the executor.
*
* @param mixed $extras Additional options or context for the flush.
*
* @return bool True on success, false otherwise.
*/
public function flush_posts( $extras = null ) {
return $this->_executor->flush_posts( $extras );
}
/**
* Flushes all caches.
*
* This method clears all caches once per execution, preventing redundant flush operations.
*
* @param mixed $extras Additional options or context for the flush.
*
* @return void
*/
public function flush_all( $extras = null ) {
static $flushed = false;
if ( ! $flushed ) {
$flushed = true;
if ( Util_Environment::is_elementor() ) {
// Flush Elementor's file manager cache.
\elementor\Plugin::$instance->files_manager->clear_cache();
// Flush W3 Total Cache's Object Cache to ensure Elementor changes are reflected.
$this->objectcache_flush();
}
$this->_executor->flush_all( $extras );
}
}
/**
* Flushes a specific cache group.
*
* @param string $group The cache group to flush.
* @param mixed $extras Additional options or context for the flush.
*
* @return void
*/
public function flush_group( $group, $extras = null ) {
static $flushed_groups = array();
if ( ! isset( $flushed_groups[ $group ] ) ) {
$flushed_groups[ $group ] = '*';
$this->_executor->flush_group( $group, $extras );
}
}
/**
* Flushes a specific URL from the cache.
*
* This method clears the cache for a single URL, ensuring it is only flushed once per execution.
*
* @param string $url The URL to flush.
* @param mixed $extras Additional options or context for the flush.
*
* @return bool True on success, false otherwise.
*/
public function flush_url( $url, $extras = null ) {
static $flushed_urls = array();
if ( ! in_array( $url, $flushed_urls, true ) ) {
$flushed_urls[] = $url;
return $this->_executor->flush_url( $url, $extras );
}
return true;
}
/**
* Primes the cache for a specific post.
*
* This method preloads the cache for the specified post by delegating to the executor.
*
* @param int $post_id The ID of the post to prime.
*
* @return bool True on success, false otherwise.
*/
public function prime_post( $post_id ) {
return $this->_executor->prime_post( $post_id );
}
/**
* Executes delayed cache operations.
*
* This method processes any delayed operations queued by the executor.
*
* @return bool True on success, false otherwise.
*/
public function execute_delayed_operations() {
return $this->_executor->execute_delayed_operations();
}
/**
* Executes delayed cache operations as part of a filter.
*
* This method ensures delayed operations are executed and returns the original value.
*
* @param mixed $v The value being filtered.
*
* @return mixed The unmodified value.
*/
public function execute_delayed_operations_filter( $v ) {
$this->execute_delayed_operations();
return $v;
}
}

View File

@@ -0,0 +1,438 @@
<?php
/**
* File: CacheFlush_Locally.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class CacheFlush_Locally
*
* W3 Cache flushing
*
* Priorities are very important for actions here.
* if e.g. CDN is flushed before local page cache - CDN can cache again
* still not flushed pages from local page cache.
* 100 - db
* 200 - 999 local objects, like object cache
* 1000 - 1999 local files (minify, pagecache)
* 2000 - 2999 local reverse proxies varnish, nginx
* 3000 - external caches like cdn, cloudflare
*
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
*/
class CacheFlush_Locally {
/**
* Flushes the database cache.
*
* Triggers the `w3tc_flush_dbcache` action and attempts to clear the database cache
* if supported by the `$wpdb` global.
*
* @param array $extras {
* Optional. Additional parameters for flushing. Default empty array.
*
* @type string $only Flush specific cache type.
* }
*
* @return bool|void True if cache flushed successfully, false otherwise, void if not applicable.
*/
public function dbcache_flush( $extras = array() ) {
if ( isset( $extras['only'] ) && 'dbcache' !== $extras['only'] ) {
return;
}
do_action( 'w3tc_flush_dbcache' );
if ( ! method_exists( $GLOBALS['wpdb'], 'flush_cache' ) ) {
return false;
}
return $GLOBALS['wpdb']->flush_cache( $extras );
}
/**
* Flushes the object cache.
*
* Triggers the `w3tc_flush_objectcache` and `w3tc_flush_after_objectcache` actions
* and clears the object cache using the appropriate component.
*
* @param array $extras {
* Optional. Additional parameters for flushing. Default empty array.
*
* @type string $only Flush specific cache type.
* }
*
* @return bool True if cache flushed successfully, false otherwise.
*/
public function objectcache_flush( $extras = array() ) {
if ( isset( $extras['only'] ) && 'objectcache' !== $extras['only'] ) {
return;
}
do_action( 'w3tc_flush_objectcache' );
$objectcache = Dispatcher::component( 'ObjectCache_WpObjectCache_Regular' );
$v = $objectcache->flush();
do_action( 'w3tc_flush_after_objectcache' );
return $v;
}
/**
* Flushes the fragment cache.
*
* Triggers the `w3tc_flush_fragmentcache` and `w3tc_flush_after_fragmentcache` actions
* and clears the fragment cache.
*
* @param array $extras {
* Optional. Additional parameters for flushing. Default empty array.
*
* @type string $only Flush specific cache type.
* }
*
* @return bool Always true.
*/
public function fragmentcache_flush( $extras = array() ) {
if ( isset( $extras['only'] ) && 'fragment' !== $extras['only'] ) {
return;
}
do_action( 'w3tc_flush_fragmentcache' );
do_action( 'w3tc_flush_after_fragmentcache' );
return true;
}
/**
* Flushes a specific fragment cache group.
*
* Triggers the `w3tc_flush_fragmentcache_group` and `w3tc_flush_after_fragmentcache_group` actions.
*
* @param string $group The fragment cache group to flush.
*
* @return bool Always true.
*/
public function fragmentcache_flush_group( $group ) {
do_action( 'w3tc_flush_fragmentcache_group', $group );
do_action( 'w3tc_flush_after_fragmentcache_group', $group );
return true;
}
/**
* Flushes the minify cache.
*
* Triggers the `w3tc_flush_minify` and `w3tc_flush_after_minify` actions and clears
* the minify cache using the appropriate component.
*
* @param array $extras {
* Optional. Additional parameters for flushing. Default empty array.
*
* @type string $only Flush specific cache type.
* }
*
* @return bool True if cache flushed successfully, false otherwise.
*/
public function minifycache_flush( $extras = array() ) {
if ( isset( $extras['only'] ) && 'minify' !== $extras['only'] ) {
return;
}
do_action( 'w3tc_flush_minify' );
$minifycache = Dispatcher::component( 'Minify_MinifiedFileRequestHandler' );
$v = $minifycache->flush( $extras );
do_action( 'w3tc_flush_after_minify' );
return $v;
}
/**
* Flushes all minify caches.
*
* Delegates the flush to the `minifycache_flush` method.
*
* @param array $extras Optional. Additional parameters for flushing. Default empty array.
*
* @return void
*/
public function minifycache_flush_all( $extras = array() ) {
$this->minifycache_flush( $extras );
}
/**
* Flushes the browser cache.
*
* Triggers the `w3tc_flush_browsercache` and `w3tc_flush_after_browsercache` actions
* and updates the browser cache flush timestamp.
*
* @param array $extras {
* Optional. Additional parameters for flushing. Default empty array.
*
* @type string $only Flush specific cache type. Defaults to 'browsercache'.
* }
*
* @return void
*/
public function browsercache_flush( $extras = array() ) {
if ( isset( $extras['only'] ) && 'browsercache' !== $extras['only'] ) {
return;
}
do_action( 'w3tc_flush_browsercache' );
update_option( 'w3tc_browsercache_flush_timestamp', wp_rand( 10000, 99999 ) . '' );
do_action( 'w3tc_flush_after_browsercache' );
}
/**
* Purges all content from the CDN.
*
* Applies the `w3tc_preflush_cdn_all` filter to determine whether the purge should occur.
* If true, it triggers the `w3tc_cdn_purge_all` and `w3tc_cdn_purge_all_after` actions
* and clears the CDN cache.
*
* @param array $extras Optional. Additional parameters for flushing. Default empty array.
*
* @return bool True if purge succeeded, false otherwise.
*/
public function cdn_purge_all( $extras = array() ) {
$do_flush = apply_filters( 'w3tc_preflush_cdn_all', true, $extras );
$v = false;
if ( $do_flush ) {
do_action( 'w3tc_cdn_purge_all' );
$cdn_core = Dispatcher::component( 'Cdn_Core' );
$cdn = $cdn_core->get_cdn();
$results = array();
$v = $cdn->purge_all( $results );
do_action( 'w3tc_cdn_purge_all_after' );
}
return $v;
}
/**
* Purges specific files from the CDN.
*
* Triggers the `w3tc_cdn_purge_files` and `w3tc_cdn_purge_files_after` actions and clears
* the specified files from the CDN cache.
*
* @param array $purgefiles List of files to purge from the CDN.
*
* @return bool True if purge succeeded, false otherwise.
*/
public function cdn_purge_files( $purgefiles ) {
do_action( 'w3tc_cdn_purge_files', $purgefiles );
$common = Dispatcher::component( 'Cdn_Core' );
$results = array();
$v = $common->purge( $purgefiles, $results );
do_action( 'w3tc_cdn_purge_files_after', $purgefiles );
return $v;
}
/**
* Flushes the OPcache.
*
* This method triggers the flushing of the OPcache using the `SystemOpCache_Core` component.
*
* @return bool True on success, false on failure.
*/
public function opcache_flush() {
$o = Dispatcher::component( 'SystemOpCache_Core' );
return $o->flush();
}
/**
* Flushes a specific post from the cache.
*
* Executes the `w3tc_flush_post` action if the `w3tc_preflush_post` filter allows it.
*
* @param int $post_id The ID of the post to flush.
* @param bool $force Optional. Whether to force the flush. Default false.
* @param mixed $extras Optional. Additional data passed to the filter and action.
*
* @return void
*/
public function flush_post( $post_id, $force = false, $extras = null ) {
$do_flush = apply_filters( 'w3tc_preflush_post', true, $extras );
if ( $do_flush ) {
do_action( 'w3tc_flush_post', $post_id, $force, $extras );
}
}
/**
* Flushes all posts from the cache.
*
* Executes the `w3tc_flush_posts` action if the `w3tc_preflush_posts` filter allows it.
*
* @param mixed $extras Optional. Additional data passed to the filter and action.
*
* @return void
*/
public function flush_posts( $extras = null ) {
$do_flush = apply_filters( 'w3tc_preflush_posts', true, $extras );
if ( $do_flush ) {
do_action( 'w3tc_flush_posts', $extras );
}
}
/**
* Flushes all cached content across multiple modules.
*
* Registers default actions for various cache components (OPcache, object cache, database cache, minify cache)
* and executes the `w3tc_flush_all` action if the `w3tc_preflush_all` filter allows it.
*
* @param mixed $extras Additional data passed to the filter and action.
*
* @return void
*/
public function flush_all( $extras ) {
static $default_actions_added = false;
if ( ! $default_actions_added ) {
$config = Dispatcher::config();
$opcache = Dispatcher::component( 'SystemOpCache_Core' );
if ( $opcache->is_enabled() ) {
add_action( 'w3tc_flush_all', array( $this, 'opcache_flush' ), 50, 1 );
}
if ( $config->get_boolean( 'dbcache.enabled' ) ) {
add_action( 'w3tc_flush_all', array( $this, 'dbcache_flush' ), 100, 2 );
}
if ( $config->getf_boolean( 'objectcache.enabled' ) ) {
add_action( 'w3tc_flush_all', array( $this, 'objectcache_flush' ), 200, 1 );
}
if ( $config->get_boolean( 'minify.enabled' ) ) {
add_action( 'w3tc_flush_all', array( $this, 'minifycache_flush_all' ), 1000, 1 );
}
$default_actions_added = true;
}
$do_flush = apply_filters( 'w3tc_preflush_all', true, $extras );
if ( $do_flush ) {
do_action( 'w3tc_flush_all', $extras );
}
}
/**
* Flushes a specific cache group.
*
* Executes the `w3tc_flush_group` action if the `w3tc_preflush_group` filter allows it.
*
* @param string $group The name of the group to flush.
* @param mixed $extras Additional data passed to the filter and action.
*
* @return void
*/
public function flush_group( $group, $extras ) {
$do_flush = apply_filters( 'w3tc_preflush_group', true, $group, $extras );
if ( $do_flush ) {
do_action( 'w3tc_flush_group', $group, $extras );
}
}
/**
* Flushes the cache for a specific URL.
*
* Executes the `w3tc_flush_url` action if the `w3tc_preflush_url` filter allows it.
*
* @param string $url The URL to flush.
* @param mixed $extras Optional. Additional data passed to the filter and action.
*
* @return void
*/
public function flush_url( $url, $extras = null ) {
$do_flush = apply_filters( 'w3tc_preflush_url', true, $extras );
if ( $do_flush ) {
do_action( 'w3tc_flush_url', $url, $extras );
}
}
/**
* Primes the cache for a specific post.
*
* Utilizes the `PgCache_Plugin_Admin` component to prime the post.
*
* @param int $post_id The ID of the post to prime.
*
* @return bool True on success, false on failure.
*/
public function prime_post( $post_id ) {
$pgcache = Dispatcher::component( 'PgCache_Plugin_Admin' );
return $pgcache->prime_post( $post_id );
}
/**
* Executes delayed cache operations for specific modules.
*
* Registers default delayed operations for page cache and Varnish, and triggers the operations
* using the `w3tc_flush_execute_delayed_operations` filter.
*
* @return array List of actions performed, with module names and error messages (empty if no error).
*/
public function execute_delayed_operations() {
static $default_actions_added = false;
if ( ! $default_actions_added ) {
$config = Dispatcher::config();
if ( $config->get_boolean( 'pgcache.enabled' ) ) {
add_filter( 'w3tc_flush_execute_delayed_operations', array( $this, '_execute_delayed_operations_pgcache' ), 1100 );
}
if ( $config->get_boolean( 'varnish.enabled' ) ) {
add_filter( 'w3tc_flush_execute_delayed_operations', array( $this, '_execute_delayed_operations_varnish' ), 2000 );
}
$default_actions_added = true;
}
// build response in a form 'module' => 'error message' (empty if no error).
$actions_made = array();
$actions_made = apply_filters( 'w3tc_flush_execute_delayed_operations', $actions_made );
return $actions_made;
}
/**
* Executes delayed operations for page cache.
*
* Flushes stale page cache entries and logs the action if successful.
*
* @param array $actions_made List of actions already performed.
*
* @return array Updated list of actions performed.
*/
public function _execute_delayed_operations_pgcache( $actions_made ) {
$o = Dispatcher::component( 'PgCache_Flush' );
$count_flushed = $o->flush_post_cleanup();
if ( $count_flushed > 0 ) {
$actions_made[] = array( 'module' => 'pgcache' );
}
return $actions_made;
}
/**
* Executes delayed operations for Varnish.
*
* Flushes stale Varnish cache entries and logs the action if successful.
*
* @param array $actions_made List of actions already performed.
*
* @return array Updated list of actions performed.
*/
public function _execute_delayed_operations_varnish( $actions_made ) {
$o = Dispatcher::component( 'Varnish_Flush' );
$count_flushed = $o->flush_post_cleanup();
if ( $count_flushed > 0 ) {
$actions_made[] = array( 'module' => 'varnish' );
}
return $actions_made;
}
}

View File

@@ -0,0 +1,281 @@
<?php
/**
* File: CacheGroups_Plugin_Admin.php
*
* @since 2.1.0
*
* @package W3TC
*/
namespace W3TC;
/**
* Class: CacheGroups_Plugin_Admin
*
* @since 2.1.0
*/
class CacheGroups_Plugin_Admin extends Base_Page_Settings {
/**
* Current page.
*
* @var string
*/
protected $_page = 'w3tc_cachegroups'; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
/**
* Cache groups settings view.
*
* @since 2.1.0
*/
public function view() {
$c = Dispatcher::config();
// Header.
require W3TC_INC_DIR . '/options/common/header.php';
// User agent groups.
$useragent_groups = array(
'value' => $c->get_array( 'mobile.rgroups' ),
'disabled' => $c->is_sealed( 'mobile.rgroups' ),
'description' =>
'<li>' .
__(
'Enabling even a single user agent group will set a cookie called "w3tc_referrer." It is used to ensure a consistent user experience across page views. Make sure any reverse proxy servers etc. respect this cookie for proper operation.',
'w3-total-cache'
) .
'</li>' .
'<li>' .
__(
'Per the above, make sure that visitors are notified about the cookie as per any regulations in your market.',
'w3-total-cache'
) .
'</li>',
);
$useragent_groups = apply_filters( 'w3tc_ui_config_item_mobile.rgroups', $useragent_groups ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
$w3_mobile = Dispatcher::component( 'Mobile_UserAgent' );
$useragent_themes = $w3_mobile->get_themes();
// Referrer groups.
$referrer_groups = $this->_config->get_array( 'referrer.rgroups' );
$w3_referrer = Dispatcher::component( 'Mobile_Referrer' );
$referrer_themes = $w3_referrer->get_themes();
// Cookie groups.
$cookie_groups = array(
'value' => $c->get_array( 'pgcache.cookiegroups.groups' ),
'disabled' => $c->is_sealed( 'pgcache.cookiegroups.groups' ),
);
$cookie_groups = apply_filters( 'w3tc_ui_config_item_pgcache.cookiegroups.groups', $cookie_groups ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
// Load view.
require W3TC_DIR . '/CacheGroups_Plugin_Admin_View.php';
}
/**
* Save settings.
*
* @since 2.1.0
*
* @static
*
* @param array $config Config.
*/
public static function w3tc_config_ui_save_w3tc_cachegroups( $config ) {
// * User agent groups.
$useragent_groups = Util_Request::get_array( 'mobile_groups' );
$mobile_groups = array();
$cached_mobile_groups = array();
foreach ( $useragent_groups as $group => $group_config ) {
$group = strtolower( $group );
$group = preg_replace( '~[^0-9a-z_]+~', '_', $group );
$group = trim( $group, '_' );
if ( $group ) {
$theme = isset( $group_config['theme'] ) ? trim( $group_config['theme'] ) : 'default';
$enabled = isset( $group_config['enabled'] ) ? (bool) $group_config['enabled'] : true;
$redirect = isset( $group_config['redirect'] ) ? trim( $group_config['redirect'] ) : '';
$agents = isset( $group_config['agents'] ) ? Util_Environment::textarea_to_array( $group_config['agents'] ) : array();
$mobile_groups[ $group ] = array(
'theme' => $theme,
'enabled' => $enabled,
'redirect' => $redirect,
'agents' => $agents,
);
$cached_mobile_groups[ $group ] = $agents;
}
}
// Allow plugins modify WPSC mobile groups.
$cached_mobile_groups = apply_filters( 'cached_mobile_groups', $cached_mobile_groups );
// Merge existent and delete removed groups.
foreach ( $mobile_groups as $group => $group_config ) {
if ( isset( $cached_mobile_groups[ $group ] ) ) {
$mobile_groups[ $group ]['agents'] = (array) $cached_mobile_groups[ $group ];
} else {
unset( $mobile_groups[ $group ] );
}
}
// Add new groups.
foreach ( $cached_mobile_groups as $group => $agents ) {
if ( ! isset( $mobile_groups[ $group ] ) ) {
$mobile_groups[ $group ] = array(
'theme' => '',
'enabled' => true,
'redirect' => '',
'agents' => $agents,
);
}
}
// Allow plugins modify W3TC mobile groups.
$mobile_groups = apply_filters( 'w3tc_mobile_groups', $mobile_groups );
// Sanitize mobile groups.
foreach ( $mobile_groups as $group => $group_config ) {
$mobile_groups[ $group ] = array_merge(
array(
'theme' => '',
'enabled' => true,
'redirect' => '',
'agents' => array(),
),
$group_config
);
$mobile_groups[ $group ]['agents'] = self::clean_values( $mobile_groups[ $group ]['agents'] );
sort( $mobile_groups[ $group ]['agents'] );
}
$enable_mobile = false;
foreach ( $mobile_groups as $group_config ) {
if ( $group_config['enabled'] ) {
$enable_mobile = true;
break;
}
}
$config->set( 'mobile.enabled', $enable_mobile );
$config->set( 'mobile.rgroups', $mobile_groups );
// * Referrer groups.
$ref_groups = Util_Request::get_array( 'referrer_groups' );
$referrer_groups = array();
foreach ( $ref_groups as $group => $group_config ) {
$group = strtolower( $group );
$group = preg_replace( '~[^0-9a-z_]+~', '_', $group );
$group = trim( $group, '_' );
if ( $group ) {
$theme = isset( $group_config['theme'] ) ? trim( $group_config['theme'] ) : 'default';
$enabled = isset( $group_config['enabled'] ) ? (bool) $group_config['enabled'] : true;
$redirect = isset( $group_config['redirect'] ) ? trim( $group_config['redirect'] ) : '';
$referrers = isset( $group_config['referrers'] ) ? Util_Environment::textarea_to_array( $group_config['referrers'] ) : array();
$referrer_groups[ $group ] = array(
'theme' => $theme,
'enabled' => $enabled,
'redirect' => $redirect,
'referrers' => $referrers,
);
}
}
// Allow plugins modify W3TC referrer groups.
$referrer_groups = apply_filters( 'w3tc_referrer_groups', $referrer_groups );
// Sanitize mobile groups.
foreach ( $referrer_groups as $group => $group_config ) {
$referrer_groups[ $group ] = array_merge(
array(
'theme' => '',
'enabled' => true,
'redirect' => '',
'referrers' => array(),
),
$group_config
);
$referrer_groups[ $group ]['referrers'] = self::clean_values( $referrer_groups[ $group ]['referrers'] );
sort( $referrer_groups[ $group ]['referrers'] );
}
$enable_referrer = false;
foreach ( $referrer_groups as $group_config ) {
if ( $group_config['enabled'] ) {
$enable_referrer = true;
break;
}
}
$config->set( 'referrer.enabled', $enable_referrer );
$config->set( 'referrer.rgroups', $referrer_groups );
// * Cookie groups.
$mobile_groups = array();
$cached_mobile_groups = array();
$cookie_groups = Util_Request::get_array( 'cookiegroups' );
foreach ( $cookie_groups as $group => $group_config ) {
$group = strtolower( $group );
$group = preg_replace( '~[^0-9a-z_]+~', '_', $group );
$group = trim( $group, '_' );
if ( $group ) {
$enabled = isset( $group_config['enabled'] ) ? (bool) $group_config['enabled'] : false;
$cache = isset( $group_config['cache'] ) ? (bool) $group_config['cache'] : false;
$cookies = isset( $group_config['cookies'] ) ? Util_Environment::textarea_to_array( $group_config['cookies'] ) : array();
$cookiegroups[ $group ] = array(
'enabled' => $enabled,
'cache' => $cache,
'cookies' => $cookies,
);
}
}
// Allow plugins modify W3TC cookie groups.
$cookiegroups = apply_filters( 'w3tc_pgcache_cookiegroups', $cookiegroups );
$enabled = false;
foreach ( $cookiegroups as $group_config ) {
if ( $group_config['enabled'] ) {
$enabled = true;
break;
}
}
$config->set( 'pgcache.cookiegroups.enabled', $enabled );
$config->set( 'pgcache.cookiegroups.groups', $cookiegroups );
}
/**
* Clean entries.
*
* @static
*
* @param array $values Values.
*/
public static function clean_values( $values ) {
return array_unique(
array_map(
function ( $value ) {
return preg_replace( '/(?<!\\\\)' . wp_spaces_regexp() . '/', '\ ', strtolower( $value ) );
},
$values
)
);
}
}

View File

@@ -0,0 +1,425 @@
/**
* File: CacheGroups_Plugin_Admin_View.js
*
* @since 2.1.0
*
* @package W3TC
*/
jQuery(function() {
jQuery(document).on( 'submit', '#cachegroups_form', function() {
var error = [];
var mobile_groups = jQuery('#mobile_groups li');
mobile_groups.each(function(index, mobile_group) {
var $mobile_group = jQuery(mobile_group);
if ($mobile_group.find('.mobile_group_enabled:checked').length) {
var name = $mobile_group.find('.mobile_group').text();
var theme = $mobile_group.find('select').val();
var redirect = $mobile_group.find('input[type=text]').val();
var agents = $mobile_group.find('textarea').val().split("\n").filter(function(line){return line.trim()!==''}).map(function(line){return line.trim();});
mobile_groups.not($mobile_group).each(function(index, compare_mobile_group) {
var $compare_mobile_group = jQuery(compare_mobile_group);
if ($compare_mobile_group.find('.mobile_group_enabled:checked').length) {
var compare_name = $compare_mobile_group.find('.mobile_group').text();
var compare_theme = $compare_mobile_group.find('select').val();
var compare_redirect = $compare_mobile_group.find('input[type=text]').val();
var compare_agents = $compare_mobile_group.find('textarea').val().split("\n").filter(function(line){return line.trim()!==''}).map(function(line){return line.trim();});
var groups = sort_array([name, compare_name]);
if (compare_redirect === '' && redirect === '' && compare_theme !== '' && compare_theme === theme) {
error.push('Duplicate theme "' + compare_theme + '" found in the user agent groups "' + groups[0] + '" and "' + groups[1] + '"');
}
if (compare_redirect !== '' && compare_redirect === redirect) {
error.push('Duplicate redirect "' + compare_redirect + '" found in the user agent groups "' + groups[0] + '" and "' + groups[1] + '"');
}
jQuery.each(compare_agents, function(index, value) {
if (jQuery.inArray(value, agents) !== -1) {
error.push('Duplicate stem "' + value + '" found in the user agent groups "' + groups[0] + '" and "' + groups[1] + '"');
}
});
}
});
}
});
var referrer_groups = jQuery('#referrer_groups li');
referrer_groups.each(function(index, referrer_group) {
var $referrer_group = jQuery(referrer_group);
if ($referrer_group.find('.referrer_group_enabled:checked').length) {
var name = $referrer_group.find('.referrer_group').text();
var theme = $referrer_group.find('select').val();
var redirect = $referrer_group.find('input[type=text]').val();
var agents = $referrer_group.find('textarea').val().split("\n").filter(function(line){return line.trim()!==''}).map(function(line){return line.trim();});
referrer_groups.not($referrer_group).each(function(index, compare_referrer_group) {
var $compare_referrer_group = jQuery(compare_referrer_group);
if ($compare_referrer_group.find('.referrer_group_enabled:checked').length) {
var compare_name = $compare_referrer_group.find('.referrer_group').text();
var compare_theme = $compare_referrer_group.find('select').val();
var compare_redirect = $compare_referrer_group.find('input[type=text]').val();
var compare_agents = $compare_referrer_group.find('textarea').val().split("\n").filter(function(line){return line.trim()!==''}).map(function(line){return line.trim();});
var groups = sort_array([name, compare_name]);
if (compare_redirect === '' && redirect === '' && compare_theme !== '' && compare_theme === theme) {
error.push('Duplicate theme "' + compare_theme + '" found in the referrer groups "' + groups[0] + '" and "' + groups[1] + '"');
}
if (compare_redirect !== '' && compare_redirect === redirect) {
error.push('Duplicate redirect "' + compare_redirect + '" found in the referrer groups "' + groups[0] + '" and "' + groups[1] + '"');
}
jQuery.each(compare_agents, function(index, value) {
if (jQuery.inArray(value, agents) !== -1) {
error.push('Duplicate stem "' + value + '" found in the referrer groups "' + groups[0] + '" and "' + groups[1] + '"');
}
});
}
});
}
});
var cookiegroups = jQuery('#cookiegroups li');
cookiegroups.each(function(index, cookiegroup) {
var $cookiegroup = jQuery(cookiegroup);
if ($cookiegroup.find('.cookiegroup_enabled:checked').length) {
var name = $cookiegroup.find('.cookiegroup_name').text();
var agents = $cookiegroup.find('textarea').val().split("\n").filter(function(line){return line.trim()!==''}).map(function(line){return line.trim();});
console.log(agents);
cookiegroups.not($cookiegroup).each(function(index, compare_cookiegroup) {
var $compare_cookiegroup = jQuery(compare_cookiegroup);
if ($compare_cookiegroup.find('.cookiegroup_enabled:checked').length) {
var compare_name = $compare_cookiegroup.find('.cookiegroup_name').text();
var compare_agents = $compare_cookiegroup.find('textarea').val().split("\n").filter(function(line){return line.trim()!==''}).map(function(line){return line.trim();});
console.log(compare_agents);
var groups = sort_array([name, compare_name]);
jQuery.each(compare_agents, function(index, value) {
if (jQuery.inArray(value, agents) !== -1) {
error.push('Duplicate stem "' + value + '" found in the cookie groups "' + groups[0] + '" and "' + groups[1] + '"');
}
});
}
});
}
});
if (error.length !== 0) {
alert(unique_array(error).join('\n'));
return false;
}
return true;
});
jQuery(document).on( 'click', '#mobile_add', function() {
var group = prompt('Enter group name (only "0-9", "a-z", "_" symbols are allowed).');
if (group !== null) {
group = group.toLowerCase();
group = group.replace(/[^0-9a-z_]+/g, '_');
group = group.replace(/^_+/, '');
group = group.replace(/_+$/, '');
if (group) {
var exists = false;
jQuery('.mobile_group').each(function() {
if (jQuery(this).html() == group) {
alert('Group already exists!');
exists = true;
return false;
}
});
if (!exists) {
var li = jQuery('<li id="mobile_group_' + group + '">' +
'<table class="form-table">' +
'<tr>' +
'<th>Group name:</th>' +
'<td>' +
'<span class="mobile_group_number">' + (jQuery('#mobile_groups li').length + 1) + '.</span> ' +
'<span class="mobile_group">' + group + '</span> ' +
'<input type="button" class="button mobile_delete" value="Delete group" />' +
'</td>' +
'</tr>' +
'<tr>' +
'<th><label for="mobile_groups_' + group + '_enabled">Enabled:</label></th>' +
'<td>' +
'<input type="hidden" name="mobile_groups[' + group + '][enabled]" value="0" />' +
'<input id="mobile_groups_' + group + '_enabled" class="mobile_group_enabled" type="checkbox" name="mobile_groups[' + group + '][enabled]" value="1" checked="checked" />' +
'</td>' +
'</tr>' +
'<tr>' +
'<th><label for="mobile_groups_' + group + '_theme">Theme:</label></th>' +
'<td>' +
'<select id="mobile_groups_' + group + '_theme" name="mobile_groups[' + group + '][theme]"><option value="">-- Pass-through --</option></select>' +
'<p class="description">Assign this group of user agents to a specific them. Leaving this option "Active Theme" allows any plugins you have (e.g. mobile plugins) to properly handle requests for these user agents. If the "redirect users to" field is not empty, this setting is ignored.</p>' +
'</td>' +
'</tr>' +
'<tr>' +
'<th><label for="mobile_groups_' + group + '_redirect">Redirect users to:</label></th>' +
'<td>' +
'<input id="mobile_groups_' + group + '_redirect" type="text" name="mobile_groups[' + group + '][redirect]" value="" size="60" />' +
'<p class="description">A 302 redirect is used to send this group of users to another hostname (domain); recommended if a 3rd party service provides a mobile version of your site.</p>' +
'</td>' +
'</tr>' +
'<tr>' +
'<th><label for="mobile_groups_' + group + '_agents">User agents:</label></th>' +
'<td>' +
'<textarea id="mobile_groups_' + group + '_agents" name="mobile_groups[' + group + '][agents]" rows="10" cols="50"></textarea>' +
'<p class="description">Specify the user agents for this group.</p>' +
'</td>' +
'</tr>' +
'</table>' +
'</li>');
var select = li.find('select');
jQuery.each(mobile_themes, function(index, value) {
select.append(jQuery('<option />').val(index).html(value));
});
jQuery('#mobile_groups').append(li);
w3tc_mobile_groups_clear();
window.location.hash = '#mobile_group_' + group;
li.find('textarea').focus();
}
} else {
alert('Empty group name!');
}
}
});
jQuery(document).on('click', '.mobile_delete', function () {
if (confirm('Are you sure want to delete this group?')) {
jQuery(this).parents('#mobile_groups li').remove();
w3tc_mobile_groups_clear();
w3tc_beforeupload_bind();
}
});
w3tc_mobile_groups_clear();
// Referrer groups.
jQuery(document).on( 'click', '#referrer_add', function() {
var group = prompt('Enter group name (only "0-9", "a-z", "_" symbols are allowed).');
if (group !== null) {
group = group.toLowerCase();
group = group.replace(/[^0-9a-z_]+/g, '_');
group = group.replace(/^_+/, '');
group = group.replace(/_+$/, '');
if (group) {
var exists = false;
jQuery('.referrer_group').each(function() {
if (jQuery(this).html() == group) {
alert('Group already exists!');
exists = true;
return false;
}
});
if (!exists) {
var li = jQuery('<li id="referrer_group_' + group + '">' +
'<table class="form-table">' +
'<tr>' +
'<th>Group name:</th>' +
'<td>' +
'<span class="referrer_group_number">' + (jQuery('#referrer_groups li').length + 1) + '.</span> ' +
'<span class="referrer_group">' + group + '</span> ' +
'<input type="button" class="button referrer_delete" value="Delete group" />' +
'</td>' +
'</tr>' +
'<tr>' +
'<th>' +
'<label for="referrer_groups_' + group + '_enabled">Enabled:</label>' +
'</th>' +
'<td>' +
'<input type="hidden" name="referrer_groups[' + group + '][enabled]" value="0" />' +
'<input id="referrer_groups_' + group + '_enabled" class="referrer_group_enabled" type="checkbox" name="referrer_groups[' + group + '][enabled]" value="1" checked="checked" />' +
'</td>' +
'</tr>' +
'<tr>' +
'<th><label for="referrer_groups_' + group + '_theme">Theme:</label></th>' +
'<td>' +
'<select id="referrer_groups_' + group + '_theme" name="referrer_groups[' + group + '][theme]"><option value="">-- Pass-through --</option></select>' +
'<p class="description">Assign this group of referrers to a specific them. Leaving this option "Active Theme" allows any plugins you have (e.g. referrer plugins) to properly handle requests for these referrers. If the "redirect users to" field is not empty, this setting is ignored.</p>' +
'</td>' +
'</tr>' +
'<tr>' +
'<th><label for="referrer_groups_' + group + '_redirect">Redirect users to:</label></th>' +
'<td>' +
'<input id="referrer_groups_' + group + '_redirect" type="text" name="referrer_groups[' + group + '][redirect]" value="" size="60" />' +
'<p class="description">A 302 redirect is used to send this group of users to another hostname (domain); recommended if a 3rd party service provides a referrer version of your site.</p>' +
'</td>' +
'</tr>' +
'<tr>' +
'<th><label for="referrer_groups_' + group + '_referrers">Referrers:</label></th>' +
'<td>' +
'<textarea id="referrer_groups_' + group + '_referrers" name="referrer_groups[' + group + '][referrers]" rows="10" cols="50"></textarea>' +
'<p class="description">Specify the referrers for this group.</p>' +
'</td>' +
'</tr>' +
'</table>' +
'</li>');
var select = li.find('select');
jQuery.each(referrer_themes, function(index, value) {
select.append(jQuery('<option />').val(index).html(value));
});
jQuery('#referrer_groups').append(li);
w3tc_referrer_groups_clear();
window.location.hash = '#referrer_group_' + group;
li.find('textarea').focus();
}
} else {
alert('Empty group name!');
}
}
});
jQuery(document).on('click', '.referrer_delete', function () {
if (confirm('Are you sure want to delete this group?')) {
jQuery(this).parents('#referrer_groups li').remove();
w3tc_referrer_groups_clear();
w3tc_beforeupload_bind();
}
});
w3tc_referrer_groups_clear();
// Cookie groups.
jQuery(document).on( 'click', '#w3tc_cookiegroup_add', function() {
var group = prompt('Enter group name (only "0-9", "a-z", "_" symbols are allowed).');
if (group !== null) {
group = group.toLowerCase();
group = group.replace(/[^0-9a-z_]+/g, '_');
group = group.replace(/^_+/, '');
group = group.replace(/_+$/, '');
if (group) {
var exists = false;
jQuery('.cookiegroup_name').each(function() {
if (jQuery(this).html() == group) {
alert('Group already exists!');
exists = true;
return false;
}
});
if (!exists) {
var li = jQuery('<li id="cookiegroup_' + group + '">' +
'<table class="form-table">' +
'<tr>' +
'<th>Group name:</th>' +
'<td>' +
'<span class="cookiegroup_number">' + (jQuery('#cookiegroups li').length + 1) + '.</span> ' +
'<span class="cookiegroup_name">' + group + '</span> ' +
'<input type="button" class="button w3tc_cookiegroup_delete" value="Delete group" />' +
'</td>' +
'</tr>' +
'<tr>' +
'<th><label for="cookiegroup_' + group + '_enabled">Enabled:</label></th>' +
'<td>' +
'<input id="cookiegroup_' + group + '_enabled" class="cookiegroup_enabled" type="checkbox" name="cookiegroups[' + group + '][enabled]" value="1" checked="checked" />' +
'</td>' +
'</tr>' +
'<tr>' +
'<th><label for="cookiegroup_' + group + '_cache">Cache:</label></th>' +
'<td>' +
'<input id="cookiegroup_' + group + '_cache" type="checkbox" name="cookiegroups[' + group + '][cache]" value="1" checked="checked" /></td>' +
'</tr>' +
'<tr>' +
'<th><label for="cookiegroups_' + group + '_cookies">Cookies:</label></th>' +
'<td>' +
'<textarea id="cookiegroups_' + group + '_cookies" name="cookiegroups[' + group + '][cookies]" rows="10" cols="50"></textarea>' +
'<p class="description">Specify the cookies for this group. Values like \'cookie\', \'cookie=value\', and cookie[a-z]+=value[a-z]+are supported. Remember to escape special characters like spaces, dots or dashes with a backslash. Regular expressions are also supported.</p>' +
'</td>' +
'</tr>' +
'</table>' +
'</li>');
var select = li.find('select');
jQuery('#cookiegroups').append(li);
w3tc_cookiegroups_clear();
window.location.hash = '#cookiegroup_' + group;
li.find('textarea').focus();
}
} else {
alert('Empty group name!');
}
}
});
jQuery(document).on( 'click', '.w3tc_cookiegroup_delete', function () {
if (confirm('Are you sure want to delete this group?')) {
jQuery(this).parents('#cookiegroups li').remove();
w3tc_cookiegroups_clear();
w3tc_beforeupload_bind();
}
});
w3tc_cookiegroups_clear();
// Add sortable.
if (jQuery.ui && jQuery.ui.sortable) {
jQuery('#cookiegroups').sortable({
axis: 'y',
stop: function() {
jQuery('#cookiegroups').find('.cookiegroup_number').each(function(index) {
jQuery(this).html((index + 1) + '.');
});
}
});
}
});
function w3tc_mobile_groups_clear() {
if (!jQuery('#mobile_groups li').length) {
jQuery('#mobile_groups_empty').show();
} else {
jQuery('#mobile_groups_empty').hide();
}
}
function w3tc_referrer_groups_clear() {
if (!jQuery('#referrer_groups li').length) {
jQuery('#referrer_groups_empty').show();
} else {
jQuery('#referrer_groups_empty').hide();
}
}
function w3tc_cookiegroups_clear() {
if (!jQuery('#cookiegroups li').length) {
jQuery('#cookiegroups_empty').show();
} else {
jQuery('#cookiegroups_empty').hide();
}
}
function unique_array(array) {
return jQuery.grep(array,function(el,i){return i === jQuery.inArray(el,array)});
}
function sort_array(array) {
return array.sort();
}

View File

@@ -0,0 +1,341 @@
<?php
/**
* File: CacheGroups_Plugin_Admin_View.php
*
* @since 2.1.0
*
* @package W3TC
*
* @uses $useragent_groups
* @uses $useragent_themes
* @uses $referrer_groups
* @uses $referrer_themes
* @uses $cookie_groups
*/
namespace W3TC;
if ( ! defined( 'W3TC' ) ) {
die();
}
?>
<form id="cachegroups_form" action="admin.php?page=<?php echo esc_attr( $this->_page ); ?>" method="post">
<?php Util_UI::print_control_bar( 'cachegroups_form_control' ); ?>
<!-- User Agenet Groups -->
<script type="text/javascript">/*<![CDATA[*/
var mobile_themes = {};
<?php foreach ( $useragent_themes as $theme_key => $theme_name ) : ?>
mobile_themes['<?php echo esc_attr( addslashes( $theme_key ) ); ?>'] = '<?php echo esc_html( addslashes( $theme_name ) ); ?>';
<?php endforeach; ?>
/*]]>*/</script>
<div class="metabox-holder">
<?php Util_Ui::postbox_header( esc_html__( 'Manage User Agent Groups', 'w3-total-cache' ), '', 'manage-uag' ); ?>
<p>
<input id="mobile_add" type="button" class="button"
<?php disabled( $useragent_groups['disabled'] ); ?>
value="<?php esc_html_e( 'Create a group', 'w3-total-cache' ); ?>" />
<?php esc_html_e( 'of user agents by specifying names in the user agents field. Assign a set of user agents to use a specific theme, redirect them to another domain or if an existing mobile plugin is active, create user agent groups to ensure that a unique cache is created for each user agent group. Drag and drop groups into order (if needed) to determine their priority (top -&gt; down).', 'w3-total-cache' ); ?>
</p>
<ul id="mobile_groups">
<?php
$index = 0;
foreach ( $useragent_groups['value'] as $group => $group_config ) :
++$index;
?>
<li id="mobile_group_<?php echo esc_attr( $group ); ?>">
<table class="form-table">
<tr>
<th>
<?php esc_html_e( 'Group name:', 'w3-total-cache' ); ?>
</th>
<td>
<span class="mobile_group_number"><?php echo esc_attr( $index ); ?>.</span> <span class="mobile_group"><?php echo esc_html( $group ); // phpcs:ignore ?></span>
<input type="button" class="button mobile_delete"
value="<?php esc_html_e( 'Delete group', 'w3-total-cache' ); ?>"
<?php disabled( $useragent_groups['disabled'] ); ?> />
</td>
</tr>
<tr>
<th>
<label for="mobile_groups_<?php echo esc_attr( $group ); ?>_enabled"><?php esc_html_e( 'Enabled:', 'w3-total-cache' ); ?></label>
</th>
<td>
<input type="hidden" name="mobile_groups[<?php echo esc_attr( $group ); ?>][enabled]" value="0" />
<input id="mobile_groups_<?php echo esc_attr( $group ); ?>_enabled"
class="mobile_group_enabled" type="checkbox"
name="mobile_groups[<?php echo esc_attr( $group ); ?>][enabled]"
<?php disabled( $useragent_groups['disabled'] ); ?> value="1"
<?php checked( $group_config['enabled'], true ); ?> />
</td>
</tr>
<tr>
<th>
<label for="mobile_groups_<?php echo esc_attr( $group ); ?>_theme"><?php esc_html_e( 'Theme:', 'w3-total-cache' ); ?></label>
</th>
<td>
<select id="mobile_groups_<?php echo esc_attr( $group ); ?>_theme"
name="mobile_groups[<?php echo esc_attr( $group ); ?>][theme]"
<?php disabled( $useragent_groups['disabled'] ); ?> >
<option value=""><?php esc_html_e( '-- Pass-through --', 'w3-total-cache' ); ?></option>
<?php foreach ( $useragent_themes as $theme_key => $theme_name ) : ?>
<option value="<?php echo esc_attr( $theme_key ); ?>"<?php selected( $theme_key, $group_config['theme'] ); ?>><?php echo esc_html( $theme_name ); ?></option>
<?php endforeach; ?>
</select>
<p class="description">
<?php esc_html_e( 'Assign this group of user agents to a specific theme. Selecting "Pass-through" allows any plugin(s) (e.g. mobile plugins) to properly handle requests for these user agents. If the "redirect users to" field is not empty, this setting is ignored.', 'w3-total-cache' ); ?>
</p>
</td>
</tr>
<tr>
<th>
<label for="mobile_groups_<?php echo esc_attr( $group ); ?>_redirect"><?php esc_html_e( 'Redirect users to:', 'w3-total-cache' ); ?></label>
</th>
<td>
<input id="mobile_groups_<?php echo esc_attr( $group ); ?>_redirect"
type="text" name="mobile_groups[<?php echo esc_attr( $group ); ?>][redirect]"
value="<?php echo esc_attr( $group_config['redirect'] ); ?>"
<?php disabled( $useragent_groups['disabled'] ); ?>
size="60" />
<p class="description"><?php esc_html_e( 'A 302 redirect is used to send this group of users to another hostname (domain); recommended if a 3rd party service provides a mobile version of your site.', 'w3-total-cache' ); ?></p>
</td>
</tr>
<tr>
<th>
<label for="mobile_groups_<?php echo esc_attr( $group ); ?>_agents"><?php esc_html_e( 'User agents:', 'w3-total-cache' ); ?></label>
</th>
<td>
<textarea id="mobile_groups_<?php echo esc_attr( $group ); ?>_agents"
name="mobile_groups[<?php echo esc_attr( $group ); ?>][agents]"
rows="10" cols="50" <?php disabled( $useragent_groups['disabled'] ); ?>><?php echo esc_textarea( implode( "\r\n", (array) $group_config['agents'] ) ); ?></textarea>
<p class="description">
<?php esc_html_e( 'Specify the user agents for this group. Remember to escape special characters like spaces, dots or dashes with a backslash. Regular expressions are also supported.', 'w3-total-cache' ); ?>
</p>
</td>
</tr>
</table>
</li>
<?php endforeach; ?>
</ul>
<div id="mobile_groups_empty" style="display: none;"><?php esc_html_e( 'No groups added. All user agents receive the same page and minify cache results.', 'w3-total-cache' ); ?></div>
<?php
Util_Ui::postbox_footer();
Util_Ui::postbox_header(
__( 'Note(s):', 'w3-total-cache' ),
'',
'notes'
);
?>
<table class="form-table">
<tr>
<th colspan="2">
<ul>
<?php echo $useragent_groups['description']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</ul>
</th>
</tr>
</table>
<?php Util_Ui::postbox_footer(); ?>
</div>
<!-- Referrer Groups -->
<script type="text/javascript">/*<![CDATA[*/
var referrer_themes = {};
<?php foreach ( $referrer_themes as $theme_key => $theme_name ) : ?>
referrer_themes['<?php echo esc_attr( $theme_key ); ?>'] = '<?php echo esc_html( $theme_name ); ?>';
<?php endforeach; ?>
/*]]>*/</script>
<div class="metabox-holder">
<?php Util_Ui::postbox_header( esc_html__( 'Manage Referrer Groups', 'w3-total-cache' ), '', 'manage-rg' ); ?>
<p>
<input id="referrer_add" type="button" class="button" value="<?php esc_html_e( 'Create a group', 'w3-total-cache' ); ?>" /> <?php esc_html_e( 'of referrers by specifying names in the referrers field. Assign a set of referrers to use a specific theme, redirect them to another domain, create referrer groups to ensure that a unique cache is created for each referrer group. Drag and drop groups into order (if needed) to determine their priority (top -&gt; down).', 'w3-total-cache' ); ?>
</p>
<ul id="referrer_groups">
<?php
$index = 0;
foreach ( $referrer_groups as $group => $group_config ) :
++$index;
?>
<li id="referrer_group_<?php echo esc_attr( $group ); ?>">
<table class="form-table">
<tr>
<th>
<?php esc_html_e( 'Group name:', 'w3-total-cache' ); ?>
</th>
<td>
<span class="referrer_group_number"><?php echo esc_attr( $index ); ?>.</span> <span class="referrer_group"><?php echo esc_html( $group ); ?></span> <input type="button" class="button referrer_delete" value="<?php esc_html_e( 'Delete group', 'w3-total-cache' ); ?>" />
</td>
</tr>
<tr>
<th>
<label for="referrer_groups_<?php echo esc_attr( $group ); ?>_enabled"><?php esc_html_e( 'Enabled:', 'w3-total-cache' ); ?></label>
</th>
<td>
<input type="hidden" name="referrer_groups[<?php echo esc_attr( $group ); ?>][enabled]" value="0" />
<input id="referrer_groups_<?php echo esc_attr( $group ); ?>_enabled"
class="referrer_group_enabled" type="checkbox"
name="referrer_groups[<?php echo esc_attr( $group ); ?>][enabled]"
value="1"<?php checked( $group_config['enabled'], true ); ?> />
</td>
</tr>
<tr>
<th>
<label for="referrer_groups_<?php echo esc_attr( $group ); ?>_theme"><?php esc_html_e( 'Theme:', 'w3-total-cache' ); ?></label>
</th>
<td>
<select id="referrer_groups_<?php echo esc_attr( $group ); ?>_theme" name="referrer_groups[<?php echo esc_attr( $group ); ?>][theme]">
<option value=""><?php esc_html_e( '-- Pass-through --', 'w3-total-cache' ); ?></option>
<?php foreach ( $referrer_themes as $theme_key => $theme_name ) : ?>
<option value="<?php echo esc_attr( $theme_key ); ?>"<?php selected( $theme_key, $group_config['theme'] ); ?>><?php echo esc_html( $theme_name ); ?></option>
<?php endforeach; ?>
</select>
<p class="description"><?php esc_html_e( 'Assign this group of referrers to a specific theme. Selecting "Pass-through" allows any plugin(s) (e.g. referrer plugins) to properly handle requests for these referrers. If the "redirect users to" field is not empty, this setting is ignored.', 'w3-total-cache' ); ?></p>
</td>
</tr>
<tr>
<th>
<label for="referrer_groups_<?php echo esc_attr( $group ); ?>_redirect"><?php esc_html_e( 'Redirect users to:', 'w3-total-cache' ); ?></label>
</th>
<td>
<input id="referrer_groups_<?php echo esc_attr( $group ); ?>_redirect" type="text" name="referrer_groups[<?php echo esc_attr( $group ); ?>][redirect]" value="<?php echo esc_attr( $group_config['redirect'] ); ?>" size="60" />
<p class="description"><?php esc_html_e( 'A 302 redirect is used to send this group of referrers to another hostname (domain).', 'w3-total-cache' ); ?></p>
</td>
</tr>
<tr>
<th>
<label for="referrer_groups_<?php echo esc_attr( $group ); ?>_referrers"><?php esc_html_e( 'Referrers:', 'w3-total-cache' ); ?></label>
</th>
<td>
<textarea id="referrer_groups_<?php echo esc_attr( $group ); ?>_referrers" name="referrer_groups[<?php echo esc_attr( $group ); ?>][referrers]" rows="10" cols="50"><?php echo esc_textarea( implode( "\r\n", (array) $group_config['referrers'] ) ); ?></textarea>
<p class="description"><?php esc_html_e( 'Specify the referrers for this group. Remember to escape special characters like spaces, dots or dashes with a backslash. Regular expressions are also supported.', 'w3-total-cache' ); ?></p>
</td>
</tr>
</table>
</li>
<?php endforeach; ?>
</ul>
<div id="referrer_groups_empty" style="display: none;"><?php esc_html_e( 'No groups added. All referrers receive the same page and minify cache results.', 'w3-total-cache' ); ?></div>
<?php Util_Ui::postbox_footer(); ?>
</div>
<!-- Cookie Groups -->
<div class="metabox-holder">
<?php Util_Ui::postbox_header( esc_html__( 'Manage Cookie Groups', 'w3-total-cache' ), '', 'manage-cg' ); ?>
<p>
<input id="w3tc_cookiegroup_add" type="button" class="button"
<?php disabled( $cookie_groups['disabled'] ); ?>
value="<?php esc_html_e( 'Create a group', 'w3-total-cache' ); ?>" />
<?php esc_html_e( 'of Cookies by specifying names in the Cookies field. Assign a set of Cookies to ensure that a unique cache is created for each Cookie group. Drag and drop groups into order (if needed) to determine their priority (top -&gt; down).', 'w3-total-cache' ); ?>
</p>
<ul id="cookiegroups" class="w3tc_cachegroups">
<?php
$index = 0;
foreach ( $cookie_groups['value'] as $group => $group_config ) :
++$index;
?>
<li id="cookiegroup_<?php echo esc_attr( $group ); ?>">
<table class="form-table">
<tr>
<th>
<?php esc_html_e( 'Group name:', 'w3-total-cache' ); ?>
</th>
<td>
<span class="cookiegroup_number"><?php echo esc_attr( $index ); ?>.</span>
<span class="cookiegroup_name"><?php echo htmlspecialchars( $group ); // phpcs:ignore ?></span>
<input type="button" class="button w3tc_cookiegroup_delete"
value="<?php esc_html_e( 'Delete group', 'w3-total-cache' ); ?>"
<?php disabled( $cookie_groups['disabled'] ); ?> />
</td>
</tr>
<tr>
<th>
<label for="cookiegroup_<?php echo esc_attr( $group ); ?>_enabled">
<?php esc_html_e( 'Enabled:', 'w3-total-cache' ); ?>
</label>
</th>
<td>
<input id="cookiegroup_<?php echo esc_attr( $group ); ?>_enabled"
class="cookiegroup_enabled" type="checkbox"
name="cookiegroups[<?php echo esc_attr( $group ); ?>][enabled]"
<?php disabled( $cookie_groups['disabled'] ); ?> value="1"
<?php checked( $group_config['enabled'], true ); ?> />
</td>
</tr>
<tr>
<th>
<label for="cookiegroup_<?php echo esc_attr( $group ); ?>_cache">
<?php esc_html_e( 'Cache:', 'w3-total-cache' ); ?>
</label>
</th>
<td>
<input id="cookiegroup_<?php echo esc_attr( $group ); ?>_cache"
type="checkbox"
name="cookiegroups[<?php echo esc_attr( $group ); ?>][cache]"
<?php disabled( $cookie_groups['disabled'] ); ?> value="1"
<?php checked( $group_config['cache'], true ); ?> /> <?php esc_html_e( 'Enable', 'w3-total-cache' ); ?>
<p class="description"><?php esc_html_e( 'Controls whether web pages can be cached or not when cookies from this group are detected.', 'w3-total-cache' ); ?></p>
</td>
</tr>
<tr>
<th>
<label for="cookiegroup_<?php echo esc_attr( $group ); ?>_cookies">
<?php esc_html_e( 'Cookies:', 'w3-total-cache' ); ?>
</label>
</th>
<td>
<textarea id="cookiegroup_<?php echo esc_attr( $group ); ?>_cookies"
name="cookiegroups[<?php echo esc_attr( $group ); ?>][cookies]"
rows="10" cols="50" <?php disabled( $cookie_groups['disabled'] ); ?>><?php echo esc_textarea( implode( "\r\n", (array) $group_config['cookies'] ) ); ?></textarea>
<p class="description">
<?php esc_html_e( 'Specify the cookies for this group. Values like \'cookie\', \'cookie=value\', and cookie[a-z]+=value[a-z]+ are supported. Remember to escape special characters like spaces, dots or dashes with a backslash. Regular expressions are also supported.', 'w3-total-cache' ); ?>
</p>
</td>
</tr>
</table>
</li>
<?php endforeach; ?>
</ul>
<div id="cookiegroups_empty" style="display: none;"><?php esc_html_e( 'No groups added. All Cookies receive the same page and minify cache results.', 'w3-total-cache' ); ?></div>
<?php
Util_Ui::postbox_footer();
Util_Ui::postbox_header(
__( 'Note(s):', 'w3-total-cache' ),
'',
'notes'
);
?>
<table class="form-table">
<tr>
<th colspan="2">
<ul>
<li>
<?php esc_html_e( 'Content is cached for each group separately.', 'w3-total-cache' ); ?>
</li>
<li>
<?php esc_html_e( 'Per the above, make sure that visitors are notified about the cookie as per any regulations in your market.', 'w3-total-cache' ); ?>
</li>
</ul>
</th>
</tr>
</table>
<?php Util_Ui::postbox_footer(); ?>
</div>
</form>

View File

@@ -0,0 +1,323 @@
<?php
/**
* File: Cache_Apc.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cache_Apc
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
* phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
*/
class Cache_Apc extends Cache_Base {
/**
* Used for faster flushing
*
* @var integer $_key_version
*/
private $_key_version = array();
/**
* Adds data
*
* @param string $key Key.
* @param mixed $value Value.
* @param integer $expire Time to expire.
* @param string $group Used to differentiate between groups of cache values.
*
* @return boolean
*/
public function add( $key, &$value, $expire = 0, $group = '' ) {
if ( $this->get( $key, $group ) === false ) {
return $this->set( $key, $value, $expire, $group );
}
return false;
}
/**
* Sets data
*
* @param string $key Key.
* @param mixed $value Value.
* @param integer $expire Time to expire.
* @param string $group Used to differentiate between groups of cache values.
*
* @return boolean
*/
public function set( $key, $value, $expire = 0, $group = '' ) {
if ( ! isset( $value['key_version'] ) ) {
$value['key_version'] = $this->_get_key_version( $group );
}
$storage_key = $this->get_item_key( $key );
return apc_store( $storage_key, serialize( $value ), $expire );
}
/**
* Returns data
*
* @param string $key Key.
* @param string $group Used to differentiate between groups of cache values.
*
* @return mixed
*/
public function get_with_old( $key, $group = '' ) {
$has_old_data = false;
$storage_key = $this->get_item_key( $key );
$v = @unserialize( apc_fetch( $storage_key ) );
if ( ! is_array( $v ) || ! isset( $v['key_version'] ) ) {
return array( null, $has_old_data );
}
$key_version = $this->_get_key_version( $group );
if ( $v['key_version'] === $key_version ) {
return array( $v, $has_old_data );
}
if ( $v['key_version'] > $key_version ) {
if ( ! empty( $v['key_version_at_creation'] ) && $v['key_version_at_creation'] !== $key_version ) {
$this->_set_key_version( $v['key_version'], $group );
}
return array( $v, $has_old_data );
}
// key version is old.
if ( ! $this->_use_expired_data ) {
return array( null, $has_old_data );
}
// if we have expired data - update it for future use and let current process recalculate it.
$expires_at = isset( $v['expires_at'] ) ? $v['expires_at'] : null;
if ( null === $expires_at || time() > $expires_at ) {
$v['expires_at'] = time() + 30;
apc_store( $storage_key, serialize( $v ), 0 );
$has_old_data = true;
return array( null, $has_old_data );
}
// return old version.
return array( $v, $has_old_data );
}
/**
* Replaces data
*
* @param string $key Key.
* @param mixed $value Value.
* @param integer $expire Time to expire.
* @param string $group Used to differentiate between groups of cache values.
*
* @return boolean
*/
public function replace( $key, &$value, $expire = 0, $group = '' ) {
if ( $this->get( $key, $group ) !== false ) {
return $this->set( $key, $value, $expire, $group );
}
return false;
}
/**
* Deletes data
*
* @param string $key Key.
* @param string $group Group.
*
* @return boolean
*/
public function delete( $key, $group = '' ) {
$storage_key = $this->get_item_key( $key );
if ( $this->_use_expired_data ) {
$v = @unserialize( apc_fetch( $storage_key ) );
if ( is_array( $v ) ) {
$v['key_version'] = 0;
apc_store( $storage_key, serialize( $v ), 0 );
return true;
}
}
return apc_delete( $storage_key );
}
/**
* Deletes _old and primary if exists.
*
* @param string $key Key.
* @param string $group Group.
*
* @return bool
*/
public function hard_delete( $key, $group = '' ) {
$storage_key = $this->get_item_key( $key );
return apc_delete( $storage_key );
}
/**
* Flushes all data
*
* @param string $group Used to differentiate between groups of cache values.
*
* @return boolean
*/
public function flush( $group = '' ) {
$this->_get_key_version( $group ); // initialize $this->_key_version.
++$this->_key_version[ $group ];
$this->_set_key_version( $this->_key_version[ $group ], $group );
return true;
}
/**
* Gets a key extension for "ahead generation" mode.
* Used by AlwaysCached functionality to regenerate content
*
* @param string $group Used to differentiate between groups of cache values.
*
* @return array
*/
public function get_ahead_generation_extension( $group ) {
$v = $this->_get_key_version( $group );
return array(
'key_version' => $v + 1,
'key_version_at_creation' => $v,
);
}
/**
* Flushes group with before condition
*
* @param string $group Used to differentiate between groups of cache values.
* @param array $extension Used to set a condition what version to flush.
*
* @return void
*/
public function flush_group_after_ahead_generation( $group, $extension ) {
$v = $this->_get_key_version( $group );
if ( $extension['key_version'] > $v ) {
$this->_set_key_version( $extension['key_version'], $group );
}
}
/**
* Checks if engine can function properly in this environment
*
* @return bool
*/
public function available() {
return function_exists( 'apc_store' );
}
/**
* Returns key postfix
*
* @param string $group Used to differentiate between groups of cache values.
*
* @return integer
*/
private function _get_key_version( $group = '' ) {
if ( ! isset( $this->_key_version[ $group ] ) || $this->_key_version[ $group ] <= 0 ) {
$v = apc_fetch( $this->_get_key_version_key( $group ) );
$v = intval( $v );
$this->_key_version[ $group ] = ( $v > 0 ? $v : 1 );
}
return $this->_key_version[ $group ];
}
/**
* Sets new key version
*
* @param unknown $v Key.
* @param string $group Used to differentiate between groups of cache values.
*
* @return void
*/
private function _set_key_version( $v, $group = '' ) {
apc_store( $this->_get_key_version_key( $group ), $v, 0 );
}
/**
* Used to replace as atomically as possible known value to new one
*
* @param string $key Key.
* @param mixed $old_value Old value.
* @param mixed $new_value New value.
*
* @return bool
*/
public function set_if_maybe_equals( $key, $old_value, $new_value ) {
// apc_cas doesnt fit here, since we are float but it works with int only cant
// guarantee atomic action here, filelocks fail often.
$value = $this->get( $key );
if ( isset( $old_value['content'] ) && $value['content'] !== $old_value['content'] ) {
return false;
}
return $this->set( $key, $new_value );
}
/**
* Use key as a counter and add integet value to it
*
* @param string $key Key.
* @param mixed $value Value.
*
* @return bool
*/
public function counter_add( $key, $value ) {
if ( 0 === $value ) {
return true;
}
$storage_key = $this->get_item_key( $key );
$r = apc_inc( $storage_key, $value );
// it doesnt initialize counter by itself.
if ( ! $r ) {
$this->counter_set( $key, 0 );
}
return $r;
}
/**
* Use key as a counter and add integet value to it
*
* @param string $key Key.
* @param mixed $value Value.
*
* @return bool
*/
public function counter_set( $key, $value ) {
$storage_key = $this->get_item_key( $key );
return apc_store( $storage_key, $value );
}
/**
* Get counter's value
*
* @param string $key Key.
*
* @return int
*/
public function counter_get( $key ) {
$storage_key = $this->get_item_key( $key );
$v = (int) apc_fetch( $storage_key );
return $v;
}
}

View File

@@ -0,0 +1,323 @@
<?php
/**
* File: Cache_Apcu.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cache_Apcu
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
* phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
*/
class Cache_Apcu extends Cache_Base {
/**
* Used for faster flushing
*
* @var integer $_key_version
*/
private $_key_version = array();
/**
* Adds data
*
* @param string $key Key.
* @param mixed $value Value.
* @param integer $expire Time to expire.
* @param string $group Used to differentiate between groups of cache values.
*
* @return boolean
*/
public function add( $key, &$value, $expire = 0, $group = '' ) {
if ( $this->get( $key, $group ) === false ) {
return $this->set( $key, $value, $expire, $group );
}
return false;
}
/**
* Sets data
*
* @param string $key Key.
* @param mixed $value Value.
* @param integer $expire Time to expire.
* @param string $group Used to differentiate between groups of cache values.
*
* @return boolean
*/
public function set( $key, $value, $expire = 0, $group = '' ) {
if ( ! isset( $value['key_version'] ) ) {
$value['key_version'] = $this->_get_key_version( $group );
}
$storage_key = $this->get_item_key( $key );
return apcu_store( $storage_key, serialize( $value ), $expire );
}
/**
* Returns data
*
* @param string $key Key.
* @param string $group Used to differentiate between groups of cache values.
*
* @return mixed
*/
public function get_with_old( $key, $group = '' ) {
$has_old_data = false;
$storage_key = $this->get_item_key( $key );
$v = @unserialize( apcu_fetch( $storage_key ) );
if ( ! is_array( $v ) || ! isset( $v['key_version'] ) ) {
return array( null, $has_old_data );
}
$key_version = $this->_get_key_version( $group );
if ( $v['key_version'] === $key_version ) {
return array( $v, $has_old_data );
}
if ( $v['key_version'] > $key_version ) {
if ( ! empty( $v['key_version_at_creation'] ) && $v['key_version_at_creation'] !== $key_version ) {
$this->_set_key_version( $v['key_version'], $group );
}
return array( $v, $has_old_data );
}
// key version is old.
if ( ! $this->_use_expired_data ) {
return array( null, $has_old_data );
}
// if we have expired data - update it for future use and let current process recalculate it.
$expires_at = isset( $v['expires_at'] ) ? $v['expires_at'] : null;
if ( null === $expires_at || time() > $expires_at ) {
$v['expires_at'] = time() + 30;
apcu_store( $storage_key, serialize( $v ), 0 );
$has_old_data = true;
return array( null, $has_old_data );
}
// return old version.
return array( $v, $has_old_data );
}
/**
* Replaces data
*
* @param string $key Key.
* @param mixed $value Value.
* @param integer $expire Time to expire.
* @param string $group Used to differentiate between groups of cache values.
*
* @return boolean
*/
public function replace( $key, &$value, $expire = 0, $group = '' ) {
if ( $this->get( $key, $group ) !== false ) {
return $this->set( $key, $value, $expire, $group );
}
return false;
}
/**
* Deletes data
*
* @param string $key Key.
* @param string $group Group.
*
* @return boolean
*/
public function delete( $key, $group = '' ) {
$storage_key = $this->get_item_key( $key );
if ( $this->_use_expired_data ) {
$v = @unserialize( apcu_fetch( $storage_key ) );
if ( is_array( $v ) ) {
$v['key_version'] = 0;
apcu_store( $storage_key, serialize( $v ), 0 );
return true;
}
}
return apcu_delete( $storage_key );
}
/**
* Deletes _old and primary if exists.
*
* @param string $key Key.
* @param string $group Group.
*
* @return bool
*/
public function hard_delete( $key, $group = '' ) {
$storage_key = $this->get_item_key( $key );
return apcu_delete( $storage_key );
}
/**
* Flushes all data
*
* @param string $group Used to differentiate between groups of cache values.
*
* @return boolean
*/
public function flush( $group = '' ) {
$this->_get_key_version( $group ); // initialize $this->_key_version.
++$this->_key_version[ $group ];
$this->_set_key_version( $this->_key_version[ $group ], $group );
return true;
}
/**
* Gets a key extension for "ahead generation" mode.
* Used by AlwaysCached functionality to regenerate content
*
* @param string $group Used to differentiate between groups of cache values.
*
* @return array
*/
public function get_ahead_generation_extension( $group ) {
$v = $this->_get_key_version( $group );
return array(
'key_version' => $v + 1,
'key_version_at_creation' => $v,
);
}
/**
* Flushes group with before condition
*
* @param string $group Used to differentiate between groups of cache values.
* @param array $extension Used to set a condition what version to flush.
*
* @return void
*/
public function flush_group_after_ahead_generation( $group, $extension ) {
$v = $this->_get_key_version( $group );
if ( $extension['key_version'] > $v ) {
$this->_set_key_version( $extension['key_version'], $group );
}
}
/**
* Checks if engine can function properly in this environment
*
* @return bool
*/
public function available() {
return function_exists( 'apcu_store' );
}
/**
* Returns key postfix
*
* @param string $group Used to differentiate between groups of cache values.
*
* @return integer
*/
private function _get_key_version( $group = '' ) {
if ( ! isset( $this->_key_version[ $group ] ) || $this->_key_version[ $group ] <= 0 ) {
$v = apcu_fetch( $this->_get_key_version_key( $group ) );
$v = intval( $v );
$this->_key_version[ $group ] = ( $v > 0 ? $v : 1 );
}
return $this->_key_version[ $group ];
}
/**
* Sets new key version
*
* @param unknown $v Key.
* @param string $group Used to differentiate between groups of cache values.
*
* @return void
*/
private function _set_key_version( $v, $group = '' ) {
apcu_store( $this->_get_key_version_key( $group ), $v, 0 );
}
/**
* Used to replace as atomically as possible known value to new one
*
* @param string $key Key.
* @param mixed $old_value Old value.
* @param mixed $new_value New value.
*
* @return bool
*/
public function set_if_maybe_equals( $key, $old_value, $new_value ) {
// apc_cas doesnt fit here, since we are float but it works with
// int only cant guarantee atomic action here, filelocks fail often.
$value = $this->get( $key );
if ( isset( $old_value['content'] ) && $value['content'] !== $old_value['content'] ) {
return false;
}
return $this->set( $key, $new_value );
}
/**
* Use key as a counter and add integet value to it
*
* @param string $key Key.
* @param mixed $value Value.
*
* @return bool
*/
public function counter_add( $key, $value ) {
if ( 0 === $value ) {
return true;
}
$storage_key = $this->get_item_key( $key );
$r = apcu_inc( $storage_key, $value );
// it doesnt initialize counter by itself.
if ( ! $r ) {
$this->counter_set( $key, 0 );
}
return $r;
}
/**
* Use key as a counter and add integet value to it
*
* @param string $key Key.
* @param mixed $value Value.
*
* @return bool
*/
public function counter_set( $key, $value ) {
$storage_key = $this->get_item_key( $key );
return apcu_store( $storage_key, $value );
}
/**
* Get counter's value
*
* @param string $key Key.
*
* @return int
*/
public function counter_get( $key ) {
$storage_key = $this->get_item_key( $key );
$v = (int) apcu_fetch( $storage_key );
return $v;
}
}

View File

@@ -0,0 +1,336 @@
<?php
/**
* File: Cache_Base.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cache_Base
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
* phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
* phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter
*/
class Cache_Base {
/**
* Blog id
*
* @var integer
*/
protected $_blog_id = 0;
/**
* To separate the caching for different modules
*
* @var string
*/
protected $_module = '';
/**
* Host
*
* @var string
*/
protected $_host = '';
/**
* Host
*
* @var int
*/
protected $_instance_id = 0;
/**
* If we are going to return expired data when some other process
* is working on new data calculation
*
* @var boolean
*/
protected $_use_expired_data = false;
/**
* Constructor
*
* @param array $config Config.
*
* @return void
*/
public function __construct( $config = array() ) {
$this->_blog_id = $config['blog_id'];
$this->_use_expired_data = isset( $config['use_expired_data'] ) ? $config['use_expired_data'] : false;
$this->_module = isset( $config['module'] ) ? $config['module'] : 'default';
$this->_host = isset( $config['host'] ) ? $config['host'] : '';
$this->_instance_id = isset( $config['instance_id'] ) ? $config['instance_id'] : 0;
}
/**
* Adds data
*
* @abstract
*
* @param string $key Key.
* @param mixed $data Data.
* @param integer $expire Time to expire.
* @param string $group Used to differentiate between groups of cache values.
*
* @return boolean
*/
public function add( $key, &$data, $expire = 0, $group = '' ) {
return false;
}
/**
* Sets data
*
* @abstract
*
* @param string $key Key.
* @param mixed $data Data.
* @param integer $expire Time to expire.
* @param string $group Used to differentiate between groups of cache values.
*
* @return boolean
*/
public function set( $key, $data, $expire = 0, $group = '' ) {
return false;
}
/**
* Returns data
*
* @abstract
*
* @param string $key Key.
* @param string $group Used to differentiate between groups of cache values.
*
* @return mixed
*/
public function get( $key, $group = '' ) {
list( $data, $has_old ) = $this->get_with_old( $key, $group );
return $data;
}
/**
* Return primary data and if old exists
*
* @abstract
*
* @param string $key Key.
* @param string $group Used to differentiate between groups of cache values.
*
* @return array|mixed
*/
public function get_with_old( $key, $group = '' ) {
return array( null, false );
}
/**
* Checks if entry exists
*
* @abstract
*
* @param string $key Key.
* @param string $group Used to differentiate between groups of cache values.
*
* @return boolean true if exists, false otherwise
*/
public function exists( $key, $group = '' ) {
list( $data, $has_old ) = $this->get_with_old( $key, $group );
return ! empty( $data ) && ! $has_old;
}
/**
* Alias for get for minify cache
*
* @abstract
*
* @param string $key Key.
* @param string $group Used to differentiate between groups of cache values.
*
* @return mixed
*/
public function fetch( $key, $group = '' ) {
return $this->get( $key, $group = '' );
}
/**
* Replaces data
*
* @abstract
*
* @param string $key Key.
* @param mixed $data Data.
* @param integer $expire Time to expire.
* @param string $group Used to differentiate between groups of cache values.
*
* @return boolean
*/
public function replace( $key, &$data, $expire = 0, $group = '' ) {
return false;
}
/**
* Deletes data
*
* @abstract
*
* @param string $key Key.
* @param string $group Used to differentiate between groups of cache values.
*
* @return boolean
*/
public function delete( $key, $group = '' ) {
return false;
}
/**
* Deletes primary data and old data
*
* @abstract
*
* @param string $key Key.
* @param string $group Group.
*
* @return boolean
*/
public function hard_delete( $key, $group = '' ) {
return false;
}
/**
* Flushes all data
*
* @abstract
*
* @param string $group Used to differentiate between groups of cache values.
*
* @return boolean
*/
public function flush( $group = '' ) {
return false;
}
/**
* Gets a key extension for "ahead generation" mode.
* Used by AlwaysCached functionality to regenerate content
*
* @abstract
*
* @param string $group Used to differentiate between groups of cache values.
*
* @return array
*/
public function get_ahead_generation_extension( $group ) {
return array();
}
/**
* Flushes group with before condition
*
* @abstract
*
* @param string $group Used to differentiate between groups of cache values.
* @param array $extension Used to set a condition what version to flush.
*
* @return boolean
*/
public function flush_group_after_ahead_generation( $group, $extension ) {
return false;
}
/**
* Checks if engine can function properly in this environment
*
* @abstract
*
* @return bool
*/
public function available() {
return true;
}
/**
* Constructs key version key
*
* @abstract
*
* @param unknown $group Group.
*
* @return string
*/
protected function _get_key_version_key( $group = '' ) {
return sprintf(
'w3tc_%d_%d_%s_%s_key_version',
$this->_instance_id,
$this->_blog_id,
$this->_module,
$group
);
}
/**
* Constructs item key
*
* @abstract
*
* @param unknown $name Name.
*
* @return string
*/
public function get_item_key( $name ) {
return sprintf(
'w3tc_%d_%s_%d_%s_%s',
$this->_instance_id,
$this->_host,
$this->_blog_id,
$this->_module,
$name
);
}
/**
* Use key as a counter and add integer value to it
*
* @abstract
*
* @param string $key Key.
* @param int $value Value.
*
* @return bool
*/
public function counter_add( $key, $value ) {
return false;
}
/**
* Use key as a counter and add integer value to it
*
* @abstract
*
* @param string $key Key.
* @param int $value Value.
*
* @return bool
*/
public function counter_set( $key, $value ) {
return false;
}
/**
* Get counter's value
*
* @abstract
*
* @param string $key Key.
*
* @return bool
*/
public function counter_get( $key ) {
return false;
}
}

View File

@@ -0,0 +1,309 @@
<?php
/**
* File: Cache_Eaccelerator.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cache_Eaccelerator
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
* phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
*/
class Cache_Eaccelerator extends Cache_Base {
/**
* Used for faster flushing
*
* @var integer $_key_postfix
*/
private $_key_version = array();
/**
* Adds data
*
* @param string $key Key.
* @param mixed $value Value.
* @param integer $expire Time to expire.
* @param string $group Used to differentiate between groups of cache values.
*
* @return boolean
*/
public function add( $key, &$value, $expire = 0, $group = '' ) {
if ( $this->get( $key, $group ) === false ) {
return $this->set( $key, $value, $expire, $group );
}
return false;
}
/**
* Sets data
*
* @param string $key Key.
* @param mixed $value Value.
* @param integer $expire Time to expire.
* @param string $group Used to differentiate between groups of cache values.
*
* @return boolean
*/
public function set( $key, $value, $expire = 0, $group = '' ) {
if ( ! isset( $value['key_version'] ) ) {
$value['key_version'] = $this->_get_key_version( $group );
}
$storage_key = $this->get_item_key( $key );
return eaccelerator_put( $storage_key, serialize( $value ), $expire );
}
/**
* Returns data
*
* @param string $key Key.
* @param string $group Used to differentiate between groups of cache values.
*
* @return mixed
*/
public function get_with_old( $key, $group = '' ) {
$has_old_data = false;
$storage_key = $this->get_item_key( $key );
$v = @unserialize( eaccelerator_get( $storage_key ) );
if ( ! is_array( $v ) || ! isset( $v['key_version'] ) ) {
return array( null, $has_old_data );
}
$key_version = $this->_get_key_version( $group );
if ( $v['key_version'] === $key_version ) {
return array( $v, $has_old_data );
}
if ( $v['key_version'] > $key_version ) {
if ( ! empty( $v['key_version_at_creation'] ) && $v['key_version_at_creation'] !== $key_version ) {
$this->_set_key_version( $v['key_version'], $group );
}
return array( $v, $has_old_data );
}
// key version is old.
if ( ! $this->_use_expired_data ) {
return array( null, $has_old_data );
}
// if we have expired data - update it for future use and let current process recalculate it.
$expires_at = isset( $v['expires_at'] ) ? $v['expires_at'] : null;
if ( null === $expires_at || time() > $expires_at ) {
$v['expires_at'] = time() + 30;
eaccelerator_put( $storage_key, serialize( $v ), 0 );
$has_old_data = true;
return array( null, $has_old_data );
}
// return old version.
return array( $v, $has_old_data );
}
/**
* Replaces data
*
* @param string $key Key.
* @param mixed $value Value.
* @param integer $expire Time to expire.
* @param string $group Used to differentiate between groups of cache values.
*
* @return boolean
*/
public function replace( $key, &$value, $expire = 0, $group = '' ) {
if ( $this->get( $key, $group ) !== false ) {
return $this->set( $key, $value, $expire, $group );
}
return false;
}
/**
* Deletes data
*
* @param string $key Key.
* @param string $group Group.
*
* @return boolean
*/
public function delete( $key, $group = '' ) {
$storage_key = $this->get_item_key( $key );
if ( $this->_use_expired_data ) {
$v = @unserialize( eaccelerator_get( $storage_key ) );
if ( is_array( $v ) ) {
$v['key_version'] = 0;
eaccelerator_put( $storage_key, serialize( $v ), 0 );
return true;
}
}
return eaccelerator_rm( $key . '_' . $this->_blog_id );
}
/**
* Deletes _old and primary if exists.
*
* @param string $key Key.
* @param string $group Group.
*
* @return bool
*/
public function hard_delete( $key, $group = '' ) {
$storage_key = $this->get_item_key( $key );
return eaccelerator_rm( $storage_key );
}
/**
* Flushes all data
*
* @param string $group Used to differentiate between groups of cache values.
*
* @return boolean
*/
public function flush( $group = '' ) {
$this->_get_key_version( $group ); // initialize $this->_key_version.
++$this->_key_version[ $group ];
$this->_set_key_version( $this->_key_version[ $group ], $group );
return true;
}
/**
* Gets a key extension for "ahead generation" mode.
* Used by AlwaysCached functionality to regenerate content
*
* @param string $group Used to differentiate between groups of cache values.
*
* @return array
*/
public function get_ahead_generation_extension( $group ) {
$v = $this->_get_key_version( $group );
return array(
'key_version' => $v + 1,
'key_version_at_creation' => $v,
);
}
/**
* Flushes group with before condition
*
* @param string $group Used to differentiate between groups of cache values.
* @param array $extension Used to set a condition what version to flush.
*
* @return void
*/
public function flush_group_after_ahead_generation( $group, $extension ) {
$v = $this->_get_key_version( $group );
if ( $extension['key_version'] > $v ) {
$this->_set_key_version( $extension['key_version'], $group );
}
}
/**
* Checks if engine can function properly in this environment
*
* @return bool
*/
public function available() {
return function_exists( 'eaccelerator_put' );
}
/**
* Returns key postfix
*
* @param string $group Used to differentiate between groups of cache values.
*
* @return integer
*/
private function _get_key_version( $group = '' ) {
if ( ! isset( $this->_key_version[ $group ] ) || $this->_key_version[ $group ] <= 0 ) {
$v = eaccelerator_get( $this->_get_key_version_key( $group ) );
$v = intval( $v );
$this->_key_version[ $group ] = ( $v > 0 ? $v : 1 );
}
return $this->_key_version[ $group ];
}
/**
* Sets new key version
*
* @param unknown $v Key.
* @param string $group Used to differentiate between groups of cache values.
*
* @return boolean
*/
private function _set_key_version( $v, $group = '' ) {
// cant guarantee atomic action here, filelocks fail often.
$value = $this->get( $key );
if ( isset( $old_value['content'] ) && $value['content'] !== $old_value['content'] ) {
return false;
}
return $this->set( $key, $new_value );
}
/**
* Used to replace as atomically as possible known value to new one
*
* @param string $key Key.
* @param mixed $old_value Old value.
* @param mixed $new_value New value.
*
* @return bool
*/
public function set_if_maybe_equals( $key, $old_value, $new_value ) {
// eaccelerator cache not supported anymore by its authors.
return false;
}
/**
* Use key as a counter and add integet value to it
*
* @param string $key Key.
* @param mixed $value Value.
*
* @return bool
*/
public function counter_add( $key, $value ) {
// eaccelerator cache not supported anymore by its authors.
return false;
}
/**
* Use key as a counter and add integet value to it
*
* @param string $key Key.
* @param mixed $value Value.
*
* @return bool
*/
public function counter_set( $key, $value ) {
// eaccelerator cache not supported anymore by its authors.
return false;
}
/**
* Get counter's value
*
* @param string $key Key.
*
* @return bool
*/
public function counter_get( $key ) {
// eaccelerator cache not supported anymore by its authors.
return false;
}
}

View File

@@ -0,0 +1,702 @@
<?php
/**
* File: Cache_File.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cache_File
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
* phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
* phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
* phpcs:disable WordPress.WP.AlternativeFunctions
*/
class Cache_File extends Cache_Base {
/**
* Path to cache dir
*
* @var string
*/
protected $_cache_dir = '';
/**
* Directory to flush
*
* @var string
*/
protected $_flush_dir = '';
/**
* Exclude files
*
* @var array
*/
protected $_exclude = array();
/**
* Flush time limit
*
* @var int
*/
protected $_flush_timelimit = 0;
/**
* File locking
*
* @var boolean
*/
protected $_locking = false;
/**
* If path should be generated based on wp_hash
*
* @var bool
*/
protected $_use_wp_hash = false;
/**
* Constructs the Cache_File instance.
*
* Initializes the cache file settings using the provided configuration array. Sets up the cache directory, exclusions, flush
* time limits, locking behavior, and flushing directory based on the configuration. If specific configurations are not provided,
* defaults are determined using environment utilities.
*
* @param array $config {
* Optional. Configuration options for the cache file.
*
* @type string $cache_dir The directory where cache files are stored.
* @type array $exclude List of items to exclude from caching.
* @type int $flush_timelimit The time limit for flushing the cache.
* @type bool $locking Whether to use locking for cache file access.
* @type string $flush_dir The directory where cache flush operations occur.
* @type bool $use_wp_hash Whether to use WordPress-specific hashing for cache files.
* }
*
* @return void
*/
public function __construct( $config = array() ) {
parent::__construct( $config );
if ( isset( $config['cache_dir'] ) ) {
$this->_cache_dir = trim( $config['cache_dir'] );
} else {
$this->_cache_dir = Util_Environment::cache_blog_dir( $config['section'], $config['blog_id'] );
}
$this->_exclude = isset( $config['exclude'] ) ? (array) $config['exclude'] : array();
$this->_flush_timelimit = isset( $config['flush_timelimit'] ) ? (int) $config['flush_timelimit'] : 180;
$this->_locking = isset( $config['locking'] ) ? (bool) $config['locking'] : false;
if ( isset( $config['flush_dir'] ) ) {
$this->_flush_dir = $config['flush_dir'];
} elseif ( $config['blog_id'] <= 0 && ! isset( $config['cache_dir'] ) ) {
// Clear whole section if we operate on master cache and in a mode when cache_dir not strictly specified.
$this->_flush_dir = Util_Environment::cache_dir( $config['section'] );
} else {
$this->_flush_dir = $this->_cache_dir;
}
if ( isset( $config['use_wp_hash'] ) && $config['use_wp_hash'] ) {
$this->_use_wp_hash = true;
}
}
/**
* Adds a value to the cache if it does not already exist.
*
* Attempts to retrieve the value using the specified key and group. If the key does not exist in the cache, the value is
* added with the specified expiration time.
*
* @param string $key The cache key.
* @param mixed $value The variable to store in the cache.
* @param int $expire Optional. Time in seconds until the cache entry expires. Default is 0 (no expiration).
* @param string $group Optional. The group to which the cache belongs. Default is an empty string.
*
* @return bool True if the value was added, false if it already exists or on failure.
*/
public function add( $key, &$value, $expire = 0, $group = '' ) {
if ( $this->get( $key, $group ) === false ) {
return $this->set( $key, $value, $expire, $group );
}
return false;
}
/**
* Stores the value in the cache with the specified expiration time. The data is serialized and written to a file with a
* header indicating the expiration time. File locking can be used for write operations if enabled.
*
* @param string $key An MD5 of the DB query.
* @param mixed $content Data to be cached.
* @param int $expiration Optional. Time in seconds until the cache entry expires. Default is 0 (no expiration).
* @param string $group Optional. The group to which the cache belongs. Default is an empty string.
*
* @return bool True on success, false on failure.
*/
public function set( $key, $content, $expiration = 0, $group = '' ) {
/**
* Get the file pointer of the cache file.
* The $key is transformed to a storage key (format "w3tc_INSTANCEID_HOST_BLOGID_dbcache_HASH").
* The file path is in the format: CACHEDIR/db/BLOGID/GROUP/[0-9a-f]{3}/[0-9a-f]{3}/[0-9a-f]{32}.
*/
$fp = $this->fopen_write( $key, $group, 'wb' );
if ( ! $fp ) {
return false;
}
if ( $this->_locking ) {
@flock( $fp, LOCK_EX );
}
if ( $expiration <= 0 || $expiration > W3TC_CACHE_FILE_EXPIRE_MAX ) {
$expiration = W3TC_CACHE_FILE_EXPIRE_MAX;
}
$expires_at = time() + $expiration;
@fputs( $fp, pack( 'L', $expires_at ) );
@fputs( $fp, '<?php exit; ?>' );
@fputs( $fp, @serialize( $content ) );
@fclose( $fp );
if ( $this->_locking ) {
@flock( $fp, LOCK_UN );
}
return true;
}
/**
* Retrieves a value from the cache along with its old state information.
*
* Fetches the cached value for the specified key and group. If the cache entry has expired but old data usage is enabled, the
* expired data can still be returned while updating its expiration time temporarily.
*
* @param string $key The cache key.
* @param string $group Optional. The group to which the cache belongs. Default is an empty string.
*
* @return array An array containing the unserialized cached data (or null if not found) and a boolean indicating if old data was used.
*/
public function get_with_old( $key, $group = '' ) {
list( $data, $has_old_data ) = $this->_get_with_old_raw( $key, $group );
if ( ! empty( $data ) ) {
$data_unserialized = @unserialize( $data );
} else {
$data_unserialized = $data;
}
return array( $data_unserialized, $has_old_data );
}
/**
* Retrieves the raw cached data and expiration status for a key.
*
* Reads the cached data file to determine the expiration time and fetches the data if it is valid. If the data is expired and
* old data usage is enabled, the expiration time is updated temporarily and the expired data is returned.
*
* @param string $key The cache key.
* @param string $group Optional. The group to which the cache belongs. Default is an empty string.
*
* @return array An array containing the raw cached data (or null if not found) and a boolean indicating if old data was used.
*/
private function _get_with_old_raw( $key, $group = '' ) {
$has_old_data = false;
$storage_key = $this->get_item_key( $key );
$path = $this->_cache_dir . DIRECTORY_SEPARATOR . $this->_get_path( $storage_key, $group );
if ( ! is_readable( $path ) ) {
return array( null, $has_old_data );
}
$fp = @fopen( $path, 'rb' );
if ( ! $fp || 4 > filesize( $path ) ) {
return array( null, $has_old_data );
}
if ( $this->_locking ) {
@flock( $fp, LOCK_SH );
}
$expires_at = @fread( $fp, 4 );
$data = null;
if ( false !== $expires_at ) {
list( , $expires_at ) = @unpack( 'L', $expires_at );
if ( time() > $expires_at ) {
if ( $this->_use_expired_data ) {
// update expiration so other threads will use old data.
$fp2 = @fopen( $path, 'cb' );
if ( $fp2 ) {
@fputs( $fp2, pack( 'L', time() + 30 ) );
@fclose( $fp2 );
}
$has_old_data = true;
}
} else {
$data = '';
while ( ! @feof( $fp ) ) {
$data .= @fread( $fp, 4096 );
}
$data = substr( $data, 14 );
}
}
if ( $this->_locking ) {
@flock( $fp, LOCK_UN );
}
@fclose( $fp );
return array( $data, $has_old_data );
}
/**
* Replaces an existing cache value with a new one.
*
* Updates the cache entry for the specified key and group if it already exists. If the key does not exist, no action is taken.
*
* @param string $key The cache key.
* @param mixed $value The variable to store in the cache.
* @param int $expire Optional. Time in seconds until the cache entry expires. Default is 0 (no expiration).
* @param string $group Optional. The group to which the cache belongs. Default is an empty string.
*
* @return bool True if the value was replaced, false otherwise.
*/
public function replace( $key, &$value, $expire = 0, $group = '' ) {
if ( false !== $this->get( $key, $group ) ) {
return $this->set( $key, $value, $expire, $group );
}
return false;
}
/**
* Deletes a value from the cache.
*
* Removes the cache entry for the specified key and group. If "use expired data" is enabled, the expiration time of the cache
* entry is set to zero instead of deleting the file.
*
* @param string $key The cache key.
* @param string $group Optional. The group to which the cache belongs. Default is an empty string.
*
* @return bool True if the value was successfully deleted, false otherwise.
*/
public function delete( $key, $group = '' ) {
$storage_key = $this->get_item_key( $key );
$path = $this->_cache_dir . DIRECTORY_SEPARATOR . $this->_get_path( $storage_key, $group );
if ( ! file_exists( $path ) ) {
return true;
}
if ( $this->_use_expired_data ) {
$fp = @fopen( $path, 'cb' );
if ( $fp ) {
if ( $this->_locking ) {
@flock( $fp, LOCK_EX );
}
@fputs( $fp, pack( 'L', 0 ) ); // make it expired.
@fclose( $fp );
if ( $this->_locking ) {
@flock( $fp, LOCK_UN );
}
return true;
}
}
return @unlink( $path );
}
/**
* Performs a hard delete of a cache entry.
*
* Completely removes the cache file for the specified key and group without checking for expiration or other conditions.
*
* @param string $key The cache key.
* @param string $group Optional. The group to which the cache belongs. Default is an empty string.
*
* @return bool True if the file was successfully deleted, false otherwise.
*/
public function hard_delete( $key, $group = '' ) {
$key = $this->get_item_key( $key );
$path = $this->_cache_dir . DIRECTORY_SEPARATOR . $this->_get_path( $key, $group );
return @unlink( $path );
}
/**
* Flushes all cache entries or those belonging to a specific group.
*
* Deletes all files in the cache directory or a specific group subdirectory. If the group is "sitemaps", the flush is performed
* based on a regular expression defined in the configuration.
*
* @param string $group Optional. The group to flush. Default is an empty string.
*
* @return bool Always returns true.
*/
public function flush( $group = '' ) {
@set_time_limit( $this->_flush_timelimit ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
if ( 'sitemaps' === $group ) {
$config = Dispatcher::config();
$sitemap_regex = $config->get_string( 'pgcache.purge.sitemap_regex' );
$this->_flush_based_on_regex( $sitemap_regex );
} else {
$flush_dir = $group ? $this->_cache_dir . DIRECTORY_SEPARATOR . $group . DIRECTORY_SEPARATOR : $this->_flush_dir;
Util_File::emptydir( $flush_dir, $this->_exclude );
}
return true;
}
/**
* Retrieves an extension array for ahead-of-generation cache handling.
*
* Returns an array containing the current timestamp for cache generation purposes.
*
* @param string $group The cache group.
*
* @return array An array with the `before_time` key set to the current timestamp.
*/
public function get_ahead_generation_extension( $group ) {
return array(
'before_time' => time(),
);
}
/**
* Flushes a cache group after ahead-of-generation processing.
*
* Performs any cleanup or flushing required for a cache group after an ahead-of-generation operation.
*
* @param string $group The cache group.
* @param array $extension {
* An extension array with generation metadata.
*
* @type mixed $before_time The time before the generation.
* }
*
* @return void
*/
public function flush_group_after_ahead_generation( $group, $extension ) {
$dir = $this->_flush_dir;
$extension['before_time'];
}
/**
* Retrieves the last modified time of a cache file.
*
* Returns the modification time of the cache file for the specified key and group.
*
* @param string $key The cache key.
* @param string $group Optional. The group to which the cache belongs. Default is an empty string.
*
* @return int|false The file modification time as a Unix timestamp, or false if the file does not exist.
*/
public function mtime( $key, $group = '' ) {
$path =
$this->_cache_dir . DIRECTORY_SEPARATOR . $this->_get_path( $key, $group );
if ( file_exists( $path ) ) {
return @filemtime( $path );
}
return false;
}
/**
* Returns subpath for the cache file (format: [0-9a-f]{3}/[0-9a-f]{3}/[0-9a-f]{32}).
*
* Creates the file path for the cache file based on the key and group. A hash of the key is used to create subdirectories
* for organizational purposes.
*
* @param string $key Storage key (format: "w3tc_INSTANCEID_HOST_BLOGID_dbcache_HASH").
* @param string $group Optional. The group to which the cache belongs. Default is an empty string.
*
* @return string The file path for the cache entry.
*/
public function _get_path( $key, $group = '' ) {
if ( $this->_use_wp_hash && function_exists( 'wp_hash' ) ) {
$hash = wp_hash( $key ); // Most common.
} else {
$hash = md5( $key ); // Less common, but still used in some cases.
}
return ( $group ? $group . DIRECTORY_SEPARATOR : '' ) . sprintf( '%s/%s/%s.php', substr( $hash, 0, 3 ), substr( $hash, 3, 3 ), $hash );
}
/**
* Calculates the size of the cache directory.
*
* Recursively calculates the total size and number of files in the cache directory. Stops processing if the timeout is exceeded.
*
* @param string $timeout_time The timeout timestamp.
*
* @return array An array containing the total size (`bytes`), the number of items (`items`), and whether a timeout occurred
* (`timeout_occurred`).
*/
public function get_stats_size( $timeout_time ) {
$size = array(
'bytes' => 0,
'items' => 0,
'timeout_occurred' => false,
);
$size = $this->dirsize( $this->_cache_dir, $size, $timeout_time );
return $size;
}
/**
* Recursively calculates the size of a directory.
*
* Iterates through all files and subdirectories within the specified directory to calculate the total size and count of items.
* Checks for timeouts every 1000 items.
*
* @param string $path The directory path.
* @param array $size {
* The size data array.
*
* @type int $bytes The total size of the directory in bytes.
* @type int $items The total number of items (files/subdirectories).
* @type bool $timeout_occurred Flag indicating whether a timeout has occurred.
* }
* @param int $timeout_time The timeout timestamp.
*
* @return array Updated size data.
*/
private function dirsize( $path, $size, $timeout_time ) {
$dir = @opendir( $path );
if ( $dir ) {
$entry = @readdir( $dir );
while ( ! $size['timeout_occurred'] && false !== $entry ) {
if ( '.' === $entry || '..' === $entry ) {
$entry = @readdir( $dir );
continue;
}
$full_path = $path . DIRECTORY_SEPARATOR . $entry;
if ( @is_dir( $full_path ) ) {
$size = $this->dirsize( $full_path, $size, $timeout_time );
} else {
$size['bytes'] += @filesize( $full_path );
// dont check time() for each file, quite expensive.
++$size['items'];
if ( 0 === $size['items'] % 1000 ) {
$size['timeout_occurred'] |= ( time() > $timeout_time );
}
}
$entry = @readdir( $dir );
}
@closedir( $dir );
}
return $size;
}
/**
* Sets a new value if the old value matches the current value.
*
* This method checks if the current value in the cache matches the provided old value. If they match, it sets the new value.
* Cannot guarantee atomicity due to potential file lock failures.
*
* @param string $key Cache key.
* @param mixed $old_value The expected current value.
* @param mixed $new_value The value to set if the old value matches.
*
* @return bool True if the value was set, false otherwise.
*/
public function set_if_maybe_equals( $key, $old_value, $new_value ) {
// Cant guarantee atomic action here, filelocks fail often.
$value = $this->get( $key );
if ( isset( $old_value['content'] ) && $value['content'] !== $old_value['content'] ) {
return false;
}
return $this->set( $key, $new_value );
}
/**
* Increments a counter stored in the cache by a given value.
*
* This method appends the increment value to the counter file. If the value is 1, it stores it as 'x' for efficiency. Larger
* increments are stored as space-separated integers.
*
* @param string $key Cache key.
* @param int $value The increment value (must be non-zero).
*
* @return bool True on success, false on failure.
*/
public function counter_add( $key, $value ) {
if ( 0 === $value ) {
return true;
}
$fp = $this->fopen_write( $key, '', 'a' );
if ( ! $fp ) {
return false;
}
// use "x" to store increment, since it's most often case
// and it will save 50% of size if only increments are used.
if ( 1 === $value ) {
@fputs( $fp, 'x' );
} else {
@fputs( $fp, ' ' . (int) $value );
}
@fclose( $fp );
return true;
}
/**
* Sets a counter value in the cache.
*
* This method initializes a counter file with the provided value, along with an expiration time and a PHP exit directive to
* prevent execution.
*
* @param string $key Cache key.
* @param int $value The counter value to set.
*
* @return bool True on success, false on failure.
*/
public function counter_set( $key, $value ) {
$fp = $this->fopen_write( $key, '', 'wb' );
if ( ! $fp ) {
return false;
}
$expire = W3TC_CACHE_FILE_EXPIRE_MAX;
$expires_at = time() + $expire;
@fputs( $fp, pack( 'L', $expires_at ) );
@fputs( $fp, '<?php exit; ?>' );
@fputs( $fp, (int) $value );
@fclose( $fp );
return true;
}
/**
* Retrieves the value of a counter from the cache.
*
* This method reads the counter file and calculates the total value by counting occurrences of 'x' and summing other stored values.
*
* @param string $key Cache key.
*
* @return int The counter value, or 0 if the key does not exist.
*/
public function counter_get( $key ) {
list( $value, $old_data ) = $this->_get_with_old_raw( $key );
if ( empty( $value ) ) {
return 0;
}
$original_length = strlen( $value );
$cut_value = str_replace( 'x', '', $value );
$count = $original_length - strlen( $cut_value );
// values more than 1 are stored as <space>value.
$a = explode( ' ', $cut_value );
foreach ( $a as $counter_value ) {
$count += (int) $counter_value;
}
return $count;
}
/**
* Open the cache file for writing and return the file pointer.
*
* Ensures the directory structure exists before attempting to open the file.
*
* @param string $key An MD5 of the DB query.
* @param string $group Cache group.
* @param string $mode File mode. For example: 'wb' for write binary.
*
* @return resource|false File pointer on success, false on failure.
*/
private function fopen_write( $key, $group, $mode ) {
// Get the storage key (format: "w3tc_INSTANCEID_HOST_BLOGID_dbcache_$key").
$storage_key = $this->get_item_key( $key );
// Get the subpath for the cache file (format: [0-9a-f]{3}/[0-9a-f]{3}/[0-9a-f]{32}).
$sub_path = $this->_get_path( $storage_key, $group );
// Ge the entire path of the cache file.
$path = $this->_cache_dir . DIRECTORY_SEPARATOR . $sub_path;
// Create the directory if it does not exist.
$dir = dirname( $path );
if ( ! @is_dir( $dir ) ) {
if ( ! Util_File::mkdir_from( $dir, dirname( W3TC_CACHE_DIR ) ) ) {
return false;
}
}
// Open the cache file for writing.
return @fopen( $path, $mode );
}
/**
* Flushes cache files matching a specific regex pattern.
*
* This method scans a directory and removes cache files that match the provided regular expression. Supports multisite setups.
*
* @since 2.7.1
*
* @param string $regex The regular expression pattern to match file names.
*
* @return void
*/
private function _flush_based_on_regex( $regex ) {
if ( Util_Environment::is_wpmu() && ! Util_Environment::is_wpmu_subdomain() ) {
$domain = get_home_url();
$parsed = parse_url( $domain );
$host = $parsed['host'];
$path = isset( $parsed['path'] ) ? '/' . trim( $parsed['path'], '/' ) : '';
$flush_dir = W3TC_CACHE_PAGE_ENHANCED_DIR . DIRECTORY_SEPARATOR . $host . $path;
} else {
$flush_dir = W3TC_CACHE_PAGE_ENHANCED_DIR . DIRECTORY_SEPARATOR . Util_Environment::host();
}
$dir = @opendir( $flush_dir );
if ( $dir ) {
$entry = @readdir( $dir );
while ( false !== $entry ) {
if ( '.' === $entry || '..' === $entry ) {
$entry = @readdir( $dir );
continue;
}
if ( preg_match( '~' . $regex . '~', basename( $entry ) ) ) {
Util_File::rmdir( $flush_dir . DIRECTORY_SEPARATOR . $entry );
}
$entry = @readdir( $dir );
}
@closedir( $dir );
}
}
}

View File

@@ -0,0 +1,146 @@
<?php
/**
* File: Cache_File_Cleaner.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cache_File_Cleaner
*
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
* phpcs:disable WordPress.WP.AlternativeFunctions
*/
class Cache_File_Cleaner {
/**
* Cache directory
*
* @var string
*/
protected $_cache_dir = '';
/**
* Clean operation time limit
*
* @var int
*/
protected $_clean_timelimit = 0;
/**
* Exclude files
*
* @var array
*/
protected $_exclude = array();
/**
* PHP5-style constructor
*
* @param array $config Config.
*
* @return void
*/
public function __construct( $config = array() ) {
$this->_cache_dir = ( isset( $config['cache_dir'] ) ? trim( $config['cache_dir'] ) : 'cache' );
$this->_clean_timelimit = ( isset( $config['clean_timelimit'] ) ? (int) $config['clean_timelimit'] : 180 );
$this->_exclude = ( isset( $config['exclude'] ) ? (array) $config['exclude'] : array() );
}
/**
* Run clean operation
*
* @return void
*/
public function clean() {
@set_time_limit( $this->_clean_timelimit ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
$this->_clean( $this->_cache_dir, false );
}
/**
* Run clean operation
*
* @return void
*/
public function clean_before() {
@set_time_limit( $this->_clean_timelimit ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
$this->_clean( $this->_cache_dir, false );
}
/**
* Clean
*
* @param string $path Path.
* @param bool $remove Remove flag.
*
* @return void
*/
public function _clean( $path, $remove = true ) {
$dir = @opendir( $path );
if ( $dir ) {
$entry = @readdir( $dir );
while ( false !== $entry ) {
if ( '.' === $entry || '..' === $entry ) {
$entry = @readdir( $dir );
continue;
}
foreach ( $this->_exclude as $mask ) {
if ( fnmatch( $mask, basename( $entry ) ) ) {
continue 2;
}
}
$full_path = $path . DIRECTORY_SEPARATOR . $entry;
if ( @is_dir( $full_path ) ) {
$this->_clean( $full_path );
} elseif ( ! $this->is_valid( $full_path ) ) {
@unlink( $full_path );
}
$entry = @readdir( $dir );
}
@closedir( $dir );
if ( $remove ) {
@rmdir( $path );
}
}
}
/**
* Check if file is valid
*
* @param string $file File.
*
* @return bool
*/
public function is_valid( $file ) {
$valid = false;
if ( file_exists( $file ) ) {
$fp = @fopen( $file, 'rb' );
if ( $fp ) {
$expires = @fread( $fp, 4 );
if ( false !== $expires ) {
list( , $expires_at ) = @unpack( 'L', $expires );
$valid = ( time() < $expires_at );
}
@fclose( $fp );
}
}
return $valid;
}
}

View File

@@ -0,0 +1,198 @@
<?php
/**
* File: Cache_File_Cleaner_Generic.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cache_File_Cleaner_Generic
*
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
* phpcs:disable WordPress.WP.AlternativeFunctions
*/
class Cache_File_Cleaner_Generic extends Cache_File_Cleaner {
/**
* Number of items processed
*
* @var integer
*/
private $processed_count = 0;
/**
* Cache expire time
*
* @var int
*/
private $_expire = 0;
/**
* Minimum valid time
*
* @var int
*/
private $time_min_valid = -1;
/**
* Old file minimum valid time
*
* @var int
*/
private $old_file_time_min_valid = -1;
/**
* PHP5-style constructor
*
* @param array $config Config.
*
* @return void
*/
public function __construct( $config = array() ) {
parent::__construct( $config );
$this->_expire = ( isset( $config['expire'] ) ? (int) $config['expire'] : 0 );
if ( ! $this->_expire ) {
$this->_expire = 0;
} elseif ( $this->_expire > W3TC_CACHE_FILE_EXPIRE_MAX ) {
$this->_expire = W3TC_CACHE_FILE_EXPIRE_MAX;
}
if ( ! empty( $config['time_min_valid'] ) ) {
$this->time_min_valid = $config['time_min_valid'];
$this->old_file_time_min_valid = $config['time_min_valid'];
} elseif ( $this->_expire > 0 ) {
$this->time_min_valid = time() - $this->_expire;
$this->old_file_time_min_valid = time() - $this->_expire * 5;
}
}
/**
* Clean
*
* @param string $path Path.
* @param bool $remove Remove flag.
*
* @return void
*/
public function _clean( $path, $remove = false ) {
$dir = false;
if ( is_dir( $path ) ) {
$dir = @opendir( $path );
}
if ( $dir ) {
$entry = @readdir( $dir );
while ( false !== $entry ) {
if ( '.' === $entry || '..' === $entry ) {
$entry = @readdir( $dir );
continue;
}
$full_path = $path . DIRECTORY_SEPARATOR . $entry;
foreach ( $this->_exclude as $mask ) {
if ( fnmatch( $mask, basename( $entry ) ) ) {
$entry = @readdir( $dir );
continue 2;
}
}
if ( @is_dir( $full_path ) ) {
$this->_clean( $full_path );
} else {
$this->_clean_file( $entry, $full_path );
}
$entry = @readdir( $dir );
}
@closedir( $dir );
if ( $this->is_empty_dir( $path ) ) {
@rmdir( $path );
}
}
}
/**
* Clean file
*
* @param string $entry Entry.
* @param string $full_path Full path.
*
* @return void
*/
public function _clean_file( $entry, $full_path ) {
if ( '_old' === substr( $entry, -4 ) ) {
if ( ! $this->is_old_file_valid( $full_path ) ) {
++$this->processed_count;
@unlink( $full_path );
}
} elseif ( ! $this->is_valid( $full_path ) ) {
$old_entry_path = $full_path . '_old';
++$this->processed_count;
if ( ! @rename( $full_path, $old_entry_path ) ) {
// if we can delete old entry - do second attempt to store in old-entry file.
if ( @unlink( $old_entry_path ) ) {
if ( ! @rename( $full_path, $old_entry_path ) ) {
// last attempt - just remove entry.
@unlink( $full_path );
}
}
}
}
}
/**
* Checks if file is valid
*
* @param string $file File.
*
* @return bool
*/
public function is_valid( $file ) {
if ( $this->time_min_valid > 0 && file_exists( $file ) ) {
$ftime = @filemtime( $file );
if ( $ftime && $ftime >= $this->time_min_valid ) {
return true;
}
}
return false;
}
/**
* Checks if old file is valid
*
* @param string $file File.
*
* @return bool
*/
public function is_old_file_valid( $file ) {
if ( $this->old_file_time_min_valid > 0 && file_exists( $file ) ) {
$ftime = @filemtime( $file );
if ( $ftime && $ftime >= $this->old_file_time_min_valid ) {
return true;
}
}
return false;
}
/**
* Checks if directory is empty
*
* @param string $dir Directory.
*
* @return bool
*/
public function is_empty_dir( $dir ) {
$files = @scandir( $dir );
return $files && count( $files ) <= 2;
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* File: Cache_File_Cleaner_Generic_HardDelete.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cache_File_Cleaner_Generic_HardDelete
*
* phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod.Found
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
* phpcs:disable WordPress.WP.AlternativeFunctions.unlink_unlink
*/
class Cache_File_Cleaner_Generic_HardDelete extends Cache_File_Cleaner_Generic {
/**
* Constructor
*
* @param Config $config Config.
*
* @return void
*/
public function __construct( $config = array() ) {
parent::__construct( $config );
}
/**
* Clean file
*
* @param string $entry Entry.
* @param string $full_path Full path.
*
* @return void
*/
public function _clean_file( $entry, $full_path ) {
if ( substr( $entry, -4 ) === '_old' && ! $this->is_old_file_valid( $full_path ) ) {
++$this->processed_count;
@unlink( $full_path );
} elseif ( ! $this->is_valid( $full_path ) ) {
@unlink( $full_path );
}
}
}

View File

@@ -0,0 +1,527 @@
<?php
/**
* File: Cache_File_Generic.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cache_File_Generic
*
* Disk:Enhanced file cache
*
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
* phpcs:disable Squiz.Strings.DoubleQuoteUsage.NotRequired
* phpcs:disable WordPress.WP.AlternativeFunctions
*/
class Cache_File_Generic extends Cache_File {
/**
* Expire
*
* @var integer
*/
private $_expire = 0;
/**
* PHP5-style constructor
*
* @param Config $config Config.
*
* @return void
*/
public function __construct( $config = array() ) {
parent::__construct( $config );
$this->_expire = ( isset( $config['expire'] ) ? (int) $config['expire'] : 0 );
if ( ! $this->_expire || $this->_expire > W3TC_CACHE_FILE_EXPIRE_MAX ) {
$this->_expire = W3TC_CACHE_FILE_EXPIRE_MAX;
}
}
/**
* Sets data
*
* @param string $key Key.
* @param string $value Value.
* @param int $expire Time to expire.
* @param string $group Used to differentiate between groups of cache values.
*
* @return boolean
*/
public function set( $key, $value, $expire = 0, $group = '' ) {
$key = $this->get_item_key( $key );
$sub_path = $this->_get_path( $key, $group );
$path = $this->_cache_dir . DIRECTORY_SEPARATOR . $sub_path;
$dir = dirname( $path );
if ( ! @is_dir( $dir ) ) {
if ( ! Util_File::mkdir_from_safe( $dir, dirname( W3TC_CACHE_DIR ) ) ) {
return false;
}
}
$tmppath = $path . '.' . getmypid();
$fp = @fopen( $tmppath, 'wb' );
if ( ! $fp ) {
return false;
}
if ( $this->_locking ) {
@flock( $fp, LOCK_EX );
}
@fputs( $fp, $value['content'] );
@fclose( $fp );
$chmod = 0644;
if ( defined( 'FS_CHMOD_FILE' ) ) {
$chmod = FS_CHMOD_FILE;
}
@chmod( $tmppath, $chmod );
if ( $this->_locking ) {
@flock( $fp, LOCK_UN );
}
// some hostings create files with restrictive permissions not allowing apache to read it later.
@chmod( $path, 0644 );
if ( @filesize( $tmppath ) > 0 ) {
@unlink( $path );
@rename( $tmppath, $path );
}
@unlink( $tmppath );
$old_entry_path = $path . '_old';
@unlink( $old_entry_path );
if ( Util_Environment::is_apache() && isset( $value['headers'] ) ) {
$rules = '';
if ( isset( $value['headers']['Content-Type'] ) && 'text/xml' === substr( $value['headers']['Content-Type'], 0, 8 ) ) {
$rules .= "<IfModule mod_mime.c>\n";
$rules .= " RemoveType .html_gzip\n";
$rules .= " AddType text/xml .html_gzip\n";
$rules .= " RemoveType .html\n";
$rules .= " AddType text/xml .html\n";
$rules .= "</IfModule>\n";
}
if ( isset( $value['headers'] ) ) {
$headers = array();
foreach ( $value['headers'] as $h ) {
if ( isset( $h['n'] ) && isset( $h['v'] ) ) {
$h2 = apply_filters( 'w3tc_pagecache_set_header', $h, $h, 'file_generic' );
if ( ! empty( $h2 ) ) {
$name_escaped = $this->escape_header_name( $h2['n'] );
if ( ! isset( $headers[ $name_escaped ] ) ) {
$headers[ $name_escaped ] = array(
'values' => array(),
'files_match' => $h2['files_match'],
);
}
$value_escaped = $this->escape_header_value( $h2['v'] );
if ( ! empty( $value_escaped ) ) {
$headers[ $name_escaped ]['values'][] =
" Header add " .
$name_escaped .
" '" . $value_escaped . "'\n";
}
}
}
}
$header_rules = '';
foreach ( $headers as $name_escaped => $value ) {
// Link header doesnt apply to .xml assets.
$header_rules .= ' <FilesMatch "' . $value['files_match'] . "\">\n";
$header_rules .= " Header unset $name_escaped\n";
$header_rules .= implode( "\n", $value['values'] );
$header_rules .= " </FilesMatch>\n";
}
if ( ! empty( $header_rules ) ) {
$rules .= "<IfModule mod_headers.c>\n";
$rules .= $header_rules;
$rules .= "</IfModule>\n";
}
}
if ( ! empty( $rules ) ) {
$htaccess_path = dirname( $path ) . DIRECTORY_SEPARATOR . '.htaccess';
@file_put_contents( $htaccess_path, $rules );
$chmod = 0644;
if ( defined( 'FS_CHMOD_FILE' ) ) {
$chmod = FS_CHMOD_FILE;
}
@chmod( $htaccess_path, $chmod );
}
}
return true;
}
/**
* Escape header name
*
* @param string $v Value.
*
* @return array
*/
private function escape_header_name( $v ) {
return preg_replace( '~[^0-9A-Za-z\-]~m', '_', $v );
}
/**
* Escape header value
*
* @param string $v Value.
*
* @return array
*/
private function escape_header_value( $v ) {
return str_replace(
"'",
"\\'",
str_replace(
"\\",
"\\\\\\", // htaccess need escape of \ to \\\.
preg_replace( '~[\r\n]~m', '_', trim( $v ) )
)
);
}
/**
* Returns data
*
* @param string $key Key.
* @param string $group Used to differentiate between groups of cache values.
*
* @return array
*/
public function get_with_old( $key, $group = '' ) {
$has_old_data = false;
$key = $this->get_item_key( $key );
$path = $this->_cache_dir . DIRECTORY_SEPARATOR . $this->_get_path( $key, $group );
$data = $this->_read( $path );
if ( null !== $data ) {
return array( $data, $has_old_data );
}
$path_old = $path . '_old';
$too_old_time = time() - 30;
$exists = file_exists( $path_old );
if ( $exists ) {
$file_time = @filemtime( $path_old );
if ( $file_time ) {
if ( $file_time > $too_old_time ) {
// return old data.
$has_old_data = true;
return array( $this->_read( $path_old ), $has_old_data );
}
// use old enough time to cause recalculation on next call.
@touch( $path_old, 1479904835 );
}
}
$has_old_data = $exists;
return array( null, $has_old_data );
}
/**
* Reads file
*
* @param string $path Path.
*
* @return array
*/
private function _read( $path ) {
// Canonicalize path to avoid unexpected variants.
$path = realpath( $path );
if ( ! is_readable( $path ) ) {
return null;
}
// make sure reading from cache folder canonicalize to avoid unexpected variants.
$base_path = realpath( $this->_cache_dir );
if ( strlen( $base_path ) <= 0 || substr( $path, 0, strlen( $base_path ) ) !== $base_path ) {
return null;
}
$fp = @fopen( $path, 'rb' );
if ( ! $fp ) {
return null;
}
if ( $this->_locking ) {
@flock( $fp, LOCK_SH );
}
$var = '';
while ( ! @feof( $fp ) ) {
$var .= @fread( $fp, 4096 );
}
@fclose( $fp );
if ( $this->_locking ) {
@flock( $fp, LOCK_UN );
}
$headers = array();
if ( '.xml' === substr( $path, -4 ) ) {
$headers['Content-type'] = 'text/xml';
}
return array(
'404' => false,
'headers' => $headers,
'time' => null,
'content' => $var,
);
}
/**
* Deletes data
*
* @param string $key Key.
* @param string $group Used to differentiate between groups of cache values.
*
* @return boolean
*/
public function delete( $key, $group = '' ) {
$key = $this->get_item_key( $key );
$path = $this->_cache_dir . DIRECTORY_SEPARATOR . $this->_get_path( $key, $group );
if ( ! file_exists( $path ) ) {
return true;
}
$dir = dirname( $path );
if ( file_exists( $dir . DIRECTORY_SEPARATOR . '.htaccess' ) ) {
@unlink( $dir . DIRECTORY_SEPARATOR . '.htaccess' );
}
$old_entry_path = $path . '_old';
if ( ! @rename( $path, $old_entry_path ) ) {
// if we can delete old entry - do second attempt to store in old-entry file.
if ( ! @unlink( $old_entry_path ) || ! @rename( $path, $old_entry_path ) ) {
return @unlink( $path );
}
}
/**
* Disabling this as we don't want to immediately hard-expire _old cache files as there is a
* 30 second window where they are still served via get_with_old calls. During AWS testing on
* WP 5.9/6.3 this was resulting in the _old file immediately being removed during the clean
* operation, resulting in failed automated tests (8/1/2023)
*/
// @touch( $old_entry_path, 1479904835 ); phpcs:ignore Squiz.PHP.CommentedOutCode.Found
return true;
}
/**
* Checks if entry exists
*
* @param string $key Key.
* @param string $group Used to differentiate between groups of cache values.
* @return boolean true if exists, false otherwise
*/
public function exists( $key, $group = '' ) {
$key = $this->get_item_key( $key );
$path = $this->_cache_dir . DIRECTORY_SEPARATOR . $this->_get_path( $key, $group );
return file_exists( $path );
}
/**
* Key to delete, deletes _old and primary if exists.
*
* @param string $key Key.
* @param string $group Group.
*
* @return bool
*/
public function hard_delete( $key, $group = '' ) {
$key = $this->get_item_key( $key );
$path = $this->_cache_dir . DIRECTORY_SEPARATOR . $this->_get_path( $key, $group );
$old_entry_path = $path . '_old';
@unlink( $old_entry_path );
if ( ! file_exists( $path ) ) {
return true;
}
@unlink( $path );
return true;
}
/**
* Flushes all data
*
* @param string $group Used to differentiate between groups of cache values.
*
* @return boolean
*/
public function flush( $group = '' ) {
if ( 'sitemaps' === $group ) {
$config = Dispatcher::config();
$sitemap_regex = $config->get_string( 'pgcache.purge.sitemap_regex' );
$this->_flush_based_on_regex( $sitemap_regex );
} else {
$dir = $this->_flush_dir;
if ( ! empty( $group ) ) {
$c = new Cache_File_Cleaner_Generic_HardDelete(
array(
'cache_dir' => $this->_flush_dir . DIRECTORY_SEPARATOR . $group,
'exclude' => $this->_exclude, // phpcs:ignore WordPressVIPMinimum
'clean_timelimit' => $this->_flush_timelimit,
)
);
} else {
$c = new Cache_File_Cleaner_Generic(
array(
'cache_dir' => $this->_flush_dir,
'exclude' => $this->_exclude, // phpcs:ignore WordPressVIPMinimum
'clean_timelimit' => $this->_flush_timelimit,
)
);
}
$c->clean();
}
return true;
}
/**
* Gets a key extension for "ahead generation" mode.
* Used by AlwaysCached functionality to regenerate content
*
* @param string $group Used to differentiate between groups of cache values.
*
* @return array
*/
public function get_ahead_generation_extension( $group ) {
return array(
'before_time' => time(),
);
}
/**
* Flushes group with before condition
*
* @param string $group Used to differentiate between groups of cache values.
* @param array $extension Used to set a condition what version to flush.
*
* @return void
*/
public function flush_group_after_ahead_generation( $group, $extension ) {
$dir = $this->_flush_dir;
if ( ! empty( $group ) ) {
$c = new Cache_File_Cleaner_Generic_HardDelete(
array(
'cache_dir' => $this->_flush_dir . DIRECTORY_SEPARATOR . $group,
'exclude' => $this->_exclude, // phpcs:ignore WordPressVIPMinimum
'clean_timelimit' => $this->_flush_timelimit,
'time_min_valid' => $extension['before_time'],
)
);
} else {
$c = new Cache_File_Cleaner_Generic(
array(
'cache_dir' => $this->_flush_dir,
'exclude' => $this->_exclude, // phpcs:ignore WordPressVIPMinimum
'clean_timelimit' => $this->_flush_timelimit,
'time_min_valid' => $extension['before_time'],
)
);
}
$c->clean();
}
/**
* Returns cache file path by key
*
* @param string $key Key.
* @param string $group Group.
*
* @return string
*/
public function _get_path( $key, $group = '' ) {
return ( empty( $group ) ? '' : $group . DIRECTORY_SEPARATOR ) . $key;
}
/**
* Returns item key
*
* @param string $key Key.
*
* @return string
*/
public function get_item_key( $key ) {
return $key;
}
/**
* Flush cache based on regex
*
* @param string $regex Regex.
*
* @return void
*/
private function _flush_based_on_regex( $regex ) {
if ( Util_Environment::is_wpmu() && ! Util_Environment::is_wpmu_subdomain() ) {
$domain = get_home_url();
$parsed = wp_parse_url( $domain );
$host = $parsed['host'];
$path = isset( $parsed['path'] ) ? '/' . trim( $parsed['path'], '/' ) : '';
$flush_dir = W3TC_CACHE_PAGE_ENHANCED_DIR . DIRECTORY_SEPARATOR . $host . $path;
} else {
$flush_dir = W3TC_CACHE_PAGE_ENHANCED_DIR . DIRECTORY_SEPARATOR . Util_Environment::host();
}
$dir = @opendir( $flush_dir );
if ( $dir ) {
$entry = @readdir( $dir );
while ( false !== $entry ) {
if ( '.' === $entry || '..' === $entry ) {
$entry = @readdir( $dir );
continue;
}
if ( preg_match( '~' . $regex . '~', basename( $entry ) ) ) {
Util_File::rmdir( $flush_dir . DIRECTORY_SEPARATOR . $entry );
}
$entry = @readdir( $dir );
}
@closedir( $dir );
}
}
}

View File

@@ -0,0 +1,490 @@
<?php
/**
* File: Cache_Memcache.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cache_Memcache
*
* PECL Memcache class
* Older than Memcached
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
* phpcs:disable Universal.CodeAnalysis.ConstructorDestructorReturn.ReturnValueFound
*/
class Cache_Memcache extends Cache_Base {
/**
* Memcache object
*
* @var Memcache
*/
private $_memcache = null;
/**
* Used for faster flushing
*
* @var integer $_key_version
*/
private $_key_version = array();
/**
* Cache_Memcache constructor.
*
* Initializes the Memcache connection and sets up servers based on the provided configuration.
*
* @param array $config {
* Configuration for Memcache, including.
*
* @type array $servers List of Memcache server endpoints.
* @type bool $persistent Whether to use persistent connections.
* @type string $key_version_mode Mode for key versioning ('disabled' to disable it).
* }
*/
public function __construct( $config ) {
parent::__construct( $config );
$this->_memcache = new \Memcache();
if ( ! empty( $config['servers'] ) ) {
$persistent = isset( $config['persistent'] ) ? (bool) $config['persistent'] : false;
foreach ( (array) $config['servers'] as $server ) {
list( $ip, $port ) = Util_Content::endpoint_to_host_port( $server );
$this->_memcache->addServer( $ip, $port, $persistent );
}
} else {
return false;
}
// when disabled - no extra requests are made to obtain key version, but flush operations not supported as a result
// group should be always empty.
if ( isset( $config['key_version_mode'] ) && 'disabled' === $config['key_version_mode'] ) {
$this->_key_version[''] = 1;
}
return true;
}
/**
* Adds a new value to the cache.
*
* If the key already exists, it will overwrite the value. This method is functionally equivalent to `set()`.
*
* @param string $key Cache key.
* @param mixed $value Value to store.
* @param int $expire Time to live for the cached item in seconds. Default is 0 (no expiration).
* @param string $group Cache group. Default is an empty string.
*
* @return bool True on success, false on failure.
*/
public function add( $key, &$value, $expire = 0, $group = '' ) {
return $this->set( $key, $value, $expire, $group );
}
/**
* Sets a value in the cache.
*
* This method stores the value in Memcache with a specific key, expiration time, and group. It also includes a versioning
* mechanism to handle key updates.
*
* @param string $key Cache key.
* @param mixed $value Value to store.
* @param int $expire Time to live for the cached item in seconds. Default is 0 (no expiration).
* @param string $group Cache group. Default is an empty string.
*
* @return bool True on success, false on failure.
*/
public function set( $key, $value, $expire = 0, $group = '' ) {
if ( ! isset( $value['key_version'] ) ) {
$value['key_version'] = $this->_get_key_version( $group );
}
$storage_key = $this->get_item_key( $key );
return @$this->_memcache->set( $storage_key, $value, false, $expire );
}
/**
* Retrieves a value and its old version from the cache.
*
* This method fetches the cached value for the given key. If the key version does not match the current version, it may return
* expired data based on configuration.
*
* @param string $key Cache key.
* @param string $group Cache group. Default is an empty string.
*
* @return array Array containing the value (or null) and a boolean indicating if old data was returned.
*/
public function get_with_old( $key, $group = '' ) {
$has_old_data = false;
$storage_key = $this->get_item_key( $key );
$v = @$this->_memcache->get( $storage_key );
if ( ! is_array( $v ) || ! isset( $v['key_version'] ) ) {
return array( null, $has_old_data );
}
$key_version = $this->_get_key_version( $group );
if ( $v['key_version'] === $key_version ) {
return array( $v, $has_old_data );
}
if ( $v['key_version'] > $key_version ) {
if ( ! empty( $v['key_version_at_creation'] ) && $v['key_version_at_creation'] !== $key_version ) {
$this->_set_key_version( $v['key_version'], $group );
}
return array( $v, $has_old_data );
}
// key version is old.
if ( ! $this->_use_expired_data ) {
return array( null, $has_old_data );
}
// if we have expired data - update it for future use and let current process recalculate it.
$expires_at = isset( $v['expires_at'] ) ? $v['expires_at'] : null;
if ( null === $expires_at || time() > $expires_at ) {
$v['expires_at'] = time() + 30;
@$this->_memcache->set( $storage_key, $v, false, 0 );
$has_old_data = true;
return array( null, $has_old_data );
}
// return old version.
return array( $v, $has_old_data );
}
/**
* Replaces the value for an existing key in the cache.
*
* This method is functionally equivalent to `set()`.
*
* @param string $key Cache key.
* @param mixed $value Value to store.
* @param int $expire Time to live for the cached item in seconds. Default is 0 (no expiration).
* @param string $group Cache group. Default is an empty string.
*
* @return bool True on success, false on failure.
*/
public function replace( $key, &$value, $expire = 0, $group = '' ) {
return $this->set( $key, $value, $expire, $group );
}
/**
* Deletes a value from the cache.
*
* If expired data is allowed, it sets the key version to 0 and updates the cache. Otherwise, it removes the key completely.
*
* @param string $key Cache key.
* @param string $group Cache group. Default is an empty string.
*
* @return bool True on success, false on failure.
*/
public function delete( $key, $group = '' ) {
$storage_key = $this->get_item_key( $key );
if ( $this->_use_expired_data ) {
$v = @$this->_memcache->get( $storage_key );
if ( is_array( $v ) ) {
$v['key_version'] = 0;
@$this->_memcache->set( $storage_key, $v, false, 0 );
return true;
}
}
return @$this->_memcache->delete( $storage_key, 0 );
}
/**
* Deletes a key and its value from the cache without considering versioning.
*
* @param string $key Cache key to delete.
* @param string $group Cache group. Default is an empty string.
*
* @return bool True on success, false on failure.
*/
public function hard_delete( $key, $group = '' ) {
$storage_key = $this->get_item_key( $key );
return @$this->_memcache->delete( $storage_key, 0 );
}
/**
* Flushes the cache for the specified group by incrementing its key version.
*
* @param string $group Cache group. Default is an empty string.
*
* @return bool Always returns true.
*/
public function flush( $group = '' ) {
$this->_increment_key_version( $group );
return true;
}
/**
* Prepares an ahead-generation extension for cache key versioning.
*
* Used to create a new version of a cache key for precomputing or updating data.
*
* @param string $group Cache group.
*
* @return array Associative array with:
* - 'key_version' (int): The new key version.
* - 'key_version_at_creation' (int): The current key version.
*/
public function get_ahead_generation_extension( $group ) {
$v = $this->_get_key_version( $group );
return array(
'key_version' => $v + 1,
'key_version_at_creation' => $v,
);
}
/**
* Updates the cache key version after an ahead-generation operation.
*
* @param string $group Cache group.
* @param array $extension {
* The extension data with the new key version.
*
* @type string $key_version The new cache key version.
* }
*
* @return void
*/
public function flush_group_after_ahead_generation( $group, $extension ) {
$v = $this->_get_key_version( $group );
if ( $extension['key_version'] > $v ) {
$this->_set_key_version( $extension['key_version'], $group );
}
}
/**
* Checks if the Memcache extension is available.
*
* @return bool True if Memcache is available, false otherwise.
*/
public function available() {
return class_exists( 'Memcache' );
}
/**
* Retrieves statistics about the Memcache instance.
*
* @return array|false An associative array of Memcache statistics, or false on failure.
*/
public function get_statistics() {
return $this->_memcache->getStats();
}
/**
* Gets the current key version for a cache group.
*
* @param string $group Cache group. Default is an empty string.
*
* @return int The current key version.
*/
private function _get_key_version( $group = '' ) {
if ( ! isset( $this->_key_version[ $group ] ) || $this->_key_version[ $group ] <= 0 ) {
$v = @$this->_memcache->get( $this->_get_key_version_key( $group ) );
$v = intval( $v );
$this->_key_version[ $group ] = ( $v > 0 ? $v : 1 );
}
return $this->_key_version[ $group ];
}
/**
* Sets a new key version for a cache group.
*
* @param int $v The new key version.
* @param string $group Cache group. Default is an empty string.
*
* @return void
*/
private function _set_key_version( $v, $group = '' ) {
// expiration has to be as long as possible since all cache data expires when key version expires.
@$this->_memcache->set( $this->_get_key_version_key( $group ), $v, false, 0 );
$this->_key_version[ $group ] = $v;
}
/**
* Increments the key version for a cache group.
*
* If the key does not exist, initializes it with version 2.
*
* @since 0.14.5
*
* @param string $group Cache group. Default is an empty string.
*
* @return void
*/
private function _increment_key_version( $group = '' ) {
$r = @$this->_memcache->increment( $this->_get_key_version_key( $group ), 1 );
if ( $r ) {
$this->_key_version[ $group ] = $r;
} else {
// it doesn't initialize the key if it doesn't exist.
$this->_set_key_version( 2, $group );
}
}
/**
* Retrieves the size and item count of the cache.
*
* This method collects statistics about the size of the cache and the number of items stored in it.
*
* @param int $timeout_time The timeout duration for retrieving stats.
*
* @return array Associative array with:
* - 'bytes' (int): Total size of the cached items in bytes.
* - 'items' (int): Total number of cached items.
* - 'timeout_occurred' (bool): Whether a timeout occurred while retrieving stats.
*/
public function get_stats_size( $timeout_time ) {
$size = array(
'bytes' => 0,
'items' => 0,
'timeout_occurred' => false,
);
$key_prefix = $this->get_item_key( '' );
$slabs = @$this->_memcache->getExtendedStats( 'slabs' );
$slabs_plain = array();
if ( is_array( $slabs ) ) {
foreach ( $slabs as $server => $server_slabs ) {
foreach ( $server_slabs as $slab_id => $slab_meta ) {
if ( (int) $slab_id > 0 ) {
$slabs_plain[ (int) $slab_id ] = '*';
}
}
}
}
foreach ( $slabs_plain as $slab_id => $nothing ) {
$cdump = @$this->_memcache->getExtendedStats( 'cachedump', (int) $slab_id );
if ( ! is_array( $cdump ) ) {
continue;
}
foreach ( $cdump as $server => $keys_data ) {
if ( ! is_array( $keys_data ) ) {
continue;
}
foreach ( $keys_data as $key => $size_expiration ) {
if ( substr( $key, 0, strlen( $key_prefix ) ) === $key_prefix ) {
if ( count( $size_expiration ) > 0 ) {
$size['bytes'] += $size_expiration[0];
++$size['items'];
}
}
}
}
}
return $size;
}
/**
* Conditionally sets a new value if the current value matches the old value.
*
* Since Memcache does not support Compare-And-Swap (CAS), atomicity cannot be guaranteed.
*
* @param string $key The cache key.
* @param array $old_value {
* The expected old value.
*
* @type mixed $content The content to match against.
* }
* @param array $new_value The new value to set if the old value matches.
*
* @return bool True if the value was set, false otherwise.
*/
public function set_if_maybe_equals( $key, $old_value, $new_value ) {
// cant guarantee atomic action here, memcache doesnt support CAS.
$value = $this->get( $key );
if ( isset( $old_value['content'] ) && $value['content'] !== $old_value['content'] ) {
return false;
}
return $this->set( $key, $new_value );
}
/**
* Adds a value to a counter stored in the cache.
*
* If the counter does not exist, it initializes the counter to 0 before adding the value.
*
* @param string $key The cache key for the counter.
* @param int $value The value to add to the counter. If 0, no changes are made.
*
* @return int|bool The new counter value on success, or false on failure.
*/
public function counter_add( $key, $value ) {
if ( 0 === $value ) {
return true;
}
$storage_key = $this->get_item_key( $key );
$r = @$this->_memcache->increment( $storage_key, $value );
if ( ! $r ) { // it doesnt initialize counter by itself.
$this->counter_set( $key, 0 );
}
return $r;
}
/**
* Sets a counter to a specific value.
*
* @param string $key The cache key for the counter.
* @param int $value The value to set for the counter.
*
* @return bool True on success, false on failure.
*/
public function counter_set( $key, $value ) {
$storage_key = $this->get_item_key( $key );
return @$this->_memcache->set( $storage_key, $value );
}
/**
* Retrieves the current value of a counter stored in the cache.
*
* @param string $key The cache key for the counter.
*
* @return int The counter value. Returns 0 if the counter does not exist.
*/
public function counter_get( $key ) {
$storage_key = $this->get_item_key( $key );
$v = (int) @$this->_memcache->get( $storage_key );
return $v;
}
/**
* Generates a unique storage key for a given cache item.
*
* The key includes the instance ID, host, blog ID, module, and an MD5 hash of the item name. Spaces in the name are sanitized
* to ensure compatibility with Memcache.
*
* @param string $name The name of the cache item.
*
* @return string The unique storage key for the cache item.
*/
public function get_item_key( $name ) {
// memcached doesn't survive spaces in a key.
$key = sprintf( 'w3tc_%d_%s_%d_%s_%s', $this->_instance_id, $this->_host, $this->_blog_id, $this->_module, md5( $name ) );
return $key;
}
}

View File

@@ -0,0 +1,586 @@
<?php
/**
* File: Cache_Memcached.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cache_Memcached
*
* PECL Memcached class
* Preferred upon Memcache
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
* phpcs:disable Universal.CodeAnalysis.ConstructorDestructorReturn.ReturnValueFound
*/
class Cache_Memcached extends Cache_Base {
/**
* Memcache object
*
* @var Memcache
*/
private $_memcache = null;
/**
* Used for faster flushing
*
* @var integer $_key_version
*/
private $_key_version = array();
/**
* Configuration used to reinitialize persistent object
*
* @var integer $_key_version
*/
private $_config = null;
/**
* Constructor to initialize the Memcached client with provided configuration.
*
* @param array $config {
* Configuration settings for the Memcached client, including server details, options, and credentials.
*
* @type bool $persistent Whether to use persistent connections.
* }
*
* @return bool True on successful initialization, false on failure.
*/
public function __construct( $config ) {
parent::__construct( $config );
if ( isset( $config['persistent'] ) && $config['persistent'] ) {
$this->_config = $config;
$this->_memcache = new \Memcached( $this->_get_key_version_key( '' ) );
$server_list = $this->_memcache->getServerList();
if ( empty( $server_list ) ) {
return $this->initialize( $config );
} else {
return true;
}
} else {
$this->_memcache = new \Memcached();
return $this->initialize( $config );
}
}
/**
* Initializes the Memcached client with the given configuration.
*
* @param array $config {
* Configuration settings for the Memcached client, including server details, options, and credentials.
*
* @type array|string $servers List of Memcached server endpoints (host:port).
* @type bool $binary_protocol Enable binary protocol if true.
* @type bool $aws_autodiscovery Enable AWS autodiscovery if true.
* @type string $username Username for SASL authentication.
* @type string $password Password for SASL authentication.
* @type string $key_version_mode Key versioning mode (disabled to skip versioning).
* }
*
* @return bool True on successful initialization, false on failure.
*/
private function initialize( $config ) {
if ( empty( $config['servers'] ) ) {
return false;
}
if ( defined( '\Memcached::OPT_REMOVE_FAILED_SERVERS' ) ) {
$this->_memcache->setOption( \Memcached::OPT_REMOVE_FAILED_SERVERS, true );
}
if ( isset( $config['binary_protocol'] ) && ! empty( $config['binary_protocol'] ) && defined( '\Memcached::OPT_BINARY_PROTOCOL' ) ) {
$this->_memcache->setOption( \Memcached::OPT_BINARY_PROTOCOL, true );
}
if ( defined( '\Memcached::OPT_TCP_NODELAY' ) ) {
$this->_memcache->setOption( \Memcached::OPT_TCP_NODELAY, true );
}
if (
isset( $config['aws_autodiscovery'] ) &&
$config['aws_autodiscovery'] &&
defined( '\Memcached::OPT_CLIENT_MODE' ) &&
defined( '\Memcached::DYNAMIC_CLIENT_MODE' )
) {
$this->_memcache->setOption( \Memcached::OPT_CLIENT_MODE, \Memcached::DYNAMIC_CLIENT_MODE );
}
foreach ( (array) $config['servers'] as $server ) {
list( $ip, $port ) = Util_Content::endpoint_to_host_port( $server );
$this->_memcache->addServer( $ip, $port );
}
if ( isset( $config['username'] ) && ! empty( $config['username'] ) && method_exists( $this->_memcache, 'setSaslAuthData' ) ) {
$this->_memcache->setSaslAuthData( $config['username'], $config['password'] );
}
// when disabled - no extra requests are made to obtain key version, but flush operations not supported as a result
// group should be always empty.
if ( isset( $config['key_version_mode'] ) && 'disabled' === $config['key_version_mode'] ) {
$this->_key_version[''] = 1;
}
return true;
}
/**
* Adds a new key-value pair to the Memcached server, or updates it if it already exists.
*
* @param string $key The key under which the data will be stored.
* @param mixed $value The data to store.
* @param int $expire The expiration time in seconds (default is 0 for no expiration).
* @param string $group The group to which the item belongs (default is an empty string).
*
* @return bool True on success, false on failure.
*/
public function add( $key, &$value, $expire = 0, $group = '' ) {
return $this->set( $key, $value, $expire, $group );
}
/**
* Sets a key-value pair in the Memcached server, or updates it if it already exists.
*
* @param string $key The key under which the data will be stored.
* @param mixed $value The data to store.
* @param int $expire The expiration time in seconds (default is 0 for no expiration).
* @param string $group The group to which the item belongs (default is an empty string).
*
* @return bool True on success, false on failure.
*/
public function set( $key, $value, $expire = 0, $group = '' ) {
if ( ! isset( $value['key_version'] ) ) {
$value['key_version'] = $this->_get_key_version( $group );
}
$storage_key = $this->get_item_key( $key );
return @$this->_memcache->set( $storage_key, $value, $expire );
}
/**
* Retrieves a value from Memcached along with a flag indicating if old data was found.
*
* Checks for data associated with the given key and ensures the version is up-to-date.
*
* @param string $key The key to fetch from the Memcached server.
* @param string $group The group the item belongs to (default is an empty string).
*
* @return array An array containing the fetched data (or null) and a flag indicating whether old data was returned.
*/
public function get_with_old( $key, $group = '' ) {
$has_old_data = false;
$storage_key = $this->get_item_key( $key );
$v = @$this->_memcache->get( $storage_key );
if ( ! is_array( $v ) || ! isset( $v['key_version'] ) ) {
return array( null, $has_old_data );
}
$key_version = $this->_get_key_version( $group );
if ( $v['key_version'] === $key_version ) {
return array( $v, $has_old_data );
}
if ( $v['key_version'] > $key_version ) {
if ( ! empty( $v['key_version_at_creation'] ) && $v['key_version_at_creation'] !== $key_version ) {
$this->_set_key_version( $v['key_version'], $group );
}
return array( $v, $has_old_data );
}
// key version is old.
if ( ! $this->_use_expired_data ) {
return array( null, $has_old_data );
}
// if we have expired data - update it for future use and let current process recalculate it.
$expires_at = isset( $v['expires_at'] ) ? $v['expires_at'] : null;
if ( null === $expires_at || time() > $expires_at ) {
$v['expires_at'] = time() + 30;
@$this->_memcache->set( $storage_key, $v, 0 );
$has_old_data = true;
return array( null, $has_old_data );
}
// return old version.
return array( $v, $has_old_data );
}
/**
* Replaces an existing key-value pair in Memcached if it already exists.
*
* @param string $key The key under which the data will be stored.
* @param mixed $value The data to store.
* @param int $expire The expiration time in seconds (default is 0 for no expiration).
* @param string $group The group to which the item belongs (default is an empty string).
*
* @return bool True on success, false on failure.
*/
public function replace( $key, &$value, $expire = 0, $group = '' ) {
return $this->set( $key, $value, $expire, $group );
}
/**
* Deletes a key-value pair from Memcached.
*
* @param string $key The key to delete from the Memcached server.
* @param string $group The group the item belongs to (default is an empty string).
*
* @return bool True on success, false on failure.
*/
public function delete( $key, $group = '' ) {
$storage_key = $this->get_item_key( $key );
if ( $this->_use_expired_data ) {
$v = @$this->_memcache->get( $storage_key );
if ( is_array( $v ) ) {
$v['key_version'] = 0;
@$this->_memcache->set( $storage_key, $v, 0 );
return true;
}
}
return @$this->_memcache->delete( $storage_key );
}
/**
* Deletes an item from the Memcached storage.
*
* @param string $key The cache key to delete.
* @param string $group The cache group. Default is an empty string.
*
* @return bool True if the item was successfully deleted, false otherwise.
*/
public function hard_delete( $key, $group = '' ) {
$storage_key = $this->get_item_key( $key );
return @$this->_memcache->delete( $storage_key );
}
/**
* Flushes all items in the Memcached storage.
*
* This method resets the server list if persistent connections are used, and reinitializes the Memcached object with the new
* configuration.
*
* @param string $group The cache group to flush. Default is an empty string.
*
* @return bool Always returns true.
*/
public function flush( $group = '' ) {
$this->_increment_key_version( $group );
// for persistent connections - apply new config to the object otherwise it will keep old servers list.
if ( ! is_null( $this->_config ) ) {
if ( method_exists( $this->_memcache, 'resetServerList' ) ) {
$this->_memcache->resetServerList();
}
$this->initialize( $this->_config );
}
return true;
}
/**
* Returns an array containing the key version and its version at creation for the specified group.
*
* @param string $group The cache group for which to retrieve version information.
*
* @return array Associative array containing 'key_version' and 'key_version_at_creation'.
*/
public function get_ahead_generation_extension( $group ) {
$v = $this->_get_key_version( $group );
return array(
'key_version' => $v + 1,
'key_version_at_creation' => $v,
);
}
/**
* Updates the key version for a given cache group after ahead generation.
*
* If the provided extension's key version is greater than the current version, it updates the key version.
*
* @param string $group The cache group to update.
* @param array $extension {
* The extension data containing the new key version.
*
* @type string $key_version The new key version to set.
* }
*
* @return void
*/
public function flush_group_after_ahead_generation( $group, $extension ) {
$v = $this->_get_key_version( $group );
if ( $extension['key_version'] > $v ) {
$this->_set_key_version( $extension['key_version'], $group );
}
}
/**
* Checks if Memcached is available for use.
*
* @return bool True if the Memcached class exists and is available, false otherwise.
*/
public function available() {
return class_exists( 'Memcached' );
}
/**
* Retrieves Memcached statistics.
*
* @return array|false Memcached statistics if available, false otherwise.
*/
public function get_statistics() {
$a = $this->_memcache->getStats();
if ( ! empty( $a ) && count( $a ) > 0 ) {
$keys = array_keys( $a );
$key = $keys[0];
return $a[ $key ];
}
return $a;
}
/**
* Retrieves the current key version for a specific cache group.
*
* If the version is not set or is invalid, it attempts to fetch it from Memcached.
*
* @param string $group The cache group to get the key version for. Default is an empty string.
*
* @return int The current key version.
*/
private function _get_key_version( $group = '' ) {
if ( ! isset( $this->_key_version[ $group ] ) || $this->_key_version[ $group ] <= 0 ) {
$v = @$this->_memcache->get( $this->_get_key_version_key( $group ) );
$v = intval( $v );
$this->_key_version[ $group ] = ( $v > 0 ? $v : 1 );
}
return $this->_key_version[ $group ];
}
/**
* Sets the key version for a specific cache group.
*
* The expiration is set to 0, which means the version does not expire.
*
* @param int $v The key version to set.
* @param string $group The cache group to set the version for. Default is an empty string.
*
* @return void
*/
private function _set_key_version( $v, $group = '' ) {
// expiration has to be as long as possible since all cache data expires when key version expires.
@$this->_memcache->set( $this->_get_key_version_key( $group ), $v, 0 );
$this->_key_version[ $group ] = $v;
}
/**
* Increments the key version for a specific cache group.
*
* This method attempts to increment the version in Memcached. If the key does not exist, it sets the version to 2 to initialize it.
*
* @since 0.14.5
*
* @param string $group The cache group to increment the version for. Default is an empty string.
*
* @return void
*/
private function _increment_key_version( $group = '' ) {
$r = @$this->_memcache->increment( $this->_get_key_version_key( $group ), 1 );
if ( $r ) {
$this->_key_version[ $group ] = $r;
} else {
// it doesn't initialize the key if it doesn't exist.
$this->_set_key_version( 2, $group );
}
}
/**
* Retrieves cache statistics size, including the total number of items and bytes.
*
* It checks for timeouts and returns statistics based on the cache server list.
*
* @param int $timeout_time The timestamp to check if the timeout has occurred.
*
* @return array An associative array containing 'bytes', 'items', and 'timeout_occurred'.
*/
public function get_stats_size( $timeout_time ) {
$size = array(
'bytes' => 0,
'items' => 0,
'timeout_occurred' => false,
);
$key_prefix = $this->get_item_key( '' );
$error_occurred = false;
$server_list = $this->_memcache->getServerList();
$n = 0;
foreach ( $server_list as $server ) {
$loader = new Cache_Memcached_Stats( $server['host'], $server['port'] );
$slabs = $loader->slabs();
if ( ! is_array( $slabs ) ) {
$error_occurred = true;
continue;
}
foreach ( $slabs as $slab_id ) {
$cdump = $loader->cachedump( $slab_id );
if ( ! is_array( $cdump ) ) {
continue;
}
foreach ( $cdump as $line ) {
$key_data = explode( ' ', $line );
if ( ! is_array( $key_data ) || count( $key_data ) < 3 ) {
continue;
}
++$n;
if ( 0 === $n % 10 ) {
$size['timeout_occurred'] = ( time() > $timeout_time );
if ( $size['timeout_occurred'] ) {
return $size;
}
}
$key = $key_data[1];
$bytes = substr( $key_data[2], 1 );
if ( substr( $key, 0, strlen( $key_prefix ) ) === $key_prefix ) {
$size['bytes'] += $bytes;
++$size['items'];
}
}
}
}
if ( $error_occurred && $size['items'] <= 0 ) {
$size['bytes'] = null;
$size['items'] = null;
}
return $size;
}
/**
* Sets a new value for a cache item if the current value matches the specified old value.
*
* This method uses CAS (Compare and Swap) to atomically update the cache item.
*
* @param string $key The cache key to update.
* @param array $old_value {
* The current value to compare against.
*
* @type mixed $content The content to compare in the old value.
* }
* @param array $new_value The new value to set if the old value matches.
*
* @return bool True if the item was updated, false otherwise.
*/
public function set_if_maybe_equals( $key, $old_value, $new_value ) {
$storage_key = $this->get_item_key( $key );
$cas = null;
$value = @$this->_memcache->get( $storage_key, null, $cas );
if ( ! is_array( $value ) ) {
return false;
}
if ( isset( $old_value['content'] ) && $value['content'] !== $old_value['content'] ) {
return false;
}
return @$this->_memcache->cas( $cas, $storage_key, $new_value );
}
/**
* Increments a counter in the cache by the specified value.
*
* If the counter does not exist, it initializes it with 0.
*
* @param string $key The cache key for the counter.
* @param int $value The value to add to the counter.
*
* @return bool True if the counter was incremented successfully, false otherwise.
*/
public function counter_add( $key, $value ) {
if ( 0 === $value ) {
return true;
}
$storage_key = $this->get_item_key( $key );
$r = $this->_memcache->increment( $storage_key, $value, 0, 3600 );
if ( ! $r ) { // it doesnt initialize counter by itself.
$this->counter_set( $key, 0 );
}
return $r;
}
/**
* Sets a counter value in the Memcached storage.
*
* This method sets a specified value for a given key in the Memcached storage. If the key doesn't already exist, it initializes
* it with the provided value. This is typically used to set counters or similar numeric values that need to be stored persistently.
*
* @param string $key The key under which the counter is stored.
* @param int $value The value to set for the counter.
*
* @return bool True on success, false on failure.
*/
public function counter_set( $key, $value ) {
$storage_key = $this->get_item_key( $key );
return @$this->_memcache->set( $storage_key, $value );
}
/**
* Retrieves the counter value from Memcached storage.
*
* This method fetches the stored counter value for the given key. If the key doesn't exist or the value is not a valid integer,
* it returns 0. This is typically used to retrieve counters or similar numeric values stored persistently.
*
* @param string $key The key under which the counter is stored.
*
* @return int The current counter value, or 0 if the key does not exist or is invalid.
*/
public function counter_get( $key ) {
$storage_key = $this->get_item_key( $key );
$v = (int) @$this->_memcache->get( $storage_key );
return $v;
}
/**
* Generates a unique Memcached key for the given item name.
*
* This method generates a unique key for an item to be stored in Memcached. The key is constructed using various instance-specific
* properties (e.g., instance ID, host, blog ID, module) combined with the md5 hash of the item name. This ensures that keys are
* unique and can safely be used in a multi-instance or multi-module environment.
*
* @param string $name The item name to generate a key for.
*
* @return string The generated unique Memcached key.
*/
public function get_item_key( $name ) {
// memcached doesn't survive spaces in a key.
$key = sprintf( 'w3tc_%d_%s_%d_%s_%s', $this->_instance_id, $this->_host, $this->_blog_id, $this->_module, md5( $name ) );
return $key;
}
}

View File

@@ -0,0 +1,149 @@
<?php
/**
* File: Cache_Memcached_Stats.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cache_Memcached_Stats
*
* Download extended statistics since module cant do it by itself
*
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
* phpcs:disable WordPress.WP.AlternativeFunctions
*/
class Cache_Memcached_Stats {
/**
* Constructor to initialize the Memcached stats handler.
*
* @param string $host The hostname or IP address of the Memcached server.
* @param int $port The port number of the Memcached server.
*
* @return void
*/
public function __construct( $host, $port ) {
$this->host = $host;
$this->port = $port;
}
/**
* Sends a command to the Memcached server and retrieves the response.
*
* @param string $command The command to send to the Memcached server.
*
* @return array|null An array of response lines from the server, or null on failure.
*/
public function request( $command ) {
$handle = @fsockopen( $this->host, $this->port );
if ( ! $handle ) {
return null;
}
fwrite( $handle, $command . "\r\n" );
$response = array();
while ( ( ! feof( $handle ) ) ) {
$line = fgets( $handle );
$response[] = $line;
if ( $this->end( $line, $command ) ) {
break;
}
}
@fclose( $handle );
return $response;
}
/**
* Determines if the server response indicates the end of a command's execution.
*
* @param string $buffer The current line of the server's response.
* @param string $command The command that was sent to the server.
*
* @return bool True if the response indicates the end of the command, false otherwise.
*/
private function end( $buffer, $command ) {
// incr or decr also return integer.
if ( ( preg_match( '/^(incr|decr)/', $command ) ) ) {
if ( preg_match( '/^(END|ERROR|SERVER_ERROR|CLIENT_ERROR|NOT_FOUND|[0-9]*)/', $buffer ) ) {
return true;
}
} elseif ( preg_match( '/^(END|DELETED|OK|ERROR|SERVER_ERROR|CLIENT_ERROR|NOT_FOUND|STORED|RESET|TOUCHED)/', $buffer ) ) {
return true;
}
return false;
}
/**
* Parses the response lines into an array of data.
*
* @param array $lines An array of response lines from the Memcached server.
*
* @return array A parsed array where each line is split into words.
*/
public function parse( $lines ) {
$return = array();
foreach ( $lines as $line ) {
$data = explode( ' ', $line );
$return[] = $data;
}
return $return;
}
/**
* Retrieves the IDs of all active slabs on the Memcached server.
*
* A slab is a logical unit of storage in Memcached.
*
* @return array|null An array of slab IDs, or null on failure.
*/
public function slabs() {
$result = $this->request( 'stats slabs' );
if ( is_null( $result ) ) {
return null;
}
$result = $this->parse( $result );
$slabs = array();
foreach ( $result as $line_words ) {
if ( count( $line_words ) < 2 ) {
continue;
}
$key = explode( ':', $line_words[1] );
if ( (int) $key[0] > 0 ) {
$slabs[ $key[0] ] = '*';
}
}
return array_keys( $slabs );
}
/**
* Retrieves a cachedump for a specific slab ID.
*
* A cachedump returns the cached items for the specified slab.
*
* @param int $slab_id The ID of the slab to retrieve the cachedump for.
*
* @return array|null An array of cachedump data, or null on failure.
*/
public function cachedump( $slab_id ) {
$result = $this->request( 'stats cachedump ' . $slab_id . ' 0' );
if ( is_null( $result ) ) {
return null;
}
// return pure data to limit memory usage.
return $result;
}
}

View File

@@ -0,0 +1,427 @@
<?php
/**
* FIle: Cache_Nginx_Memcached.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cache_Nginx_Memcached
*
* PECL Memcached class
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
* phpcs:disable Universal.CodeAnalysis.ConstructorDestructorReturn.ReturnValueFound
*/
class Cache_Nginx_Memcached extends Cache_Base {
/**
* Memcache object
*
* @var Memcache
*/
private $_memcache = null;
/**
* Configuration used to reinitialize persistent object
*
* @var Config
*/
private $_config = null;
/**
* Constructor for initializing the Memcached client.
*
* This constructor initializes the Memcached client with the specified configuration. It checks if persistent connections are
* enabled, and if so, attempts to reconnect to existing Memcached servers. If no servers are available, it calls the
* initialization method. If persistence is not enabled, it initializes a non-persistent Memcached connection.
*
* @param array $config Configuration array containing settings for Memcached.
*
* @return bool True if the Memcached connection was initialized successfully, false otherwise.
*/
public function __construct( $config ) {
parent::__construct( $config );
if ( isset( $config['persistent'] ) && $config['persistent'] ) {
$this->_config = $config;
$this->_memcache = new \Memcached( $this->_get_key_version_key( '' ) );
$server_list = $this->_memcache->getServerList();
if ( empty( $server_list ) ) {
return $this->initialize( $config );
} else {
return true;
}
} else {
$this->_memcache = new \Memcached();
return $this->initialize( $config );
}
}
/**
* Initializes the Memcached connection and configures the servers.
*
* This method is responsible for configuring the Memcached instance with options like compression, server list, and
* authentication. It adds each server from the configuration and handles optional features like AWS autodiscovery and
* SASL authentication if configured.
*
* @param array $config Configuration array containing Memcached settings, including server details and authentication.
*
* @return bool True if initialization is successful, false if no servers are configured.
*/
private function initialize( $config ) {
if ( empty( $config['servers'] ) ) {
return false;
}
if ( defined( '\Memcached::OPT_REMOVE_FAILED_SERVERS' ) ) {
$this->_memcache->setOption( \Memcached::OPT_REMOVE_FAILED_SERVERS, true );
}
$this->_memcache->setOption( \Memcached::OPT_COMPRESSION, false );
if (
isset( $config['aws_autodiscovery'] ) &&
$config['aws_autodiscovery'] &&
defined( '\Memcached::OPT_CLIENT_MODE' ) &&
defined( '\Memcached::DYNAMIC_CLIENT_MODE' )
) {
$this->_memcache->setOption( \Memcached::OPT_CLIENT_MODE, \Memcached::DYNAMIC_CLIENT_MODE );
}
foreach ( (array) $config['servers'] as $server ) {
list( $ip, $port ) = Util_Content::endpoint_to_host_port( $server );
$this->_memcache->addServer( $ip, $port );
}
if ( isset( $config['username'] ) && ! empty( $config['username'] ) && method_exists( $this->_memcache, 'setSaslAuthData' ) ) {
$this->_memcache->setSaslAuthData( $config['username'], $config['password'] );
}
return true;
}
/**
* Adds an item to Memcached with a given key.
*
* This method adds a new item to Memcached. It first calls the `set` method to store the item. This is typically used for
* storing objects or arrays in Memcached.
*
* @param string $key The key under which the item is stored.
* @param mixed $value The variable to store in Memcached.
* @param int $expire The expiration time for the item in seconds. Default is 0 (no expiration).
* @param string $group An optional group to categorize the item.
*
* @return bool True on success, false on failure.
*/
public function add( $key, &$value, $expire = 0, $group = '' ) {
return $this->set( $key, $value, $expire, $group );
}
/**
* Sets an item in Memcached.
*
* This method stores an item in Memcached under the specified key. The item will be serialized and stored, and an expiration
* time can be set.
*
* @param string $key The key under which the item is stored.
* @param mixed $value The variable to store in Memcached.
* @param int $expire The expiration time in seconds. Default is 0 (no expiration).
* @param string $group An optional group to categorize the item.
*
* @return bool True on success, false on failure.
*/
public function set( $key, $value, $expire = 0, $group = '' ) {
$this->_memcache->setOption( \Memcached::OPT_USER_FLAGS, ( isset( $value['c'] ) ? 1 : 0 ) );
return @$this->_memcache->set( $key, $value['content'], $expire );
}
/**
* Retrieves an item from Memcached with its old data.
*
* This method attempts to retrieve an item from Memcached. If the item exists, it returns the content along with an indicator
* of whether compression was applied based on the key suffix.
*
* @param string $key The key of the item to retrieve.
* @param string $group The group associated with the item.
*
* @return array|null The content of the item along with compression info, or null if not found.
*/
public function get_with_old( $key, $group = '' ) {
$has_old_data = false;
$v = @$this->_memcache->get( $key );
if ( false === $v ) {
return null;
}
$data = array( 'content' => $v );
$data['compression'] = ( ' _gzip' === substr( $key, -5 ) ? 'gzip' : '' );
return array( $data, false );
}
/**
* Replaces an existing item in Memcached.
*
* This method replaces an existing item in Memcached. It calls the `set` method to store the new value under the same key.
* If the key doesn't exist, it behaves like a regular set.
*
* @param string $key The key under which the item is stored.
* @param mixed $value The variable to store in Memcached.
* @param int $expire The expiration time in seconds. Default is 0 (no expiration).
* @param string $group An optional group to categorize the item.
*
* @return bool True on success, false on failure.
*/
public function replace( $key, &$value, $expire = 0, $group = '' ) {
return $this->set( $key, $value, $expire, $group );
}
/**
* Deletes an item from Memcached.
*
* This method deletes an item from Memcached by its key. If the key doesn't exist, it silently does nothing.
*
* @param string $key The key of the item to delete.
* @param string $group The group associated with the item.
*
* @return bool True on success, false on failure.
*/
public function delete( $key, $group = '' ) {
return @$this->_memcache->delete( $key );
}
/**
* Hard deletes an item from Memcached.
*
* This method forces the deletion of an item from Memcached. It is similar to the regular `delete` method but emphasizes
* immediate removal.
*
* @param string $key The key of the item to delete.
* @param string $group The group associated with the item.
*
* @return bool True on success, false on failure.
*/
public function hard_delete( $key, $group = '' ) {
return @$this->_memcache->delete( $key );
}
/**
* Flushes all items from Memcached.
*
* This method clears all the stored items from Memcached. It has no way to flush individual caches.
*
* @param string $group An optional group to categorize the items.
*
* @return bool True on success, false on failure.
*/
public function flush( $group = '' ) {
// can only flush everything from memcached, no way to flush only pgcache cache.
return @$this->_memcache->flush();
}
/**
* Checks if Memcached is available.
*
* This method checks if the Memcached class exists and is available for use.
*
* @return bool True if Memcached is available, false otherwise.
*/
public function available() {
return class_exists( 'Memcached' );
}
/**
* Retrieves statistics from Memcached.
*
* This method returns statistics from the Memcached server. If multiple servers are available, it returns stats from the first server.
*
* @return array The statistics from Memcached, or an empty array if no stats are available.
*/
public function get_statistics() {
$a = $this->_memcache->getStats();
if ( count( $a ) > 0 ) {
$keys = array_keys( $a );
$key = $keys[0];
return $a[ $key ];
}
return $a;
}
/**
* Retrieves cache size statistics from Memcached.
*
* This method collects statistics on the size of cached data on the Memcached server, including the total bytes and items.
*
* @param int $timeout_time The timeout time for statistics retrieval.
*
* @return array An array containing the size of cached data, number of items, and whether a timeout occurred.
*/
public function get_stats_size( $timeout_time ) {
$size = array(
'bytes' => 0,
'items' => 0,
'timeout_occurred' => false,
);
$key_prefix = $this->get_item_key( '' );
$error_occurred = false;
$server_list = $this->_memcache->getServerList();
$n = 0;
foreach ( $server_list as $server ) {
$loader = new Cache_Memcached_Stats( $server['host'], $server['port'] );
$slabs = $loader->slabs();
if ( ! is_array( $slabs ) ) {
$error_occurred = true;
continue;
}
foreach ( $slabs as $slab_id ) {
$cdump = $loader->cachedump( $slab_id );
if ( ! is_array( $cdump ) ) {
continue;
}
foreach ( $cdump as $line ) {
$key_data = explode( ' ', $line );
if ( ! is_array( $key_data ) || count( $key_data ) < 3 ) {
continue;
}
++$n;
if ( 0 === $n % 10 ) {
$size['timeout_occurred'] = ( time() > $timeout_time );
if ( $size['timeout_occurred'] ) {
return $size;
}
}
$key = $key_data[1];
$bytes = substr( $key_data[2], 1 );
if ( substr( $key, 0, strlen( $key_prefix ) ) === $key_prefix ) {
$size['bytes'] += $bytes;
++$size['items'];
}
}
}
}
if ( $error_occurred && $size['items'] <= 0 ) {
$size['bytes'] = null;
$size['items'] = null;
}
return $size;
}
/**
* Sets a new value for a given key in Memcached if it matches the old value.
*
* This method attempts to update a value in Memcached only if the current value for the given key is equal to the provided
* old value. It uses CAS (Check And Set) to ensure that the value is updated atomically and only when the existing value
* has not changed.
*
* @param string $key The key of the item to update.
* @param array $old_value The old value to compare against the current value in cache.
* @param mixed $new_value The new value to set if the old value matches the current value.
*
* @return bool True if the value was successfully set, false otherwise.
*/
public function set_if_maybe_equals( $key, $old_value, $new_value ) {
$storage_key = $this->get_item_key( $key );
$cas = null;
$value = @$this->_memcache->get( $storage_key, null, $cas );
if ( ! is_array( $value ) ) {
return false;
}
if ( isset( $old_value['content'] ) && $value['content'] !== $old_value['content'] ) {
return false;
}
return @$this->_memcache->cas( $cas, $storage_key, $new_value );
}
/**
* Increments the counter for a given key in Memcached by a specified value.
*
* This method increments the value of a counter stored in Memcached. If the key does not exist or is not a number, the counter
* is initialized to 0 and then incremented.
*
* @param string $key The key of the counter to increment.
* @param int $value The amount by which to increment the counter.
*
* @return bool True if the increment was successful, false otherwise.
*/
public function counter_add( $key, $value ) {
if ( 0 === $value ) {
return true;
}
$storage_key = $this->get_item_key( $key );
$r = @$this->_memcache->increment( $storage_key, $value );
if ( ! $r ) { // it doesnt initialize counter by itself.
$this->counter_set( $key, 0 );
}
return $r;
}
/**
* Sets the counter value for a given key in Memcached.
*
* This method sets the value of a counter in Memcached. If the key does not exist, it will create a new entry with the provided value.
*
* @param string $key The key of the counter to set.
* @param int $value The value to set the counter to.
*
* @return bool True if the value was successfully set, false otherwise.
*/
public function counter_set( $key, $value ) {
$storage_key = $this->get_item_key( $key );
return @$this->_memcache->set( $storage_key, $value );
}
/**
* Retrieves the current value of a counter stored in Memcached.
*
* This method retrieves the value of a counter stored in Memcached. If the counter does not exist, it returns 0.
*
* @param string $key The key of the counter to retrieve.
*
* @return int The current value of the counter.
*/
public function counter_get( $key ) {
$storage_key = $this->get_item_key( $key );
$v = (int) @$this->_memcache->get( $storage_key );
return $v;
}
/**
* Generates a unique storage key for a given name.
*
* This method generates a unique key for an item in Memcached by including the instance ID, host, blog ID, module, and a
* hashed version of the provided name. Memcached keys cannot contain spaces, so this method ensures the key format is valid
* for Memcached.
*
* @param string $name The name for which to generate a unique key.
*
* @return string The generated storage key.
*/
public function get_item_key( $name ) {
// memcached doesn't survive spaces in a key.
$key = sprintf( 'w3tc_%d_%s_%d_%s_%s', $this->_instance_id, $this->_host, $this->_blog_id, $this->_module, md5( $name ) );
return $key;
}
}

View File

@@ -0,0 +1,578 @@
<?php
/**
* File: Cache_Redis.php
*
* @package W3TC
*
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore,PSR2.Classes.PropertyDeclaration.Underscore,WordPress.PHP.DiscouragedPHPFunctions,WordPress.PHP.NoSilencedErrors
*/
namespace W3TC;
/**
* Redis cache engine.
*/
class Cache_Redis extends Cache_Base {
/**
* Accessors.
*
* @var array
*/
private $_accessors = array();
/**
* Key value.
*
* @var array
*/
private $_key_version = array();
/**
* Persistent.
*
* @var bool
*/
private $_persistent;
/**
* Password.
*
* @var string
*/
private $_password;
/**
* Servers.
*
* @var array
*/
private $_servers;
/**
* Verify TLS certificate.
*
* @var bool
*/
private $_verify_tls_certificates;
/**
* DB id.
*
* @var string
*/
private $_dbid;
/**
* Timeout.
*
* @var int.
*/
private $_timeout;
/**
* Retry interval.
*
* @var int
*/
private $_retry_interval;
/**
* Retry timeout.
*
* @var int
*/
private $_read_timeout;
/**
* Constructor.
*
* @param array $config Config.
*/
public function __construct( $config ) {
parent::__construct( $config );
$this->_persistent = ( isset( $config['persistent'] ) && $config['persistent'] );
$this->_servers = (array) $config['servers'];
$this->_verify_tls_certificates = ( isset( $config['verify_tls_certificates'] ) && $config['verify_tls_certificates'] );
$this->_password = $config['password'];
$this->_dbid = $config['dbid'];
$this->_timeout = $config['timeout'] ?? 3600000;
$this->_retry_interval = $config['retry_interval'] ?? 3600000;
$this->_read_timeout = $config['read_timeout'] ?? 60.0;
/**
* When disabled - no extra requests are made to obtain key version,
* but flush operations not supported as a result group should be always empty.
*/
if ( isset( $config['key_version_mode'] ) && 'disabled' === $config['key_version_mode'] ) {
$this->_key_version[''] = 1;
}
}
/**
* Adds data.
*
* @param string $key Key.
* @param mixed $value Var.
* @param integer $expire Expire.
* @param string $group Used to differentiate between groups of cache values.
* @return bool
*/
public function add( $key, &$value, $expire = 0, $group = '' ) {
return $this->set( $key, $value, $expire, $group );
}
/**
* Sets data.
*
* @param string $key Key.
* @param mixed $value Value.
* @param integer $expire Expire.
* @param string $group Used to differentiate between groups of cache values.
* @return bool
*/
public function set( $key, $value, $expire = 0, $group = '' ) {
if ( ! isset( $value['key_version'] ) ) {
$value['key_version'] = $this->_get_key_version( $group );
}
$storage_key = $this->get_item_key( $key );
$accessor = $this->_get_accessor( $storage_key );
if ( is_null( $accessor ) ) {
return false;
}
if ( ! $expire ) {
return $accessor->set( $storage_key, serialize( $value ) );
}
return $accessor->setex( $storage_key, $expire, serialize( $value ) );
}
/**
* Returns data
*
* @param string $key Key.
* @param string $group Used to differentiate between groups of cache values.
* @return mixed
*/
public function get_with_old( $key, $group = '' ) {
$has_old_data = false;
$storage_key = $this->get_item_key( $key );
$accessor = $this->_get_accessor( $storage_key );
if ( is_null( $accessor ) ) {
return array( null, false );
}
$v = $accessor->get( $storage_key );
$v = @unserialize( $v );
if ( ! is_array( $v ) || ! isset( $v['key_version'] ) ) {
return array( null, $has_old_data );
}
$key_version = $this->_get_key_version( $group );
if ( $v['key_version'] === $key_version ) {
return array( $v, $has_old_data );
}
if ( $v['key_version'] > $key_version ) {
if ( ! empty( $v['key_version_at_creation'] ) && $v['key_version_at_creation'] !== $key_version ) {
$this->_set_key_version( $v['key_version'], $group );
}
return array( $v, $has_old_data );
}
// Key version is old.
if ( ! $this->_use_expired_data ) {
return array( null, $has_old_data );
}
// If we have expired data - update it for future use and let current process recalculate it.
$expires_at = isset( $v['expires_at'] ) ? $v['expires_at'] : null;
if ( is_null( $expires_at ) || time() > $expires_at ) {
$v['expires_at'] = time() + 30;
$accessor->setex( $storage_key, 60, serialize( $v ) );
$has_old_data = true;
return array( null, $has_old_data );
}
// Return old version.
return array( $v, $has_old_data );
}
/**
* Replaces data.
*
* @param string $key Key.
* @param mixed $value Value.
* @param integer $expire Expire.
* @param string $group Used to differentiate between groups of cache values.
* @return bool
*/
public function replace( $key, &$value, $expire = 0, $group = '' ) {
return $this->set( $key, $value, $expire, $group );
}
/**
* Deletes data.
*
* @param string $key Key.
* @param string $group Group.
* @return bool
*/
public function delete( $key, $group = '' ) {
$storage_key = $this->get_item_key( $key );
$accessor = $this->_get_accessor( $storage_key );
if ( is_null( $accessor ) ) {
return false;
}
if ( $this->_use_expired_data ) {
$v = $accessor->get( $storage_key );
$ttl = $accessor->ttl( $storage_key );
if ( is_array( $v ) ) {
$v['key_version'] = 0;
$accessor->setex( $storage_key, $ttl, $v );
return true;
}
}
return $accessor->setex( $storage_key, 1, '' );
}
/**
* Key to delete, deletes _old and primary if exists.
*
* @param string $key Key.
* @param string $group Group.
* @return bool
*/
public function hard_delete( $key, $group = '' ) {
$storage_key = $this->get_item_key( $key );
$accessor = $this->_get_accessor( $storage_key );
if ( is_null( $accessor ) ) {
return false;
}
return $accessor->setex( $storage_key, 1, '' );
}
/**
* Flushes all data.
*
* @param string $group Used to differentiate between groups of cache values.
* @return bool
*/
public function flush( $group = '' ) {
$this->_get_key_version( $group ); // Initialize $this->_key_version.
if ( isset( $this->_key_version[ $group ] ) ) {
++$this->_key_version[ $group ];
$this->_set_key_version( $this->_key_version[ $group ], $group );
}
return true;
}
/**
* Gets a key extension for "ahead generation" mode.
* Used by AlwaysCached functionality to regenerate content
*
* @param string $group Used to differentiate between groups of cache values.
*
* @return array
*/
public function get_ahead_generation_extension( $group ) {
$v = $this->_get_key_version( $group );
return array(
'key_version' => $v + 1,
'key_version_at_creation' => $v,
);
}
/**
* Flushes group with before condition
*
* @param string $group Used to differentiate between groups of cache values.
* @param array $extension Used to set a condition what version to flush.
*
* @return void
*/
public function flush_group_after_ahead_generation( $group, $extension ) {
$v = $this->_get_key_version( $group );
if ( $extension['key_version'] > $v ) {
$this->_set_key_version( $extension['key_version'], $group );
}
}
/**
* Checks if engine can function properly in this environment.
*
* @return bool
*/
public function available() {
return class_exists( 'Redis' );
}
/**
* Get statistics.
*
* @return array
*/
public function get_statistics() {
$accessor = $this->_get_accessor( '' ); // Single-server mode used for stats.
if ( is_null( $accessor ) ) {
return array();
}
$a = $accessor->info();
return $a;
}
/**
* Returns key version.
*
* @param string $group Used to differentiate between groups of cache values.
* @return int
*/
private function _get_key_version( $group = '' ) {
if ( ! isset( $this->_key_version[ $group ] ) || $this->_key_version[ $group ] <= 0 ) {
$storage_key = $this->_get_key_version_key( $group );
$accessor = $this->_get_accessor( $storage_key );
if ( is_null( $accessor ) ) {
return 0;
}
$v_original = $accessor->get( $storage_key );
$v = intval( $v_original );
$v = ( $v > 0 ? $v : 1 );
if ( (string) $v_original !== (string) $v ) {
$accessor->set( $storage_key, $v );
}
$this->_key_version[ $group ] = $v;
}
return $this->_key_version[ $group ];
}
/**
* Sets new key version.
*
* @param string $v Version.
* @param string $group Used to differentiate between groups of cache values.
* @return bool
*/
private function _set_key_version( $v, $group = '' ) {
$storage_key = $this->_get_key_version_key( $group );
$accessor = $this->_get_accessor( $storage_key );
if ( is_null( $accessor ) ) {
return false;
}
$accessor->set( $storage_key, $v );
return true;
}
/**
* Used to replace as atomically as possible known value to new one.
*
* @param string $key Key.
* @param string $old_value Old value.
* @param string $new_value New value.
*/
public function set_if_maybe_equals( $key, $old_value, $new_value ) {
$storage_key = $this->get_item_key( $key );
$accessor = $this->_get_accessor( $storage_key );
if ( is_null( $accessor ) ) {
return false;
}
$accessor->watch( $storage_key );
$value = $accessor->get( $storage_key );
$value = @unserialize( $value );
if ( ! is_array( $value ) ) {
$accessor->unwatch();
return false;
}
if ( isset( $old_value['content'] ) && $value['content'] !== $old_value['content'] ) {
$accessor->unwatch();
return false;
}
return $accessor->multi()
->set( $storage_key, $new_value )
->exec();
}
/**
* Use key as a counter and add integet value to it.
*
* @param string $key Key.
* @param int $value Value.
*/
public function counter_add( $key, $value ) {
if ( empty( $value ) ) {
return true;
}
$storage_key = $this->get_item_key( $key );
$accessor = $this->_get_accessor( $storage_key );
if ( is_null( $accessor ) ) {
return false;
}
$r = $accessor->incrBy( $storage_key, (int) $value );
if ( ! $r ) { // It doesn't initialize counter by itself.
$this->counter_set( $key, 0 );
}
return $r;
}
/**
* Use key as a counter and add integet value to it.
*
* @param string $key Key.
* @param int $value Value.
*/
public function counter_set( $key, $value ) {
$storage_key = $this->get_item_key( $key );
$accessor = $this->_get_accessor( $storage_key );
if ( is_null( $accessor ) ) {
return false;
}
return $accessor->set( $storage_key, $value );
}
/**
* Get counter's value.
*
* @param string $key Key.
*/
public function counter_get( $key ) {
$storage_key = $this->get_item_key( $key );
$accessor = $this->_get_accessor( $storage_key );
if ( is_null( $accessor ) ) {
return 0;
}
$v = (int) $accessor->get( $storage_key );
return $v;
}
/**
* Build Redis connection arguments based on server URI
*
* @param string $server Server URI to connect to.
*/
private function build_connect_args( $server ) {
$connect_args = array();
if ( substr( $server, 0, 5 ) === 'unix:' ) {
$connect_args[] = trim( substr( $server, 5 ) );
$connect_args[] = 0; // Port (int). For no port, use integer 0.
} else {
list( $ip, $port ) = Util_Content::endpoint_to_host_port( $server, 0 ); // Port (int). For no port, use integer 0.
$connect_args[] = $ip;
$connect_args[] = $port;
}
$connect_args[] = $this->_timeout;
$connect_args[] = $this->_persistent ? $this->_instance_id . '_' . $this->_dbid : null;
$connect_args[] = $this->_retry_interval;
$phpredis_version = phpversion( 'redis' );
// The read_timeout parameter was added in phpredis 3.1.3.
if ( version_compare( $phpredis_version, '3.1.3', '>=' ) ) {
$connect_args[] = $this->_read_timeout;
}
// Support for stream context was added in phpredis 5.3.2.
if ( version_compare( $phpredis_version, '5.3.2', '>=' ) ) {
$context = array();
if ( 'tls:' === substr( $server, 0, 4 ) && ! $this->_verify_tls_certificates ) {
$context['stream'] = array(
'verify_peer' => false,
'verify_peer_name' => false,
);
}
$connect_args[] = $context;
}
return $connect_args;
}
/**
* Get accessor.
*
* @param string $key Key.
* @return object
*/
private function _get_accessor( $key ) {
if ( count( $this->_servers ) <= 1 ) {
$index = 0;
} else {
$index = crc32( $key ) % count( $this->_servers );
}
if ( isset( $this->_accessors[ $index ] ) ) {
return $this->_accessors[ $index ];
}
if ( ! isset( $this->_servers[ $index ] ) ) {
$this->_accessors[ $index ] = null;
} else {
try {
$server = $this->_servers[ $index ];
$connect_args = $this->build_connect_args( $server );
$accessor = new \Redis();
if ( $this->_persistent ) {
$accessor->pconnect( ...$connect_args );
} else {
$accessor->connect( ...$connect_args );
}
if ( ! empty( $this->_password ) ) {
$accessor->auth( $this->_password );
}
$accessor->select( $this->_dbid );
} catch ( \Exception $e ) {
error_log( __METHOD__ . ': ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
$accessor = null;
}
$this->_accessors[ $index ] = $accessor;
}
return $this->_accessors[ $index ];
}
}

View File

@@ -0,0 +1,370 @@
<?php
/**
* File: Cache_Wincache.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cache_Wincache
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
* phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
* phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
*/
class Cache_Wincache extends Cache_Base {
/**
* Used for faster flushing
*
* @var integer $_key_version
*/
private $_key_version = array();
/**
* Adds a value to the cache.
*
* This method is an alias of the `set()` method and allows adding data to the cache with an optional expiration
* time and group. It is used for storing data that is not already present in the cache.
*
* @param string $key The unique key to identify the cached item.
* @param mixed $value The value to be stored in the cache, passed by reference.
* @param int $expire The expiration time in seconds. Default is 0 (no expiration).
* @param string $group The group to which the cache item belongs. Default is an empty string (no group).
*
* @return bool True on success, false on failure.
*/
public function add( $key, &$value, $expire = 0, $group = '' ) {
return $this->set( $key, $value, $expire, $group );
}
/**
* Sets a value in the cache.
*
* This method stores a value in the cache for a given key, and optionally allows setting an expiration time
* and group. The value is serialized before storing to ensure it can handle complex data types.
*
* @param string $key The unique key to identify the cached item.
* @param mixed $value The value to be stored in the cache.
* @param int $expire The expiration time in seconds. Default is 0 (no expiration).
* @param string $group The group to which the cache item belongs. Default is an empty string (no group).
*
* @return bool True on success, false on failure.
*/
public function set( $key, $value, $expire = 0, $group = '' ) {
if ( ! isset( $value['key_version'] ) ) {
$value['key_version'] = $this->_get_key_version( $group );
}
$storage_key = $this->get_item_key( $key );
return wincache_ucache_set( $storage_key, serialize( $value ), $expire );
}
/**
* Retrieves a cached value with version checking.
*
* This method retrieves a cached value for a given key, checking the version of the cached data. If the stored
* data's version is different from the current version, it may return old data or null depending on settings.
*
* @param string $key The unique key to identify the cached item.
* @param string $group The group to which the cache item belongs. Default is an empty string (no group).
*
* @return array The cached value and a flag indicating if old data was returned.
*/
public function get_with_old( $key, $group = '' ) {
$has_old_data = false;
$storage_key = $this->get_item_key( $key );
$v = @unserialize( wincache_ucache_get( $storage_key ) );
if ( ! is_array( $v ) || ! isset( $v['key_version'] ) ) {
return array( null, $has_old_data );
}
$key_version = $this->_get_key_version( $group );
if ( $v['key_version'] === $key_version ) {
return array( $v, $has_old_data );
}
if ( $v['key_version'] > $key_version ) {
if ( ! empty( $v['key_version_at_creation'] ) && $v['key_version_at_creation'] !== $key_version ) {
$this->_set_key_version( $v['key_version'], $group );
}
return array( $v, $has_old_data );
}
// key version is old.
if ( ! $this->_use_expired_data ) {
return array( null, $has_old_data );
}
// if we have expired data - update it for future use and let current process recalculate it.
$expires_at = isset( $v['expires_at'] ) ? $v['expires_at'] : null;
if ( null === $expires_at || time() > $expires_at ) {
$v['expires_at'] = time() + 30;
wincache_ucache_set( $storage_key, serialize( $v ), 0 );
$has_old_data = true;
return array( null, $has_old_data );
}
// return old version.
return array( $v, $has_old_data );
}
/**
* Replaces a cached value only if it already exists.
*
* This method updates the cache with a new value for the given key, but only if the key already exists in the cache.
* If the key does not exist, it returns false.
*
* @param string $key The unique key to identify the cached item.
* @param mixed $value The new value to be stored in the cache, passed by reference.
* @param int $expire The expiration time in seconds. Default is 0 (no expiration).
* @param string $group The group to which the cache item belongs. Default is an empty string (no group).
*
* @return bool True on success, false on failure.
*/
public function replace( $key, &$value, $expire = 0, $group = '' ) {
if ( $this->get( $key, $group ) !== false ) {
return $this->set( $key, $value, $expire, $group );
}
return false;
}
/**
* Deletes a cached value.
*
* This method deletes a cached item for a given key, optionally checking the group. If expired data is allowed,
* it may mark the data as invalid instead of deleting it immediately.
*
* @param string $key The unique key to identify the cached item.
* @param string $group The group to which the cache item belongs. Default is an empty string (no group).
*
* @return bool True on success, false on failure.
*/
public function delete( $key, $group = '' ) {
$storage_key = $this->get_item_key( $key );
if ( $this->_use_expired_data ) {
$v = @unserialize( wincache_ucache_get( $storage_key ) );
if ( is_array( $v ) ) {
$v['key_version'] = 0;
wincache_ucache_set( $storage_key, serialize( $v ), 0 );
return true;
}
}
return wincache_ucache_delete( $storage_key );
}
/**
* Forcefully deletes a cached value without considering expired data.
*
* This method immediately deletes the cached item for a given key, disregarding any expired data settings.
*
* @param string $key The unique key to identify the cached item.
* @param string $group The group to which the cache item belongs. Default is an empty string (no group).
*
* @return bool True on success, false on failure.
*/
public function hard_delete( $key, $group = '' ) {
$storage_key = $this->get_item_key( $key );
return wincache_ucache_delete( $storage_key );
}
/**
* Flushes the entire cache for a given group.
*
* This method increments the key version for the specified group and clears the associated cache data,
* effectively resetting the cache for that group.
*
* @param string $group The group to which the cache belongs. Default is an empty string (no group).
*
* @return bool True on success, false on failure.
*/
public function flush( $group = '' ) {
$this->_get_key_version( $group ); // initialize $this->_key_version.
++$this->_key_version[ $group ];
$this->_set_key_version( $this->_key_version[ $group ], $group );
return true;
}
/**
* Generates the next key version and return the extension data.
*
* This method generates a new key version for a given group and returns an array containing both the new version
* and the version at creation. This is used to track versioning and support cache invalidation.
*
* @param string $group The group to which the cache belongs.
*
* @return array An array containing the next key version and the version at creation.
*/
public function get_ahead_generation_extension( $group ) {
$v = $this->_get_key_version( $group );
return array(
'key_version' => $v + 1,
'key_version_at_creation' => $v,
);
}
/**
* Flushes the cache group after ahead generation.
*
* This method flushes the cache for a given group if the generated extension has a higher key version. It ensures
* that the cache version is updated appropriately to avoid serving outdated data.
*
* @param string $group The group to which the cache belongs.
* @param array $extension {
* The extension data containing the new key version.
*
* @type string $key_version The new key version.
* }
*
* @return void
*/
public function flush_group_after_ahead_generation( $group, $extension ) {
$v = $this->_get_key_version( $group );
if ( $extension['key_version'] > $v ) {
$this->_set_key_version( $extension['key_version'], $group );
}
}
/**
* Checks if WinCache is available for use.
*
* This method checks whether the necessary WinCache functions are available. It is used to determine if
* WinCache is enabled and can be utilized for caching operations.
*
* @return bool True if WinCache is available, false otherwise.
*/
public function available() {
return function_exists( 'wincache_ucache_set' );
}
/**
* Retrieves the current version of a key for a given group.
*
* This method gets the current version of the cache key for a specified group. If the key does not exist, it
* initializes the version for the group.
*
* @param string $group The group to which the cache belongs.
*
* @return int The current version of the cache key.
*/
private function _get_key_version( $group = '' ) {
if ( ! isset( $this->_key_version[ $group ] ) || $this->_key_version[ $group ] <= 0 ) {
$v = wincache_ucache_get( $this->_get_key_version_key( $group ) );
$v = intval( $v );
$this->_key_version[ $group ] = ( $v > 0 ? $v : 1 );
}
return $this->_key_version[ $group ];
}
/**
* Sets the version of the cache key for a specified group.
*
* This method sets the version of the cache key for a specified group. It is used when updating the version
* of a cache key to reflect changes in the cache.
*
* @param int $v The new version of the cache key.
* @param string $group The group to which the cache belongs.
*
* @return void
*/
private function _set_key_version( $v, $group ) {
wincache_ucache_set( $this->_get_key_version_key( $group ), $v, 0 );
}
/**
* Sets a value in the cache if it matches the given old value.
*
* This method checks if the cached value for a given key matches the provided `old_value`. If the `content` of
* the cached value does not match the `old_value['content']`, it returns false. Otherwise, it updates the cache
* with the new value.
*
* Note: This method does not guarantee atomicity as file locks may fail.
*
* @param string $key The unique key to identify the cached item.
* @param array $old_value {
* The old value to compare against, should include a 'content' key.
*
* @type mixed $content The content to compare.
* }
* @param mixed $new_value The new value to be stored in the cache.
*
* @return bool True on success, false on failure (e.g., if the old value does not match).
*/
public function set_if_maybe_equals( $key, $old_value, $new_value ) {
// cant guarantee atomic action here, filelocks fail often.
$value = $this->get( $key );
if ( isset( $old_value['content'] ) && $value['content'] !== $old_value['content'] ) {
return false;
}
return $this->set( $key, $new_value );
}
/**
* Increments a cached counter by a specified value.
*
* This method increments the counter stored in the cache for a given key by the provided value. If the key does
* not exist or the counter is not initialized, it will initialize the counter with a value of 0.
*
* @param string $key The unique key to identify the cached counter.
* @param int $value The value by which to increment the counter.
*
* @return bool True on success, false if the counter could not be incremented or initialized.
*/
public function counter_add( $key, $value ) {
if ( 0 === $value ) {
return true;
}
$storage_key = $this->get_item_key( $key );
$r = wincache_ucache_inc( $storage_key, $value );
if ( ! $r ) { // it doesnt initialize counter by itself.
$this->counter_set( $key, 0 );
}
return $r;
}
/**
* Sets a cached counter to a specified value.
*
* This method sets the counter for a given key in the cache to the specified value.
*
* @param string $key The unique key to identify the cached counter.
* @param int $value The value to set the counter to.
*
* @return bool True on success, false on failure.
*/
public function counter_set( $key, $value ) {
$storage_key = $this->get_item_key( $key );
return wincache_ucache_set( $storage_key, $value );
}
/**
* Retrieves the current value of a cached counter.
*
* This method retrieves the cached value of a counter for a given key. If the counter does not exist, it returns 0.
*
* @param string $key The unique key to identify the cached counter.
*
* @return int The current value of the counter.
*/
public function counter_get( $key ) {
$storage_key = $this->get_item_key( $key );
$v = (int) wincache_ucache_get( $storage_key );
return $v;
}
}

View File

@@ -0,0 +1,359 @@
<?php
/**
* File: Cache_Xcache.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cache_Xcache
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
* phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
*/
class Cache_Xcache extends Cache_Base {
/**
* Used for faster flushing
*
* @var integer $_key_version
*/
private $_key_version = array();
/**
* Adds a new item to the cache if it does not already exist.
*
* If the item does not exist in the cache, it is added with the specified expiration and group. If it already exists,
* the method returns false.
*
* @param string $key The unique key to identify the cached item.
* @param mixed $value The value to store in the cache (passed by reference).
* @param int $expire The expiration time in seconds. Defaults to 0 (no expiration).
* @param string $group The group to which the key belongs. Defaults to an empty string.
*
* @return bool True if the item was added successfully, false if the item already exists.
*/
public function add( $key, &$value, $expire = 0, $group = '' ) {
if ( false === $this->get( $key, $group ) ) {
return $this->set( $key, $value, $expire, $group );
}
return false;
}
/**
* Sets a value in the cache, overwriting any existing value.
*
* This method sets a value in the cache for a given key, with an optional expiration time and group. If the value
* does not have a `key_version`, it is assigned the current group key version.
*
* @param string $key The unique key to identify the cached item.
* @param mixed $value The value to store in the cache.
* @param int $expire The expiration time in seconds. Defaults to 0 (no expiration).
* @param string $group The group to which the key belongs. Defaults to an empty string.
*
* @return bool True if the value was successfully stored, false otherwise.
*/
public function set( $key, $value, $expire = 0, $group = '' ) {
if ( ! isset( $value['key_version'] ) ) {
$value['key_version'] = $this->_get_key_version( $group );
}
$storage_key = $this->get_item_key( $key );
return xcache_set( $storage_key, serialize( $value ), $expire );
}
/**
* Retrieves a cached item along with its version status.
*
* This method retrieves the cached value for a given key, checking if the key version matches the current group key
* version. It also determines whether the cached value is expired and returns a flag indicating if old data is used.
*
* @param string $key The unique key to identify the cached item.
* @param string $group The group to which the key belongs. Defaults to an empty string.
*
* @return array An array containing the cached value (or null if not found) and a boolean indicating old data usage.
*/
public function get_with_old( $key, $group = '' ) {
$has_old_data = false;
$storage_key = $this->get_item_key( $key );
$v = @unserialize( xcache_get( $storage_key ) );
if ( ! is_array( $v ) || ! isset( $v['key_version'] ) ) {
return array( null, $has_old_data );
}
$key_version = $this->_get_key_version( $group );
if ( $v['key_version'] === $key_version ) {
return array( $v, $has_old_data );
}
if ( $v['key_version'] > $key_version ) {
if ( ! empty( $v['key_version_at_creation'] ) && $v['key_version_at_creation'] !== $key_version ) {
$this->_set_key_version( $v['key_version'], $group );
}
return array( $v, $has_old_data );
}
// key version is old.
if ( ! $this->_use_expired_data ) {
return array( null, $has_old_data );
}
// if we have expired data - update it for future use and let current process recalculate it.
$expires_at = isset( $v['expires_at'] ) ? $v['expires_at'] : null;
if ( null === $expires_at || time() > $expires_at ) {
$v['expires_at'] = time() + 30;
xcache_set( $storage_key, serialize( $v ), 0 );
$has_old_data = true;
return array( null, $has_old_data );
}
// return old version.
return array( $v, $has_old_data );
}
/**
* Replaces an existing cached item with a new value.
*
* This method updates the value for a given key only if the key already exists in the cache.
*
* @param string $key The unique key to identify the cached item.
* @param mixed $value The new value to store in the cache (passed by reference).
* @param int $expire The expiration time in seconds. Defaults to 0 (no expiration).
* @param string $group The group to which the key belongs. Defaults to an empty string.
*
* @return bool True if the value was replaced successfully, false if the key does not exist.
*/
public function replace( $key, &$value, $expire = 0, $group = '' ) {
if ( $this->get( $key, $group ) !== false ) {
return $this->set( $key, $value, $expire, $group );
}
return false;
}
/**
* Deletes a cached item, optionally keeping expired data.
*
* If expired data usage is enabled, the key version is set to 0 instead of completely removing the item.
* Otherwise, the item is fully deleted from the cache.
*
* @param string $key The unique key to identify the cached item.
* @param string $group The group to which the key belongs. Defaults to an empty string.
*
* @return bool True if the item was deleted successfully, false otherwise.
*/
public function delete( $key, $group = '' ) {
$storage_key = $this->get_item_key( $key );
if ( $this->_use_expired_data ) {
$v = @unserialize( xcache_get( $storage_key ) );
if ( is_array( $v ) ) {
$v['key_version'] = 0;
xcache_set( $storage_key, serialize( $v ), 0 );
return true;
}
}
return xcache_unset( $storage_key );
}
/**
* Completely removes a cached item from the cache.
*
* This method fully deletes the cached item, bypassing any logic for handling expired data.
*
* @param string $key The unique key to identify the cached item.
* @param string $group The group to which the key belongs. Defaults to an empty string.
*
* @return bool True if the item was removed successfully, false otherwise.
*/
public function hard_delete( $key, $group = '' ) {
$storage_key = $this->get_item_key( $key );
return xcache_unset( $storage_key );
}
/**
* Flushes the cache for a specific group or all groups.
*
* This increments the key version for the specified group, effectively invalidating all cache entries
* associated with the current key version.
*
* @param string $group (Optional) The cache group to flush. Default is an empty string, which applies to all groups.
*
* @return bool True on success.
*/
public function flush( $group = '' ) {
$this->_get_key_version( $group ); // initialize $this->_key_version.
++$this->_key_version[ $group ];
$this->_set_key_version( $this->_key_version[ $group ], $group );
return true;
}
/**
* Gets the key version extension for ahead-of-time cache generation.
*
* This provides the current key version and the next version to be used for generating ahead-of-time cache.
*
* @param string $group The cache group to retrieve the extension for.
*
* @return array An associative array with:
* - 'key_version' (int): The next key version.
* - 'key_version_at_creation' (int): The current key version.
*/
public function get_ahead_generation_extension( $group ) {
$v = $this->_get_key_version( $group );
return array(
'key_version' => $v + 1,
'key_version_at_creation' => $v,
);
}
/**
* Updates the key version for a cache group after ahead-of-time generation.
*
* If the provided key version is higher than the current version, the key version is updated.
*
* @param string $group The cache group to update.
* @param array $extension {
* The extension data containing 'key_version'.
*
* @type string $key_version The version of the cache key.
* }
*
* @return void
*/
public function flush_group_after_ahead_generation( $group, $extension ) {
$v = $this->_get_key_version( $group );
if ( $extension['key_version'] > $v ) {
$this->_set_key_version( $extension['key_version'], $group );
}
}
/**
* Checks if Wincache is available on the server.
*
* @return bool True if Wincache functions are available, false otherwise.
*/
public function available() {
return function_exists( 'xcache_set' );
}
/**
* Retrieves the current key version for a cache group.
*
* If no version exists, initializes the version to 1.
*
* @param string $group (Optional) The cache group to retrieve the key version for. Default is an empty string.
*
* @return int The current key version for the specified group.
*/
private function _get_key_version( $group = '' ) {
if ( ! isset( $this->_key_version[ $group ] ) || $this->_key_version[ $group ] <= 0 ) {
$v = xcache_get( $this->_get_key_version_key( $group ) );
$v = intval( $v );
$this->_key_version[ $group ] = ( $v > 0 ? $v : 1 );
}
return $this->_key_version[ $group ];
}
/**
* Sets the key version for a cache group.
*
* @param int $v The key version to set.
* @param string $group (Optional) The cache group to set the key version for. Default is an empty string.
*
* @return void
*/
private function _set_key_version( $v, $group = '' ) {
xcache_set( $this->_get_key_version_key( $group ), $v, 0 );
}
/**
* Sets a value conditionally if the old value matches the expected value.
*
* This method attempts to simulate an atomic check-and-set operation. If the current value does not
* match the old value, the operation fails.
*
* @param string $key The cache key to update.
* @param array $old_value {
* The expected current value.
*
* @type string $content The expected content to compare.
* }
* @param array $new_value The new value to set.
*
* @return bool True if the operation succeeds, false otherwise.
*/
public function set_if_maybe_equals( $key, $old_value, $new_value ) {
// cant guarantee atomic action here, filelocks fail often.
$value = $this->get( $key );
if ( isset( $old_value['content'] ) && $value['content'] !== $old_value['content'] ) {
return false;
}
return $this->set( $key, $new_value );
}
/**
* Increments a counter by the specified value.
*
* If the counter does not exist, initializes it with a value of 0 before incrementing.
*
* @param string $key The key of the counter to increment.
* @param int $value The value to increment the counter by.
*
* @return int|bool The new counter value on success, or false on failure.
*/
public function counter_add( $key, $value ) {
if ( 0 === $value ) {
return true;
}
$storage_key = $this->get_item_key( $key );
$r = xcache_inc( $storage_key, $value );
if ( ! $r ) { // it doesnt initialize counter by itself.
$this->counter_set( $key, 0 );
}
return $r;
}
/**
* Sets the value of a counter.
*
* @param string $key The key of the counter to set.
* @param int $value The value to set the counter to.
*
* @return bool True on success, false on failure.
*/
public function counter_set( $key, $value ) {
$storage_key = $this->get_item_key( $key );
return xcache_set( $storage_key, $value );
}
/**
* Retrieves the value of a counter.
*
* @param string $key The key of the counter to retrieve.
*
* @return int The current counter value, or 0 if the counter does not exist.
*/
public function counter_get( $key ) {
$storage_key = $this->get_item_key( $key );
$v = (int) xcache_get( $storage_key );
return $v;
}
}

View File

@@ -0,0 +1,104 @@
<?php
/**
* File: CdnEngine.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class: CdnEngine
*
* phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
*/
class CdnEngine {
/**
* Returns CdnEngine_Base instance.
*
* @param string $engine CDN engine.
* @param array $config Configuration.
*
* @return CdnEngine_Base
*/
public static function instance( $engine, array $config = array() ) {
static $instances = array();
$instance_key = sprintf( '%s_%s', $engine, md5( serialize( $config ) ) );
if ( ! isset( $instances[ $instance_key ] ) ) {
switch ( $engine ) {
case 'akamai':
$instances[ $instance_key ] = new CdnEngine_Mirror_Akamai( $config );
break;
case 'att':
$instances[ $instance_key ] = new CdnEngine_Mirror_Att( $config );
break;
case 'azure':
$instances[ $instance_key ] = new CdnEngine_Azure( $config );
break;
case 'azuremi':
$instances[ $instance_key ] = new CdnEngine_Azure_MI( $config );
break;
case 'bunnycdn':
$instances[ $instance_key ] = new CdnEngine_Mirror_BunnyCdn( $config );
break;
case 'cf':
$instances[ $instance_key ] = new CdnEngine_CloudFront( $config );
break;
case 'cf2':
$instances[ $instance_key ] = new CdnEngine_Mirror_CloudFront( $config );
break;
case 'cotendo':
$instances[ $instance_key ] = new CdnEngine_Mirror_Cotendo( $config );
break;
case 'edgecast':
$instances[ $instance_key ] = new CdnEngine_Mirror_Edgecast( $config );
break;
case 'ftp':
$instances[ $instance_key ] = new CdnEngine_Ftp( $config );
break;
case 'google_drive':
$instances[ $instance_key ] = new CdnEngine_GoogleDrive( $config );
break;
case 'mirror':
$instances[ $instance_key ] = new CdnEngine_Mirror( $config );
break;
case 'rackspace_cdn':
$instances[ $instance_key ] = new CdnEngine_Mirror_RackSpaceCdn( $config );
break;
case 'rscf':
$instances[ $instance_key ] = new CdnEngine_RackSpaceCloudFiles( $config );
break;
case 's3':
$instances[ $instance_key ] = new CdnEngine_S3( $config );
break;
case 's3_compatible':
$instances[ $instance_key ] = new CdnEngine_S3_Compatible( $config );
break;
default:
empty( $engine ) || trigger_error( 'Incorrect CDN engine', E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
$instances[ $instance_key ] = new CdnEngine_Base();
break;
}
}
return $instances[ $instance_key ];
}
}

View File

@@ -0,0 +1,443 @@
<?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
*
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
* phpcs:disable WordPress.WP.AlternativeFunctions
*/
class CdnEngine_Azure extends CdnEngine_Base {
/**
* Storage client object
*
* @var \MicrosoftAzure\Storage\Blob\BlobRestProxy
*/
private $_client = null;
/**
* Constructor for initializing the CdnEngine_Azure object.
*
* @param array $config An associative array of configuration values.
*
* @return void
*/
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';
}
/**
* Initialize the Azure Blob Storage client.
*
* Validates the configuration and establishes a connection to Azure Blob Storage.
*
* @param string $error A reference variable to capture error messages.
*
* @return bool Returns true if initialization is successful, false otherwise.
*/
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']
);
} catch ( \Exception $ex ) {
$error = $ex->getMessage();
return false;
}
return true;
}
/**
* Upload files to Azure Blob Storage.
*
* @param array $files An array of files to be uploaded.
* @param array $results A reference to an array where the upload results will be stored.
* @param bool $force_rewrite Whether to force rewrite of existing files (default false).
* @param int|null $timeout_time The time (in Unix timestamp) when the upload should timeout (optional).
*
* @return bool|string Returns true if the upload is successful, 'timeout' if a timeout occurs, or false if there is an error.
*/
public 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 );
}
/**
* Upload a single file to Azure Blob Storage.
*
* @param array $file An array containing the local and remote file paths.
* @param bool $force_rewrite Whether to force rewrite of existing files (default false).
*
* @return array The result of the upload operation.
*/
public 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 ); phpcs:ignore Squiz.PHP.CommentedOutCode.Found
$content_md5 = $this->_get_content_md5( $md5 );
if ( ! $force_rewrite ) {
try {
$properties_result = $this->_client->getBlobProperties( $this->_config['container'], $remote_path );
$p = $properties_result->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 ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
}
}
$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 );
}
/**
* Delete files from Azure Blob Storage.
*
* @param array $files An array of files to be deleted.
* @param array $results A reference to an array where the delete results will be stored.
*
* @return bool Returns true if all deletions are successful, false otherwise.
*/
public 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 );
}
/**
* Test the connection and functionality of Azure Blob Storage.
*
* @param string $error A reference variable to capture error messages.
*
* @return bool Returns true if the test is successful, false otherwise.
*/
public 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 {
$properties_result = $this->_client->getBlobProperties( $this->_config['container'], $string );
$p = $properties_result->getProperties();
$size = $p->getContentLength();
$md5 = $p->getContentMD5();
} catch ( \Exception $exception ) {
$error = sprintf( 'Unable to get blob properties (%s).', $exception->getMessage() );
return false;
}
if ( strlen( $string ) !== $size || $this->_get_content_md5( md5( $string ) ) !== $md5 ) {
try {
$this->_client->deleteBlob( $this->_config['container'], $string );
} catch ( \Exception $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
}
$error = 'Blob data properties are not equal.';
return false;
}
try {
$get_blob = $this->_client->getBlob( $this->_config['container'], $string );
$data_stream = $get_blob->getContentStream();
$data = stream_get_contents( $data_stream );
} 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 ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
}
$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;
}
/**
* Retrieves the domains for the Azure CDN configuration.
*
* @return array The list of domains based on the current configuration.
*/
public function get_domains() {
if ( ! empty( $this->_config['cname'] ) ) {
return (array) $this->_config['cname'];
} elseif ( ! empty( $this->_config['user'] ) ) {
$domain = sprintf( '%s.blob.core.windows.net', $this->_config['user'] );
return array(
$domain,
);
}
return array();
}
/**
* Retrieves the "via" string indicating the source of the request.
*
* @return string The "via" string for the Azure CDN.
*/
public function get_via() {
return sprintf( 'Windows Azure Storage: %s', parent::get_via() );
}
/**
* Creates a container in Azure Storage if it doesn't already exist.
*
* @throws \Exception If there is an error creating the container.
*
* @return void
*/
public function create_container() {
if ( ! $this->_init( $error ) ) {
throw new \Exception( esc_html( $error ) );
}
try {
$containers = $this->_client->listContainers();
} catch ( \Exception $exception ) {
$error = sprintf( 'Unable to list containers (%s).', $exception->getMessage() );
throw new \Exception( esc_html( $error ) );
}
if ( in_array( $this->_config['container'], (array) $containers, true ) ) {
$error = sprintf( 'Container already exists: %s.', $this->_config['container'] );
throw new \Exception( esc_html( $error ) );
}
try {
$create_container_options = new \MicrosoftAzure\Storage\Blob\Models\CreateContainerOptions();
$create_container_options->setPublicAccess( \MicrosoftAzure\Storage\Blob\Models\PublicAccessType::CONTAINER_AND_BLOBS );
$this->_client->createContainer( $this->_config['container'], $create_container_options );
} catch ( \Exception $exception ) {
$error = sprintf( 'Unable to create container: %s (%s)', $this->_config['container'], $exception->getMessage() );
throw new \Exception( esc_html( $error ) );
}
}
/**
* Converts the provided MD5 hash to a base64-encoded string.
*
* @param string $md5 The MD5 hash to convert.
*
* @return string The base64-encoded MD5 hash.
*/
public function _get_content_md5( $md5 ) {
return base64_encode( pack( 'H*', $md5 ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
}
/**
* Formats the URL for a given path in the Azure CDN.
*
* @param string $path The path to format the URL for.
*
* @return string|false The formatted URL or false if the domain or container is missing.
*/
public 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;
}
/**
* Returns the header support flag for the CDN.
*
* @return int The header support flag indicating the CDN's capabilities.
*/
public function headers_support() {
return W3TC_CDN_HEADER_UPLOADABLE;
}
/**
* Returns the path to prepend for the given path, considering the container.
*
* @param string $path The path to modify.
*
* @return string The modified path with the container prepended.
*/
public function get_prepend_path( $path ) {
$path = parent::get_prepend_path( $path );
$path = $this->_config['container'] ? trim( $path, '/' ) . '/' . trim( $this->_config['container'], '/' ) : $path;
return $path;
}
}

View File

@@ -0,0 +1,493 @@
<?php
/**
* File: CdnEngine_Azure.php
*
* Microsoft Azure Managed Identities are available only for services running on Azure when a "system assigned" identity is enabled.
*
* A system assigned managed identity is restricted to one per resource and is tied to the lifecycle of a resource.
* You can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC).
* The managed identity is authenticated with Microsoft Entra ID, so you dont have to store any credentials in code.
*
* @package W3TC
* @since 2.7.7
*/
namespace W3TC;
/**
* Class: CdnEngine_Azure_MI
*
* Windows Azure Storage CDN engine.
*
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
* phpcs:disable WordPress.WP.AlternativeFunctions
*/
class CdnEngine_Azure_MI extends CdnEngine_Base {
/**
* Constructor.
*
* @since 2.7.7
*
* @param array $config Configuration.
*/
public function __construct( $config = array() ) {
$config = array_merge(
array(
'user' => (string) getenv( 'STORAGE_ACCOUNT_NAME' ),
'client_id' => (string) getenv( 'ENTRA_CLIENT_ID' ),
'container' => (string) getenv( 'BLOB_CONTAINER_NAME' ),
'cname' => empty( getenv( 'BLOB_STORAGE_URL' ) ) ? array() : array( (string) getenv( 'BLOB_STORAGE_URL' ) ),
),
$config
);
parent::__construct( $config );
// Load the Composer autoloader.
require_once W3TC_DIR . '/vendor/autoload.php';
}
/**
* Initialize storage client object.
*
* @since 2.7.7
*
* @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['client_id'] ) ) {
$error = 'Empty Entra client ID.';
return false;
}
if ( empty( $this->_config['container'] ) ) {
$error = 'Empty container name.';
return false;
}
return true;
}
/**
* Upload files to Azure Blob Storage.
*
* @since 2.7.7
*
* @param array $files Files.
* @param array $results Results.
* @param bool $force_rewrite Force rewrite.
* @param int|null $timeout_time Timeout time.
* @return bool
*/
public 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 ) {
// Process at least one item before timeout so that progress goes on.
if ( ! empty( $results ) ) {
if ( ! is_null( $timeout_time ) && time() > $timeout_time ) {
// Timeout.
return false;
}
}
$results[] = $this->_upload( $file, $force_rewrite );
}
return ! $this->_is_error( $results );
}
/**
* Upload file to Azure Blob Storage.
*
* @since 2.7.7
*
* @param string $file File path.
* @param bool $force_rewrite Force rewrite.
* @return array
*/
public 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 );
$content_md5 = $this->_get_content_md5( $md5 );
if ( ! $force_rewrite ) {
try {
$p = CdnEngine_Azure_MI_Utility::get_blob_properties(
$this->_config['client_id'],
$this->_config['user'],
$this->_config['container'],
$remote_path
);
$local_size = @filesize( $local_path );
// Check if Content-Length is available in $p array.
if ( isset( $p['Content-Length'] ) && (int) $local_size === (int) $p['Content-Length'] && isset( $p['Content-MD5'] ) && $content_md5 === $p['Content-MD5'] ) {
return $this->_get_result( $local_path, $remote_path, W3TC_CDN_RESULT_OK, 'File up-to-date.', $file );
}
} catch ( \Exception $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
}
}
$headers = $this->get_headers_for_file( $file );
try {
$content_type = isset( $headers['Content-Type'] ) ? $headers['Content-Type'] : 'application/octet-stream';
$cache_control = isset( $headers['Cache-Control'] ) ? $headers['Cache-Control'] : '';
CdnEngine_Azure_MI_Utility::create_block_blob(
$this->_config['client_id'],
$this->_config['user'],
$this->_config['container'],
$remote_path,
$contents,
$content_type,
$content_md5,
$cache_control
);
} catch ( \Exception $exception ) {
return $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_ERROR,
sprintf( 'Unable to put blob (%1$s).', $exception->getMessage() ),
$file
);
}
return $this->_get_result( $local_path, $remote_path, W3TC_CDN_RESULT_OK, 'OK', $file );
}
/**
* Delete files from Azure Blob Storage.
*
* @since 2.7.7
*
* @param array $files Files.
* @param array $results Results.
* @return bool
*/
public 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 {
CdnEngine_Azure_MI_Utility::delete_blob(
$this->_config['client_id'],
$this->_config['user'],
$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 (%1$s).', $exception->getMessage() ),
$file
);
}
}
return ! $this->_is_error( $results );
}
/**
* Test Azure Blob Storage.
*
* @since 2.7.7
*
* @param string $error Error message.
* @return bool
*/
public function test( &$error ) {
if ( ! parent::test( $error ) ) {
return false;
}
$string = 'test_azure_' . md5( time() );
if ( ! $this->_init( $error ) ) {
return false;
}
try {
$containers = CdnEngine_Azure_MI_Utility::list_containers(
$this->_config['client_id'],
$this->_config['user']
);
} catch ( \Exception $exception ) {
$error = sprintf( 'Unable to list containers (%1$s).', $exception->getMessage() );
return false;
}
$container = null;
foreach ( $containers as $_container ) {
if ( $_container['Name'] === $this->_config['container'] ) {
$container = $_container;
break;
}
}
if ( ! $container ) {
$error = sprintf( 'Container doesn\'t exist: %1$s.', $this->_config['container'] );
return false;
}
try {
CdnEngine_Azure_MI_Utility::create_block_blob(
$this->_config['client_id'],
$this->_config['user'],
$this->_config['container'],
$string,
$string
);
} catch ( \Exception $exception ) {
$error = sprintf( 'Unable to create blob (%1$s).', $exception->getMessage() );
return false;
}
try {
$p = CdnEngine_Azure_MI_Utility::get_blob_properties(
$this->_config['client_id'],
$this->_config['user'],
$this->_config['container'],
$string
);
$size = isset( $p['Content-Length'] ) ? (int) $p['Content-Length'] : -1;
$md5 = isset( $p['Content-MD5'] ) ? $p['Content-MD5'] : '';
} catch ( \Exception $exception ) {
$error = sprintf( 'Unable to get blob properties (%1$s).', $exception->getMessage() );
return false;
}
if ( strlen( $string ) !== $size || $this->_get_content_md5( md5( $string ) ) !== $md5 ) {
try {
CdnEngine_Azure_MI_Utility::delete_blob(
$this->_config['client_id'],
$this->_config['user'],
$this->_config['container'],
$string
);
} catch ( \Exception $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
}
$error = 'Blob data properties are not equal.';
return false;
}
try {
$blob_response = CdnEngine_Azure_MI_Utility::get_blob(
$this->_config['client_id'],
$this->_config['user'],
$this->_config['container'],
$string
);
$data = isset( $blob_response['data'] ) ? $blob_response['data'] : '';
} catch ( \Exception $exception ) {
$error = sprintf( 'Unable to get blob data (%1$s).', $exception->getMessage() );
return false;
}
if ( $data !== $string ) {
try {
CdnEngine_Azure_MI_Utility::delete_blob(
$this->_config['client_id'],
$this->_config['user'],
$this->_config['container'],
$string
);
} catch ( \Exception $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
}
$error = 'Blob datas are not equal.';
return false;
}
try {
CdnEngine_Azure_MI_Utility::delete_blob(
$this->_config['client_id'],
$this->_config['user'],
$this->_config['container'],
$string
);
} catch ( \Exception $exception ) {
$error = sprintf( 'Unable to delete blob (%s).', $exception->getMessage() );
return false;
}
return true;
}
/**
* Returns CDN domains.
*
* @since 2.7.7
*
* @return array
*/
public function get_domains() {
if ( ! empty( $this->_config['cname'] ) ) {
return (array) $this->_config['cname'];
} elseif ( ! empty( $this->_config['user'] ) ) {
$domain = sprintf( '%1$s.blob.core.windows.net', $this->_config['user'] );
return array( $domain );
}
return array();
}
/**
* Returns via string.
*
* @since 2.7.7
*
* @return string
*/
public function get_via() {
return sprintf( 'Windows Azure Storage: %1$s', parent::get_via() );
}
/**
* Create an Azure Blob Storage container/bucket.
*
* @since 2.7.7
*
* @return bool
* @throws \Exception Exception.
*/
public function create_container() {
if ( ! $this->_init( $error ) ) {
throw new \Exception( esc_html( $error ) );
}
try {
$containers = CdnEngine_Azure_MI_Utility::list_containers(
$this->_config['client_id'],
$this->_config['user']
);
} catch ( \Exception $exception ) {
$error = sprintf( 'Unable to list containers (%1$s).', $exception->getMessage() );
throw new \Exception( esc_html( $error ) );
}
foreach ( $containers as $_container ) {
if ( $_container['Name'] === $this->_config['container'] ) {
$error = sprintf( 'Container already exists: %1$s.', $this->_config['container'] );
throw new \Exception( esc_html( $error ) );
}
}
try {
$result = CdnEngine_Azure_MI_Utility::create_container(
$this->_config['client_id'],
$this->_config['user'],
$this->_config['container']
);
return true; // Maybe return container ID.
} catch ( \Exception $exception ) {
$error = sprintf( 'Unable to create container: %1$s (%2$s)', $this->_config['container'], $exception->getMessage() );
throw new \Exception( esc_html( $error ) );
}
}
/**
* Return Content-MD5 header value.
*
* @since 2.7.7
*
* @param string $md5 MD5 hash.
* @return string Base64-encoded packed (hex string, high nibble first, repeating to the end of the input data) data from the input MD% string.
*/
public function _get_content_md5( $md5 ) {
return base64_encode( pack( 'H*', $md5 ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
}
/**
* Format object URL.
*
* @since 2.7.7
*
* @param string $path Path.
* @return string|false
*/
public function _format_url( $path ) {
$domain = $this->get_domain( $path );
if ( $domain && ! empty( $this->_config['container'] ) ) {
$scheme = $this->_get_scheme();
$url = sprintf( '%1$s://%2$s/%3$s/%4$s', $scheme, $domain, $this->_config['container'], $path );
return $url;
}
return false;
}
/**
* How and if headers should be set.
*
* @since 2.7.7
*
* @return string W3TC_CDN_HEADER_NONE, W3TC_CDN_HEADER_UPLOADABLE, or W3TC_CDN_HEADER_MIRRORING.
*/
public function headers_support() {
return W3TC_CDN_HEADER_UPLOADABLE;
}
/**
* Get prepend path.
*
* @since 2.7.7
*
* @param string $path Path.
* @return string
*/
public function get_prepend_path( $path ) {
$path = parent::get_prepend_path( $path );
$path = $this->_config['container'] ? trim( $path, '/' ) . '/' . trim( $this->_config['container'], '/' ) : $path;
return $path;
}
}

View File

@@ -0,0 +1,698 @@
<?php
/**
* File: CdnEngine_Azure_MI_Utility.php
*
* Microsoft Azure Managed Identities are available only for services running on Azure when a "system assigned" identity is enabled.
*
* A system assigned managed identity is restricted to one per resource and is tied to the lifecycle of a resource.
* You can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC).
* The managed identity is authenticated with Microsoft Entra ID, so you dont have to store any credentials in code.
*
* @package W3TC
* @since 2.7.7
*/
namespace W3TC;
/**
* Class: CdnEngine_Azure_MI_Utility
*
* This class defines utility functions for Azure blob storage access using Managed Identity.
*
* @since 2.7.7
* @author Zubair <zmohammed@microsoft.com>
* @author BoldGrid <development@boldgrid.com>
*
* phpcs:disable WordPress.WP.AlternativeFunctions
*/
class CdnEngine_Azure_MI_Utility {
/**
* Entra API version.
*
* @since 2.7.7
*
* @var string
*/
const ENTRA_API_VERSION = '2019-08-01';
/**
* Entra resource URI.
*
* @since 2.7.7
*
* @var string
*/
const ENTRA_RESOURCE_URI = 'https://storage.azure.com';
/**
* Blob API version.
*
* @since 2.7.7
*
* @var string
*/
const BLOB_API_VERSION = '2020-10-02';
/**
* Retrieves an access token from the Managed Identity endpoint.
*
* @since 2.7.7
*
* @param string $entra_client_id Entra ID.
* @return string $access_token
* @throws \RuntimeException Runtine Exception.
*/
public static function get_access_token( string $entra_client_id ): string {
// Get environment variables.
$identity_header = \getenv( 'IDENTITY_HEADER' );
$identity_endpoint = \getenv( 'IDENTITY_ENDPOINT' );
// Validate variables.
if ( empty( $identity_endpoint ) || empty( $identity_header ) || empty( $entra_client_id ) ) {
throw new \RuntimeException( \esc_html__( 'Error: get_access_token - missing required environment variables.', 'w3-total-cache' ) );
}
// Construct URL for cURL request.
$url = $identity_endpoint . '?' . http_build_query(
array(
'api-version' => self::ENTRA_API_VERSION,
'resource' => self::ENTRA_RESOURCE_URI,
'client_id' => $entra_client_id,
)
);
// Initialize and execute cURL request.
$ch = \curl_init();
\curl_setopt( $ch, CURLOPT_URL, $url );
\curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, 'GET' );
\curl_setopt( $ch, CURLOPT_HTTPHEADER, array( 'X-IDENTITY-HEADER: ' . $identity_header ) );
\curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
\curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
$response = \curl_exec( $ch );
if ( \curl_errno( $ch ) ) {
$error = \curl_error( $ch );
\curl_close( $ch );
throw new \RuntimeException(
\esc_html(
sprintf(
// Translators: 1 Error message.
\__( 'Error: get_access_token - cURL request failed: %1$s', 'w3-total-cache' ),
$error
)
)
);
}
$http_code = \curl_getinfo( $ch, CURLINFO_HTTP_CODE );
\curl_close( $ch );
if ( 200 !== $http_code ) {
throw new \RuntimeException(
\esc_html(
sprintf(
// Translators: 1 HTTP status code.
\__( 'Error: get_access_token - HTTP request failed with status code: %1$s', 'w3-total-cache' ),
$http_code
)
)
);
}
if ( empty( $response ) ) {
throw new \RuntimeException(
\esc_html(
sprintf(
// Translators: 1 Response.
\__( 'Error: get_access_token - invalid response data: %1$s', 'w3-total-cache' ),
$response
)
)
);
}
// Parse JSON response and extract access_token.
$json_response = \json_decode( $response, true );
if ( \json_last_error() !== JSON_ERROR_NONE ) {
throw new \RuntimeException(
\esc_html(
sprintf(
// Translators: 1 JSON last error message.
\__( 'Error: get_access_token - failed to parse the JSON response: %1$s', 'w3-total-cache' ),
\json_last_error_msg()
)
)
);
}
if ( empty( $json_response['access_token'] ) ) {
throw new \RuntimeException(
\esc_html(
sprintf(
// Translators: 1 Response.
\__( 'Error: get_access_token - no token found in response data: %1$s', 'w3-total-cache' ),
$response
)
)
);
}
return $json_response['access_token'];
}
/**
* Get Azure Blob Storage blob properties.
*
* @since 2.7.7
*
* @param string $entra_client_id Entra ID.
* @param string $storage_account Storage account name.
* @param string $container_id Container ID.
* @param string $blob Blob ID.
* @return array
* @throws \RuntimeException Runtine Exception.
*/
public static function get_blob_properties( string $entra_client_id, string $storage_account, string $container_id, $blob ): array {
$ch = \curl_init();
\curl_setopt( $ch, CURLOPT_URL, 'https://' . $storage_account . '.blob.core.windows.net/' . $container_id . '/' . $blob );
\curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
\curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
\curl_setopt(
$ch,
CURLOPT_HTTPHEADER,
array(
'Authorization: Bearer ' . self::get_access_token( $entra_client_id ),
'x-ms-version: ' . self::BLOB_API_VERSION,
'x-ms-date: ' . \gmdate( 'D, d M Y H:i:s T', time() ),
)
);
\curl_setopt( $ch, CURLOPT_HEADER, true );
\curl_setopt( $ch, CURLOPT_NOBODY, true );
$response = \curl_exec( $ch );
if ( \curl_errno( $ch ) ) {
$error = \curl_error( $ch );
\curl_close( $ch );
throw new \RuntimeException(
\esc_html(
sprintf(
// Translators: 1 Error message.
\__( 'Error: get_blob_properties - cURL request failed: %1$s', 'w3-total-cache' ),
$error
)
)
);
}
$http_code = \curl_getinfo( $ch, CURLINFO_HTTP_CODE );
\curl_close( $ch );
if ( 200 !== $http_code ) {
throw new \RuntimeException(
\esc_html(
sprintf(
// Translators: 1 HTTP status code.
\__( 'Error: get_blob_properties - HTTP request failed with status code: %1$s', 'w3-total-cache' ),
$http_code
)
)
);
}
if ( empty( $response ) ) {
throw new \RuntimeException(
\esc_html(
sprintf(
// Translators: 1 Response.
\__( 'Error: get_blob_properties - invalid response data: %1$s', 'w3-total-cache' ),
$response
)
)
);
}
return self::parse_header( $response );
}
/**
* Create block blob.
*
* @since 2.7.7
*
* @param string $entra_client_id Entra ID.
* @param string $storage_account Storage account name.
* @param string $container_id Container ID.
* @param string $blob Blob ID.
* @param mixed $contents Contents.
* @param string $content_type Content type.
* @param string $content_md5 Content MD5 hash.
* @param string $cache_control Cache control header value.
* @return array
* @throws \RuntimeException Runtine Exception.
*/
public static function create_block_blob(
string $entra_client_id,
string $storage_account,
string $container_id,
string $blob,
$contents,
string $content_type = null,
string $content_md5 = null,
string $cache_control = null
): array {
$headers = array(
'Authorization: Bearer ' . self::get_access_token( $entra_client_id ),
'x-ms-version: ' . self::BLOB_API_VERSION,
'x-ms-date: ' . \gmdate( 'D, d M Y H:i:s T', \time() ),
'x-ms-blob-type: BlockBlob',
);
if ( $content_type ) {
$headers[] = 'x-ms-blob-content-type: ' . $content_type;
}
if ( $content_md5 ) {
$headers[] = 'x-ms-blob-content-md5: ' . $content_md5;
}
if ( $cache_control ) {
$headers[] = 'x-ms-blob-cache-control: ' . $cache_control;
}
$ch = \curl_init();
\curl_setopt( $ch, CURLOPT_URL, 'https://' . $storage_account . '.blob.core.windows.net/' . $container_id . '/' . $blob );
\curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
\curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, 'PUT' );
\curl_setopt( $ch, CURLOPT_POSTFIELDS, $contents );
\curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
\curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
\curl_setopt( $ch, CURLOPT_HEADER, true );
$response = \curl_exec( $ch );
if ( \curl_errno( $ch ) ) {
$error = \curl_error( $ch );
\curl_close( $ch );
throw new \RuntimeException(
\esc_html(
sprintf(
// Translators: 1 Error message.
\__( 'Error: create_block_blob - cURL request failed: %1$s', 'w3-total-cache' ),
$error
)
)
);
}
$http_code = \curl_getinfo( $ch, CURLINFO_HTTP_CODE );
\curl_close( $ch );
if ( 201 !== $http_code ) {
throw new \RuntimeException(
\esc_html(
sprintf(
// Translators: 1 HTTP status code.
\__( 'Error: create_block_blob - HTTP request failed with status code: %1$s', 'w3-total-cache' ),
$http_code
)
)
);
}
if ( empty( $response ) ) {
throw new \RuntimeException(
\esc_html(
sprintf(
// Translators: 1 Response.
\__( 'Error: create_block_blob - invalid response data: %1$s', 'w3-total-cache' ),
$response
)
)
);
}
return self::parse_header( $response );
}
/**
* Delete blob.
*
* @since 2.7.7
*
* @param string $entra_client_id Entra ID.
* @param string $storage_account Storage account name.
* @param string $container_id Container ID.
* @param string $blob Blob ID.
* @return array
* @throws \RuntimeException Runtine Exception.
*/
public static function delete_blob( string $entra_client_id, string $storage_account, string $container_id, string $blob ) {
$ch = \curl_init();
\curl_setopt( $ch, CURLOPT_URL, 'https://' . $storage_account . '.blob.core.windows.net/' . $container_id . '/' . $blob );
\curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
\curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, 'DELETE' );
\curl_setopt(
$ch,
CURLOPT_HTTPHEADER,
array(
'Authorization: Bearer ' . self::get_access_token( $entra_client_id ),
'x-ms-version: ' . self::BLOB_API_VERSION,
'x-ms-date: ' . \gmdate( 'D, d M Y H:i:s T', \time() ),
)
);
\curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
\curl_setopt( $ch, CURLOPT_HEADER, true );
$response = \curl_exec( $ch );
if ( \curl_errno( $ch ) ) {
$error = \curl_error( $ch );
\curl_close( $ch );
throw new \RuntimeException(
\esc_html(
sprintf(
// Translators: 1 Error message.
\__( 'Error: delete_blob - cURL request failed: %1$s', 'w3-total-cache' ),
$error
)
)
);
}
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
curl_close( $ch );
if ( 202 !== $http_code ) {
throw new \RuntimeException(
\esc_html(
sprintf(
// Translators: 1 HTTP status code.
\__( 'Error: delete_blob - HTTP request failed with status code: %1$s', 'w3-total-cache' ),
$http_code
)
)
);
}
if ( empty( $response ) ) {
throw new \RuntimeException(
\esc_html(
sprintf(
// Translators: 1 Response.
\__( 'Error: delete_blob - invalid response data: %1$s', 'w3-total-cache' ),
$response
)
)
);
}
return self::parse_header( $response );
}
/**
* Create an Azure Blob Storage container/bucket.
*
* @since 2.7.7
*
* @param string $entra_client_id Entra ID.
* @param string $storage_account Storage account name.
* @param string $container_id Container ID.
* @param string $public_access_type Public access type.
* @return array
* @throws \RuntimeException Runtine Exception.
*/
public static function create_container(
string $entra_client_id,
string $storage_account,
string $container_id,
string $public_access_type = 'blob'
): array {
$headers = array(
'Authorization: Bearer ' . self::get_access_token( $entra_client_id ),
'x-ms-version: ' . self::BLOB_API_VERSION,
'x-ms-date: ' . \gmdate( 'D, d M Y H:i:s T', \time() ),
'Content-Length: 0',
);
if ( $public_access_type ) {
$headers[] = "x-ms-blob-public-access: $public_access_type";
}
$ch = \curl_init();
\curl_setopt( $ch, CURLOPT_URL, 'https://' . $storage_account . '.blob.core.windows.net/' . $container_id . '?restype=container' );
\curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
\curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, 'PUT' );
\curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
\curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
\curl_setopt( $ch, CURLOPT_HEADER, true );
$response = \curl_exec( $ch );
if ( \curl_errno( $ch ) ) {
$error = \curl_error( $ch );
\curl_close( $ch );
throw new \RuntimeException(
\esc_html(
sprintf(
// Translators: 1 Error message.
\__( 'Error: create_container - cURL request failed: %1$s', 'w3-total-cache' ),
$error
)
)
);
}
$http_code = \curl_getinfo( $ch, CURLINFO_HTTP_CODE );
\curl_close( $ch );
if ( 201 !== $http_code ) {
throw new \RuntimeException(
\esc_html(
sprintf(
// Translators: 1 HTTP status code.
\__( 'Error: create_container - HTTP request failed with status code: %1$s', 'w3-total-cache' ),
$http_code
)
)
);
}
if ( empty( $response ) ) {
throw new \RuntimeException( \esc_html__( 'Error: create_container - empty response.', 'w3-total-cache' ) );
}
return self::parse_header( $response );
}
/**
* List Azure Blob Storage containers.
*
* @since 2.7.7
*
* @param string $entra_client_id Entra ID.
* @param string $storage_account Storage account name.
* @return array
* @throws \RuntimeException Runtine Exception.
*/
public static function list_containers( string $entra_client_id, string $storage_account ): array {
$ch = \curl_init();
\curl_setopt( $ch, CURLOPT_URL, 'https://' . $storage_account . '.blob.core.windows.net/?comp=list' );
\curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
\curl_setopt(
$ch,
CURLOPT_HTTPHEADER,
array(
'Authorization: Bearer ' . self::get_access_token( $entra_client_id ),
'x-ms-version: ' . self::BLOB_API_VERSION,
'x-ms-date: ' . \gmdate( 'D, d M Y H:i:s T', \time() ),
)
);
\curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
$response = \curl_exec( $ch );
if ( \curl_errno( $ch ) ) {
$error = \curl_error( $ch );
\curl_close( $ch );
throw new \RuntimeException(
\esc_html(
sprintf(
// Translators: 1 Error message.
\__( 'Error: list_containers - cURL request failed: %1$s', 'w3-total-cache' ),
$error
)
)
);
}
$http_code = \curl_getinfo( $ch, CURLINFO_HTTP_CODE );
\curl_close( $ch );
if ( 200 !== $http_code ) {
throw new \RuntimeException(
\esc_html(
sprintf(
// Translators: 1 HTTP status code.
\__( 'Error: list_containers - HTTP request failed with status code: %1$s', 'w3-total-cache' ),
$http_code
)
)
);
}
if ( empty( $response ) ) {
throw new \RuntimeException(
\esc_html(
sprintf(
// Translators: 1 Response.
\__( 'Error: list_containers - Invalid response data: %1$s', 'w3-total-cache' ),
$response
)
)
);
}
// Parse XML response to array.
$xml = \simplexml_load_string( $response );
$json = \json_encode( $xml );
$response = \json_decode( $json, true );
$array_response = array();
if ( ! empty( $response['Containers']['Container'] ) ) {
$array_response = self::get_array( $response['Containers']['Container'] );
}
return $array_response;
}
/**
* Get blob.
*
* @since 2.7.7
*
* @param string $entra_client_id Entra ID.
* @param string $storage_account Storage account name.
* @param string $container_id Container ID.
* @param string $blob Blob ID.
* @return array
* @throws \RuntimeException Runtine Exception.
*/
public static function get_blob( string $entra_client_id, string $storage_account, string $container_id, string $blob ): array {
$ch = \curl_init();
\curl_setopt( $ch, CURLOPT_URL, 'https://' . $storage_account . '.blob.core.windows.net/' . $container_id . '/' . $blob );
\curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
\curl_setopt(
$ch,
CURLOPT_HTTPHEADER,
array(
'Authorization: Bearer ' . self::get_access_token( $entra_client_id ),
'x-ms-version: ' . self::BLOB_API_VERSION,
'x-ms-date: ' . \gmdate( 'D, d M Y H:i:s T', \time() ),
)
);
\curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
\curl_setopt( $ch, CURLOPT_HEADER, true );
$response = \curl_exec( $ch );
if ( \curl_errno( $ch ) ) {
$error = \curl_error( $ch );
\curl_close( $ch );
throw new \RuntimeException(
\esc_html(
sprintf(
// Translators: 1 Error message.
\__( 'Error: get_blob - cURL request failed: %1$s', 'w3-total-cache' ),
$error
)
)
);
}
$header_size = \curl_getinfo( $ch, CURLINFO_HEADER_SIZE );
$http_code = \curl_getinfo( $ch, CURLINFO_HTTP_CODE );
\curl_close( $ch );
if ( 200 !== $http_code ) {
throw new \RuntimeException(
\esc_html(
sprintf(
// Translators: 1 HTTP status code.
\__( 'Error: get_blob - HTTP request failed with status code: %1$s', 'w3-total-cache' ),
$http_code
)
)
);
}
if ( empty( $response ) ) {
throw new \RuntimeException(
\esc_html(
sprintf(
// Translators: 1 Response.
\__( 'Error: get_blob - Invalid response data: %1$s', 'w3-total-cache' ),
$response
)
)
);
}
return array(
'headers' => self::parse_header( \substr( $response, 0, $header_size ) ),
'data' => \substr( $response, $header_size ),
);
}
/**
* Get array.
*
* @since 2.7.7
* @access private
*
* @param mixed $input Variable used to get array.
*
* @return array
*/
private static function get_array( $input ): array {
if ( empty( $input ) || ! \is_array( $input ) ) {
return array();
}
foreach ( $input as $value ) {
if ( ! \is_array( $value ) ) {
return array( $input );
}
return $input;
}
}
/**
* Parse header from string to array.
*
* @since 2.7.7
* @access private
*
* @param string $header Header.
* @return array
*/
private static function parse_header( string $header ): array {
$headers = array();
$header_text = \substr( $header, 0, \strpos( $header, "\r\n\r\n" ) );
$header_parts = \explode( "\r\n", $header_text );
foreach ( $header_parts as $header ) {
if ( \strpos( $header, ':' ) !== false ) {
$header_parts = \explode( ':', $header );
$headers[ $header_parts[0] ] = \trim( $header_parts[1] );
}
}
return $headers;
}
}

View File

@@ -0,0 +1,699 @@
<?php
/**
* File: CdnEngine_Base.php
*
* @package W3TC
*/
namespace W3TC;
/**
* W3 CDN Base class
*/
define( 'W3TC_CDN_RESULT_HALT', -1 );
define( 'W3TC_CDN_RESULT_ERROR', 0 );
define( 'W3TC_CDN_RESULT_OK', 1 );
define( 'W3TC_CDN_HEADER_NONE', 'none' );
define( 'W3TC_CDN_HEADER_UPLOADABLE', 'uploadable' );
define( 'W3TC_CDN_HEADER_MIRRORING', 'mirroring' );
/**
* Class CdnEngine_Base
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
* phpcs:disable WordPress.WP.AlternativeFunctions
* phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter
*/
class CdnEngine_Base {
/**
* Engine configuration
*
* @var array
*/
protected $_config = array();
/**
* Gzip extension
*
* @var string
*/
protected $_gzip_extension = '.gzip';
/**
* Last error
*
* @var string
*/
protected $_last_error = '';
/**
* Constructor method for initializing the CdnEngine_Base object with configuration settings.
*
* @param array $config Optional. An array of configuration options to override default values.
* Defaults include 'debug', 'ssl', 'compression', and 'headers'.
*/
public function __construct( $config = array() ) {
$this->_config = array_merge(
array(
'debug' => false,
'ssl' => 'auto',
'compression' => false,
'headers' => array(),
),
$config
);
}
/**
* Upload files to the CDN.
*
* @param array $files An array of files to upload.
* @param array $results A reference to an array where results will be stored.
* @param bool $force_rewrite Optional. Whether to force a rewrite. Default is false.
* @param int $timeout_time Optional. The timeout time in seconds. Default is null.
*
* @return bool False on failure.
*/
public function upload( $files, &$results, $force_rewrite = false, $timeout_time = null ) {
$results = $this->_get_results(
$files,
W3TC_CDN_RESULT_HALT,
'Not implemented.'
);
return false;
}
/**
* Delete files from the CDN.
*
* @param array $files An array of files to delete.
* @param array $results A reference to an array where results will be stored.
*
* @return bool False on failure.
*/
public function delete( $files, &$results ) {
$results = $this->_get_results(
$files,
W3TC_CDN_RESULT_HALT,
'Not implemented.'
);
return false;
}
/**
* Purge files from the CDN.
*
* @param array $files An array of files to purge.
* @param array $results A reference to an array where results will be stored.
*
* @return bool False on failure.
*/
public function purge( $files, &$results ) {
return $this->upload( $files, $results, true );
}
/**
* Purge all files from the CDN.
*
* @param array $results A reference to an array where results will be stored.
*
* @return bool False on failure.
*/
public function purge_all( &$results ) {
$results = $this->_get_results(
array(),
W3TC_CDN_RESULT_HALT,
'Not implemented.'
);
return false;
}
/**
* Test the connection to the CDN.
*
* @param string $error A reference to a variable where any error message will be stored.
*
* @return bool True if the test is successful, false otherwise.
*/
public function test( &$error ) {
if ( ! $this->_test_domains( $error ) ) {
return false;
}
return true;
}
/**
* Create a container on the CDN.
*
* @throws \Exception If the method is not implemented.
*/
public function create_container() {
throw new \Exception( \esc_html__( 'Not implemented.', 'w3-total-cache' ) );
}
/**
* Get the appropriate domain for a given path.
*
* @param string $path Optional. The path to check. Default is an empty string.
*
* @return string|false The selected domain or false if no domain is found.
*/
public function get_domain( $path = '' ) {
$domains = $this->get_domains();
$count = count( $domains );
if ( $count ) {
switch ( true ) {
/**
* Reserved CSS
*/
case ( isset( $domains[0] ) && $this->_is_css( $path ) ):
$domain = $domains[0];
break;
/**
* Reserved JS after body
*/
case ( isset( $domains[2] ) && $this->_is_js_body( $path ) ):
$domain = $domains[2];
break;
/**
* Reserved JS before /body
*/
case ( isset( $domains[3] ) && $this->_is_js_footer( $path ) ):
$domain = $domains[3];
break;
/**
* Reserved JS in head, moved here due to greedy regex
*/
case ( isset( $domains[1] ) && $this->_is_js( $path ) ):
$domain = $domains[1];
break;
default:
if ( ! isset( $domains[0] ) ) {
$scheme = $this->_get_scheme();
if ( 'https' === $scheme && ! empty( $domains['https_default'] ) ) {
return $domains['https_default'];
} else {
return isset( $domains['http_default'] ) ? $domains['http_default'] :
$domains['https_default'];
}
} elseif ( $count > 4 ) {
$domain = $this->_get_domain( array_slice( $domains, 4 ), $path );
} else {
$domain = $this->_get_domain( $domains, $path );
}
}
/**
* Custom host for SSL
*/
list( $domain_http, $domain_https ) = array_map( 'trim', explode( ',', $domain . ',' ) );
$scheme = $this->_get_scheme();
switch ( $scheme ) {
case 'http':
$domain = $domain_http;
break;
case 'https':
$domain = ( $domain_https ? $domain_https : $domain_http );
break;
}
return $domain;
}
return false;
}
/**
* Get all available domains.
*
* @return array An array of domains.
*/
public function get_domains() {
return array();
}
/**
* Get the domain used for accessing the CDN.
*
* @return string The domain URL.
*/
public function get_via() {
$domain = $this->get_domain();
if ( $domain ) {
return $domain;
}
return 'N/A';
}
/**
* Format a URL for the given path.
*
* @param string $path The path to format.
*
* @return string|false The formatted URL or false on failure.
*/
public function format_url( $path ) {
$url = $this->_format_url( $path );
if ( $url && $this->_config['compression'] && ( isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) ? stristr( sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT_ENCODING'] ) ), 'gzip' ) !== false : false ) && $this->_may_gzip( $path ) ) {
$qpos = strpos( $url, '?' );
if ( false !== $qpos ) {
$url = substr_replace( $url, $this->_gzip_extension, $qpos, 0 );
} else {
$url .= $this->_gzip_extension;
}
}
return $url;
}
/**
* Get the URL to prepend to a given path.
*
* @param string $path The path to prepend the URL to.
*
* @return string|false The full URL or false if no domain is found.
*/
public function get_prepend_path( $path ) {
$domain = $this->get_domain( $path );
if ( $domain ) {
$scheme = $this->_get_scheme();
$url = sprintf( '%s://%s', $scheme, $domain );
return $url;
}
return false;
}
/**
* Format a URL for the given path, with the appropriate scheme and domain.
*
* @param string $path The path to format.
*
* @return string|false The formatted URL or false if no domain is found.
*/
public function _format_url( $path ) {
$domain = $this->get_domain( $path );
if ( $domain ) {
$scheme = $this->_get_scheme();
$url = sprintf( '%s://%s/%s', $scheme, $domain, $path );
return $url;
}
return false;
}
/**
* Get results for a set of files.
*
* @param array $files The files for which results are generated.
* @param string $result Optional. The result status. Default is W3TC_CDN_RESULT_OK.
* @param string $error Optional. The error message. Default is 'OK'.
*
* @return array An array of results for each file.
*/
public function _get_results( $files, $result = W3TC_CDN_RESULT_OK, $error = 'OK' ) {
$results = array();
foreach ( $files as $key => $file ) {
if ( is_array( $file ) ) {
$local_path = $file['local_path'];
$remote_path = $file['remote_path'];
} else {
$local_path = $key;
$remote_path = $file;
}
$results[] = $this->_get_result(
$local_path,
$remote_path,
$result,
$error,
$file
);
}
return $results;
}
/**
* Retrieves the result data for a local and remote file path.
*
* @param string $local_path The local file path.
* @param string $remote_path The remote file path.
* @param int $result The result status (default is W3TC_CDN_RESULT_OK).
* @param string $error The error message (default is 'OK').
* @param mixed|null $descriptor Additional descriptor (default is null).
*
* @return array The result array containing local path, remote path, result, error, and descriptor.
*/
public function _get_result( $local_path, $remote_path, $result = W3TC_CDN_RESULT_OK, $error = 'OK', $descriptor = null ) {
if ( $this->_config['debug'] ) {
$this->_log( $local_path, $remote_path, $error );
}
return array(
'local_path' => $local_path,
'remote_path' => $remote_path,
'result' => $result,
'error' => $error,
'descriptor' => $descriptor,
);
}
/**
* Checks if any of the results contain an error.
*
* @param array $results The results to check.
*
* @return bool True if any result is an error, otherwise false.
*/
public function _is_error( $results ) {
foreach ( $results as $result ) {
if ( W3TC_CDN_RESULT_OK !== $result['result'] ) {
return true;
}
}
return false;
}
/**
* Retrieves the HTTP headers for a given file.
*
* @param array $file The file data array containing local path and original URL.
* @param array $whitelist Optional whitelist for specific headers (default is empty).
*
* @return array The HTTP headers for the file.
*/
public function get_headers_for_file( $file, $whitelist = array() ) {
$local_path = $file['local_path'];
$mime_type = Util_Mime::get_mime_type( $local_path );
$link = $file['original_url'];
$headers = array(
'Content-Type' => $mime_type,
'Last-Modified' => Util_Content::http_date( time() ),
'Access-Control-Allow-Origin' => '*',
'Link' => '<' . $link . '>; rel="canonical"',
);
$section = Util_Mime::mime_type_to_section( $mime_type );
if ( isset( $this->_config['headers'][ $section ] ) ) {
$hc = $this->_config['headers'][ $section ];
if ( isset( $whitelist['ETag'] ) && $hc['etag'] ) {
$headers['ETag'] = '"' . @md5_file( $local_path ) . '"';
}
if ( $hc['expires'] ) {
$headers['Expires'] = Util_Content::http_date( time() + $hc['lifetime'] );
$expires_set = true;
}
$headers = array_merge( $headers, $hc['static'] );
}
return $headers;
}
/**
* Determines whether a file may be compressed using Gzip.
*
* @param string $file The file path.
*
* @return bool True if the file may be gzipped, otherwise false.
*/
public function _may_gzip( $file ) {
/**
* Remove query string
*/
$file = preg_replace( '~\?.*$~', '', $file );
/**
* Check by file extension
*/
if ( preg_match( '~\.(ico|js|css|xml|xsd|xsl|svg|htm|html|txt)$~i', $file ) ) {
return true;
}
return false;
}
/**
* Tests the configured domains for valid hostnames.
*
* @param string $error A reference to store the error message if any domain is invalid.
*
* @return bool True if all domains are valid, otherwise false.
*/
public function _test_domains( &$error ) {
$domains = $this->get_domains();
if ( ! count( $domains ) ) {
$error = 'Empty hostname / CNAME list.';
return false;
}
foreach ( $domains as $domain ) {
$_domains = array_map( 'trim', explode( ',', $domain ) );
foreach ( $_domains as $_domain ) {
$matches = null;
if ( preg_match( '~^([a-z0-9\-\.]*)~i', $_domain, $matches ) ) {
$hostname = $matches[1];
} else {
$hostname = $_domain;
}
if ( empty( $hostname ) ) {
continue;
}
if ( gethostbyname( $hostname ) === $hostname ) {
$error = sprintf( 'Unable to resolve hostname: %s.', $hostname );
return false;
}
}
}
return true;
}
/**
* Checks if a file is a CSS file.
*
* @param string $path The file path.
*
* @return bool True if the file is a CSS file, otherwise false.
*/
public function _is_css( $path ) {
return preg_match( '~[a-zA-Z0-9\-_]*(\.include\.[0-9]+)?\.css$~', $path );
}
/**
* Checks if a file is a JavaScript file.
*
* @param string $path The file path.
*
* @return bool True if the file is a JavaScript file, otherwise false.
*/
public function _is_js( $path ) {
return preg_match( '~([a-z0-9\-_]+(\.include\.[a-z0-9]+)\.js)$~', $path ) || preg_match( '~[\w\d\-_]+\.js~', $path );
}
/**
* Checks if a file is a JavaScript file that should be included in the body.
*
* @param string $path The file path.
*
* @return bool True if the file is a JavaScript file for the body, otherwise false.
*/
public function _is_js_body( $path ) {
return preg_match( '~[a-z0-9\-_]+(\.include-body\.[a-z0-9]+)\.js$~', $path );
}
/**
* Checks if a file is a JavaScript file that should be included in the footer.
*
* @param string $path The file path.
*
* @return bool True if the file is a JavaScript file for the footer, otherwise false.
*/
public function _is_js_footer( $path ) {
return preg_match( '~[a-z0-9\-_]+(\.include-footer\.[a-z0-9]+)\.js$~', $path );
}
/**
* Retrieves the domain for a specific file path from a list of domains.
*
* @param array $domains The list of domains.
* @param string $path The file path.
*
* @return string|false The selected domain or false if no domain is found.
*/
public function _get_domain( $domains, $path ) {
$count = count( $domains );
if ( isset( $domains['http_default'] ) ) {
--$count;
}
if ( isset( $domains['https_default'] ) ) {
--$count;
}
if ( $count ) {
/**
* Use for equal URLs same host to allow caching by browser
*/
$hash = $this->_get_hash( $path );
$domain = $domains[ $hash % $count ];
return $domain;
}
return false;
}
/**
* Generates a hash from a given key.
*
* @param string $key The key to hash.
*
* @return int The generated hash value.
*/
public function _get_hash( $key ) {
$hash = abs( crc32( $key ) );
return $hash;
}
/**
* Retrieves the scheme (HTTP or HTTPS) based on the configuration.
*
* @return string The scheme ('http' or 'https').
*/
public function _get_scheme() {
switch ( $this->_config['ssl'] ) {
default:
case 'auto':
$scheme = ( Util_Environment::is_https() ? 'https' : 'http' );
break;
case 'enabled':
$scheme = 'https';
break;
case 'disabled':
$scheme = 'http';
break;
case 'rejected':
$scheme = 'http';
break;
}
return $scheme;
}
/**
* Logs a message with local and remote file paths and an error.
*
* @param string $local_path The local file path.
* @param string $remote_path The remote file path.
* @param string $error The error message.
*
* @return int|false The number of bytes written to the log file, or false on failure.
*/
public function _log( $local_path, $remote_path, $error ) {
$data = sprintf( "[%s] [%s => %s] %s\n", gmdate( 'r' ), $local_path, $remote_path, $error );
$data = strtr( $data, '<>', '..' );
$filename = Util_Debug::log_filename( 'cdn' );
return @file_put_contents( $filename, $data, FILE_APPEND );
}
/**
* Handles errors by saving the error message.
*
* @param int $errno The error number.
* @param string $errstr The error message.
*
* @return bool Always returns false.
*/
public function _error_handler( $errno, $errstr ) {
$this->_last_error = $errstr;
return false;
}
/**
* Retrieves the last error message.
*
* @return string The last error message.
*/
public function _get_last_error() {
return $this->_last_error;
}
/**
* Sets a custom error handler.
*
* phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler
*
* @return void
*/
public function _set_error_handler() {
set_error_handler(
array(
$this,
'_error_handler',
)
);
}
/**
* Restores the default error handler.
*
* @return void
*/
public function _restore_error_handler() {
restore_error_handler();
}
/**
* Retrieves the header support status.
*
* @return string The header support status (W3TC_CDN_HEADER_NONE).
*/
public function headers_support() {
return W3TC_CDN_HEADER_NONE;
}
}

View File

@@ -0,0 +1,477 @@
<?php
/**
* File: CdnEngine_CloudFront.php
*
* @package W3TC
*/
namespace W3TC;
if ( ! defined( 'W3TC_SKIPLIB_AWS' ) ) {
require_once W3TC_DIR . '/vendor/autoload.php';
}
/**
* Class CdnEngine_CloudFront
*
* Amazon CloudFront (S3 origin) CDN engine
*
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
*/
class CdnEngine_CloudFront extends CdnEngine_Base {
/**
* CDN Engine S3 object
*
* @var CdnEngine_S3
*/
private $s3;
/**
* CloudFront Client API object
*
* @var CloudFrontClient
*/
private $api;
/**
* Constructs the CDN Engine CloudFront instance.
*
* @param array $config Configuration settings.
*
* @return void
*/
public function __construct( $config = array() ) {
$config = array_merge(
array(
'id' => '',
),
$config
);
parent::__construct( $config );
$this->s3 = new CdnEngine_S3( $config );
}
/**
* Initializes the CloudFront API client.
*
* @return bool Returns true if the initialization is successful.
*/
public function _init() {
if ( ! is_null( $this->api ) ) {
return;
}
if ( empty( $this->_config['key'] ) && empty( $this->_config['secret'] ) ) {
$credentials = \Aws\Credentials\CredentialProvider::defaultProvider();
} else {
$credentials = new \Aws\Credentials\Credentials(
$this->_config['key'],
$this->_config['secret']
);
}
$this->api = new \Aws\CloudFront\CloudFrontClient(
array(
'credentials' => $credentials,
'region' => $this->_config['bucket_location'],
'version' => '2018-11-05',
)
);
return true;
}
/**
* Formats the URL based on the provided path.
*
* @param string $path The file path to format.
*
* @return string|false Returns the formatted URL or false if the domain is not found.
*/
public function _format_url( $path ) {
$domain = $this->get_domain( $path );
if ( $domain ) {
$scheme = $this->_get_scheme();
// it does not support '+', requires '%2B'.
$path = str_replace( '+', '%2B', $path );
$url = sprintf( '%s://%s/%s', $scheme, $domain, $path );
return $url;
}
return false;
}
/**
* Uploads files to the CloudFront CDN.
*
* @param array $files Files to upload.
* @param array $results Reference to store the results of the upload.
* @param bool $force_rewrite Whether to force file overwrite.
* @param int $timeout_time Timeout duration for the upload.
*
* @return bool Returns true if upload is successful, false otherwise.
*/
public function upload( $files, &$results, $force_rewrite = false, $timeout_time = null ) {
return $this->s3->upload( $files, $results, $force_rewrite, $timeout_time );
}
/**
* Deletes files from the CloudFront CDN.
*
* @param array $files Files to delete.
* @param array $results Reference to store the results of the delete operation.
*
* @return bool Returns true if delete is successful, false otherwise.
*/
public function delete( $files, &$results ) {
return $this->s3->delete( $files, $results );
}
/**
* Purges files from the CloudFront CDN and uploads them to S3.
*
* @param array $files Files to purge.
* @param array $results Reference to store the results of the purge operation.
*
* @return bool Returns true if purge is successful, false otherwise.
*/
public function purge( $files, &$results ) {
if ( ! $this->s3->upload( $files, $results, true ) ) {
return false;
}
try {
$this->_init();
$dist = $this->_get_distribution();
} catch ( \Exception $ex ) {
$results = $this->_get_results( $files, W3TC_CDN_RESULT_HALT, $ex->getMessage() );
return false;
}
$paths = array();
foreach ( $files as $file ) {
$remote_file = $file['remote_path'];
$paths[] = '/' . $remote_file;
}
try {
$invalidation = $this->api->createInvalidation(
array(
'DistributionId' => $dist['Id'],
'InvalidationBatch' => array(
'CallerReference' => 'w3tc-' . microtime(),
'Paths' => array(
'Items' => $paths,
'Quantity' => count( $paths ),
),
),
)
);
} catch ( \Exception $ex ) {
$results = $this->_get_results(
$files,
W3TC_CDN_RESULT_HALT,
sprintf( 'Unable to create invalidation batch (%s).', $ex->getMessage() )
);
return false;
}
$results = $this->_get_results( $files, W3TC_CDN_RESULT_OK, 'OK' );
return true;
}
/**
* Retrieves the region based on the bucket location.
*
* @return string The region for the CDN.
*/
public function get_region() {
switch ( $this->_config['bucket_location'] ) {
case 'us-east-1':
$region = '';
break;
case 'us-east-1-e':
$region = 'us-east-1.';
break;
default:
$region = $this->_config['bucket_location'] . '.';
break;
}
return $region;
}
/**
* Gets the origin URL for the CloudFront CDN.
*
* @return string The origin URL.
*/
public function _get_origin() {
return sprintf( '%1$s.s3.%2$samazonaws.com', $this->_config['bucket'], $this->get_region() );
}
/**
* Retrieves the list of domains for the CloudFront distribution.
*
* @return array An array of domains associated with the CDN.
*/
public function get_domains() {
if ( ! empty( $this->_config['cname'] ) ) {
return (array) $this->_config['cname'];
} elseif ( ! empty( $this->_config['id'] ) ) {
$domain = sprintf( '%s.cloudfront.net', $this->_config['id'] );
return array(
$domain,
);
}
return array();
}
/**
* Tests the connection and configuration of the CloudFront CDN.
*
* @param string $error Reference to store any error message if the test fails.
*
* @return bool Returns true if the test passes, false otherwise.
*/
public function test( &$error ) {
$this->_init();
if ( ! $this->s3->test( $error ) ) {
return false;
}
/**
* Search active CF distribution
*/
$dists = $this->api->listDistributions();
if ( ! isset( $dists['DistributionList']['Items'] ) ) {
$error = 'Unable to list distributions.';
return false;
}
if ( ! count( $dists['DistributionList']['Items'] ) ) {
$error = 'No distributions found.';
return false;
}
$dist = $this->_get_distribution( $dists );
if ( 'Deployed' !== $dist['Status'] ) {
$error = sprintf( 'Distribution status is not Deployed, but "%s".', $dist['Status'] );
return false;
}
if ( ! $dist['Enabled'] ) {
$error = sprintf( 'Distribution for origin "%s" is disabled.', $this->_get_origin() );
return false;
}
if ( ! empty( $this->_config['cname'] ) ) {
$domains = (array) $this->_config['cname'];
$cnames = ( isset( $dist['Aliases']['Items'] ) ? (array) $dist['Aliases']['Items'] : array() );
foreach ( $domains as $domain ) {
$_domains = array_map( 'trim', explode( ',', $domain ) );
foreach ( $_domains as $_domain ) {
if ( ! in_array( $_domain, $cnames, true ) ) {
$error = sprintf( 'Domain name %s is not in distribution <acronym title="Canonical Name">CNAME</acronym> list.', $_domain );
return false;
}
}
}
} elseif ( ! empty( $this->_config['id'] ) ) {
$domain = $this->get_domain();
if ( $domain !== $dist['DomainName'] ) {
$error = sprintf( 'Distribution domain name mismatch (%s != %s).', $domain, $dist['DomainName'] );
return false;
}
}
return true;
}
/**
* Creates a CloudFront distribution container and returns the container ID.
*
* This method initializes the container, creates a CloudFront distribution using the provided
* configuration, and extracts the domain name from the distribution result. It handles CNAMEs
* and origins and returns the distribution's container ID based on the CloudFront domain.
*
* @return string The container ID associated with the CloudFront distribution.
*
* @throws \Exception If unable to create the distribution for the origin.
*/
public function create_container() {
$this->_init();
$this->s3->create_container();
// plugin cant set CNAMEs list since it CloudFront requires certificate to be specified associated with it.
$cnames = array();
// make distibution.
$origin_domain = $this->_get_origin();
try {
$result = $this->api->createDistribution(
array(
'DistributionConfig' => array(
'CallerReference' => $origin_domain,
'Comment' => 'Created by W3-Total-Cache',
'DefaultCacheBehavior' => array(
'AllowedMethods' => array(
'CachedMethods' => array(
'Items' => array( 'HEAD', 'GET' ),
'Quantity' => 2,
),
'Items' => array( 'HEAD', 'GET' ),
'Quantity' => 2,
),
'Compress' => true,
'DefaultTTL' => 86400,
'FieldLevelEncryptionId' => '',
'ForwardedValues' => array(
'Cookies' => array(
'Forward' => 'none',
),
'Headers' => array(
'Quantity' => 0,
),
'QueryString' => false,
'QueryStringCacheKeys' => array(
'Quantity' => 0,
),
),
'LambdaFunctionAssociations' => array( 'Quantity' => 0 ),
'MinTTL' => 0,
'SmoothStreaming' => false,
'TargetOriginId' => $origin_domain,
'TrustedSigners' => array(
'Enabled' => false,
'Quantity' => 0,
),
'ViewerProtocolPolicy' => 'allow-all',
),
'Enabled' => true,
'Origins' => array(
'Items' => array(
array(
'DomainName' => $origin_domain,
'Id' => $origin_domain,
'OriginPath' => '',
'CustomHeaders' => array( 'Quantity' => 0 ),
'S3OriginConfig' => array(
'OriginAccessIdentity' => '',
),
),
),
'Quantity' => 1,
),
'Aliases' => array(
'Items' => $cnames,
'Quantity' => count( $cnames ),
),
),
)
);
// extract domain dynamic part stored later in a config.
$domain = $result['Distribution']['DomainName'];
$container_id = '';
if ( preg_match( '~^(.+)\.cloudfront\.net$~', $domain, $matches ) ) {
$container_id = $matches[1];
}
return $container_id;
} catch ( \Exception $ex ) {
throw new \Exception(
\esc_html(
sprintf(
// Translators: 1 Origin domain name, 2 Error message.
\__( 'Unable to create distribution for origin %1$s: %2$s', 'w3-total-cache' ),
$origin_domain,
$ex->getMessage()
)
)
);
}
}
/**
* Retrieves the "via" information for the CloudFront distribution.
*
* This method fetches the domain and formats the "via" string in the format:
* 'Amazon Web Services: CloudFront: <domain>', returning 'N/A' if the domain is not set.
*
* @return string The formatted "via" information.
*/
public function get_via() {
$domain = $this->get_domain();
$via = ( $domain ? $domain : 'N/A' );
return sprintf( 'Amazon Web Services: CloudFront: %s', $via );
}
/**
* Retrieves the CloudFront distribution based on the origin.
*
* This method checks for an existing distribution matching the provided origin. If no
* distribution is found, it throws an exception. It can also accept an optional parameter
* to provide a list of distributions.
*
* @param array|null $dists Optional. A list of distributions to search through. If null,
* the list is fetched from the CloudFront API.
*
* @return array The distribution details associated with the origin.
*
* @throws \Exception If no distribution is found for the origin.
*/
private function _get_distribution( $dists = null ) {
if ( is_null( $dists ) ) {
$dists = $this->api->listDistributions();
}
if ( ! isset( $dists['DistributionList']['Items'] ) || ! count( $dists['DistributionList']['Items'] ) ) {
throw new \Exception( \esc_html__( 'No distributions found.', 'w3-total-cache' ) );
}
$dist = false;
$origin = $this->_get_origin();
$items = $dists['DistributionList']['Items'];
foreach ( $items as $dist ) {
if ( isset( $dist['Origins']['Items'] ) ) {
foreach ( $dist['Origins']['Items'] as $o ) {
if ( isset( $o['DomainName'] ) && $o['DomainName'] === $origin ) {
return $dist;
}
}
}
}
throw new \Exception(
\esc_html(
sprintf(
// Translators: 1 Origin name.
\__( 'Distribution for origin "%1$s" not found.', 'w3-total-cache' ),
$origin
)
)
);
}
}

View File

@@ -0,0 +1,788 @@
<?php
/**
* File: CdnEngine_CloudFront.php
*
* @package W3TC
*/
namespace W3TC;
define( 'W3TC_CDN_FTP_CONNECT_TIMEOUT', 30 );
/**
* Class CdnEngine_Ftp
*
* W3 CDN FTP Class
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
* phpcs:disable WordPress.WP.AlternativeFunctions
*/
class CdnEngine_Ftp extends CdnEngine_Base {
/**
* FTP resource
*
* @var resource
*/
private $_ftp = null;
/**
* Class constructor for initializing FTP/SFTP connection settings.
*
* @param array $config Optional configuration array to override default values.
* Keys include 'host', 'type', 'user', 'pass', 'default_keys',
* 'pubkey', 'privkey', 'path', 'pasv', 'domain', and 'docroot'.
*/
public function __construct( $config = array() ) {
$config = array_merge(
array(
'host' => '',
'type' => '',
'user' => '',
'pass' => '',
'default_keys' => false,
'pubkey' => '',
'privkey' => '',
'path' => '',
'pasv' => false,
'domain' => array(),
'docroot' => '',
),
$config
);
list( $ip, $port ) = Util_Content::endpoint_to_host_port( $config['host'], 21 );
$config['host'] = $ip;
$config['port'] = $port;
if ( 'sftp' === $config['type'] && $config['default_keys'] ) {
$home = isset( $_SERVER['HOME'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HOME'] ) ) : '';
$config['pubkey'] = $home . '/.ssh/id_rsa.pub';
$config['privkey'] = $home . '/.ssh/id_rsa';
}
parent::__construct( $config );
}
/**
* Establishes a connection to the FTP/SFTP server.
*
* Attempts to connect to the server using the configuration provided in the class.
* Handles both FTP and SFTP types with error handling and connection setup.
*
* @param string $error Reference to a variable to capture error messages.
*
* @return bool Returns true on successful connection, false on failure.
*/
public function _connect( &$error ) {
if ( empty( $this->_config['host'] ) ) {
$error = 'Empty host.';
return false;
}
$this->_set_error_handler();
if ( 'sftp' === $this->_config['type'] ) {
if ( ! function_exists( 'ssh2_connect' ) ) {
$error = sprintf( 'Missing required php-ssh2 extension.' );
$this->_restore_error_handler();
$this->_disconnect();
return false;
}
$this->_ftp = @ssh2_connect( $this->_config['host'], (int) $this->_config['port'] );
return $this->_connect_sftp( $error );
}
if ( 'ftps' === $this->_config['type'] ) {
$this->_ftp = @ftp_ssl_connect( $this->_config['host'], (int) $this->_config['port'], W3TC_CDN_FTP_CONNECT_TIMEOUT );
} else {
$this->_ftp = @ftp_connect( $this->_config['host'], (int) $this->_config['port'], W3TC_CDN_FTP_CONNECT_TIMEOUT );
}
if ( ! $this->_ftp ) {
$error = sprintf(
'Unable to connect to %s:%d (%s).',
$this->_config['host'],
$this->_config['port'],
$this->_get_last_error()
);
$this->_restore_error_handler();
return false;
}
if ( ! @ftp_login( $this->_ftp, $this->_config['user'], $this->_config['pass'] ) ) {
$error = sprintf( 'Incorrect login or password (%s).', $this->_get_last_error() );
$this->_restore_error_handler();
$this->_disconnect();
return false;
}
if ( ! @ftp_pasv( $this->_ftp, $this->_config['pasv'] ) ) {
$error = sprintf( 'Unable to change mode to passive (%s).', $this->_get_last_error() );
$this->_restore_error_handler();
$this->_disconnect();
return false;
}
if ( ! empty( $this->_config['path'] ) && ! @ftp_chdir( $this->_ftp, $this->_config['path'] ) ) {
$error = sprintf( 'Unable to change directory to: %s (%s).', $this->_config['path'], $this->_get_last_error() );
$this->_restore_error_handler();
$this->_disconnect();
return false;
}
$this->_restore_error_handler();
return true;
}
/**
* Establishes an SFTP connection and authenticates using the provided credentials.
*
* This method is used specifically for handling SFTP connections, including both
* public key and password-based authentication.
*
* @param string $error Reference to a variable to capture error messages.
*
* @return bool Returns true on successful connection, false on failure.
*/
public function _connect_sftp( &$error ) {
if ( is_file( $this->_config['pass'] ) ) {
if ( ! @ssh2_auth_pubkey_file( $this->_ftp, $this->_config['user'], $this->_config['pubkey'], $this->_config['privkey'], $this->_config['pass'] ) ) {
$error = sprintf( 'Public key authentication failed (%s).', $this->_get_last_error() );
$this->_restore_error_handler();
$this->_disconnect();
return false;
}
} elseif ( ! @ssh2_auth_password( $this->_ftp, $this->_config['user'], $this->_config['pass'] ) ) {
$error = sprintf( 'Incorrect login or password (%s).', $this->_get_last_error() );
$this->_restore_error_handler();
$this->_disconnect();
return false;
}
if ( ! empty( $this->_config['path'] ) && ! @ssh2_exec( $this->_ftp, 'cd ' . $this->_config['path'] ) ) {
$error = sprintf( 'Unable to change directory to: %s (%s).', $this->_config['path'], $this->_get_last_error() );
$this->_restore_error_handler();
$this->_disconnect();
return false;
}
$this->_restore_error_handler();
return true;
}
/**
* Closes the current FTP/SFTP connection.
*
* This method properly terminates the connection to the server based on the connection type.
*
* @return void
*/
public function _disconnect() {
if ( 'sftp' === $this->_config['type'] ) {
if ( function_exists( 'ssh2_connect' ) ) {
@ssh2_exec( $this->_ftp, 'echo "EXITING" && exit;' );
$this->_ftp = null;
}
} else {
@ftp_close( $this->_ftp );
}
}
/**
* Sends an MDTM (Modification Time) command to the FTP server to update the modification time of a file.
*
* This method is used for updating the timestamp of a remote file.
*
* @param string $remote_file The remote file path to modify.
* @param int $mtime The modification time to set for the file.
*
* @return array Returns the raw response from the FTP server.
*/
public function _mdtm( $remote_file, $mtime ) {
$command = sprintf( 'MDTM %s %s', gmdate( 'YmdHis', $mtime ), $remote_file );
return @ftp_raw( $this->_ftp, $command );
}
/**
* Uploads files to the FTP/SFTP server.
*
* Handles the upload of files, including checking for existing files and handling
* overwrites, directory creation, and error logging.
*
* @param array $files Array of files to upload, with 'local_path' and 'remote_path' for each file.
* @param array $results Array to capture the results of the upload attempt.
* @param bool $force_rewrite Whether to force overwriting files even if they are up-to-date.
* @param int|null $timeout_time Optional timeout time to stop the process if exceeded.
*
* @return bool Returns true if the upload is successful, false if there were errors.
*/
public function upload( $files, &$results, $force_rewrite = false, $timeout_time = null ) {
$error = null;
if ( ! $this->_connect( $error ) ) {
$results = $this->_get_results( $files, W3TC_CDN_RESULT_HALT, $error );
return false;
}
$this->_set_error_handler();
if ( 'sftp' === $this->_config['type'] ) {
return $this->_upload_sftp( $files, $results, $force_rewrite, $timeout_time );
}
$home = @ftp_pwd( $this->_ftp );
if ( false === $home ) {
$results = $this->_get_results( $files, W3TC_CDN_RESULT_HALT, sprintf( 'Unable to get current directory (%s).', $this->_get_last_error() ) );
$this->_restore_error_handler();
$this->_disconnect();
return false;
}
foreach ( $files as $file ) {
$local_path = $file['local_path'];
$remote_path = $file['remote_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';
}
}
if ( ! file_exists( $local_path ) ) {
$results[] = $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_ERROR,
'Source file not found.',
$file
);
continue;
}
@ftp_chdir( $this->_ftp, $home );
$remote_dir = dirname( $remote_path );
$remote_dirs = preg_split( '~\\/+~', $remote_dir );
foreach ( $remote_dirs as $dir ) {
if ( ! @ftp_chdir( $this->_ftp, $dir ) ) {
if ( ! @ftp_mkdir( $this->_ftp, $dir ) ) {
$results[] = $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_ERROR,
sprintf( 'Unable to create directory (%s).', $this->_get_last_error() ),
$file
);
continue 2;
}
if ( ! @ftp_chdir( $this->_ftp, $dir ) ) {
$results[] = $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_ERROR,
sprintf( 'Unable to change directory (%s).', $this->_get_last_error() ),
$file
);
continue 2;
}
}
}
// basename cannot be used, kills chinese chars and similar characters.
$remote_file = substr( $remote_path, strrpos( $remote_path, '/' ) + 1 );
$mtime = @filemtime( $local_path );
if ( ! $force_rewrite ) {
$size = @filesize( $local_path );
$ftp_size = @ftp_size( $this->_ftp, $remote_file );
$ftp_mtime = @ftp_mdtm( $this->_ftp, $remote_file );
if ( $size === $ftp_size && $mtime === $ftp_mtime ) {
$results[] = $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_OK,
'File up-to-date.',
$file
);
continue;
}
}
$result = @ftp_put( $this->_ftp, $remote_file, $local_path, FTP_BINARY );
if ( $result ) {
$this->_mdtm( $remote_file, $mtime );
$results[] = $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_OK,
'OK',
$file
);
} else {
$results[] = $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_ERROR,
sprintf( 'Unable to upload file (%s).', $this->_get_last_error() ),
$file
);
}
}
$this->_restore_error_handler();
$this->_disconnect();
return ! $this->_is_error( $results );
}
/**
* Handles the SFTP-specific file upload process.
*
* This method is called when the connection type is SFTP, and is responsible for
* uploading files using the SFTP protocol, including directory creation and file transfer.
*
* @param array $files Array of files to upload, with 'local_path' and 'remote_path' for each file.
* @param array $results Array to capture the results of the upload attempt.
* @param bool $force_rewrite Whether to force overwriting files even if they are up-to-date.
* @param int|null $timeout_time Optional timeout time to stop the process if exceeded.
*
* @return string Returns 'timeout' if the process times out, otherwise no return value on success.
*/
public function _upload_sftp( $files, $results, $force_rewrite, $timeout_time ) {
$sftp = ssh2_sftp( $this->_ftp );
foreach ( $files as $file ) {
$local_path = $file['local_path'];
$remote_path = $file['remote_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';
}
}
if ( ! file_exists( $local_path ) ) {
$results[] = $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_ERROR,
'Source file not found.',
$file
);
continue;
}
$remote_dir = dirname( $remote_path );
if ( ! @file_exists( 'ssh2.sftp://' . intval( $sftp ) . $remote_dir ) ) {
if ( ! @ssh2_sftp_mkdir( $sftp, $remote_dir, null, true ) ) {
$results[] = $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_ERROR,
sprintf( 'Unable to create directory (%s).', $this->_get_last_error() ),
$file
);
continue;
}
}
$mtime = @filemtime( $local_path );
if ( ! $force_rewrite ) {
$size = @filesize( $local_path );
$statinfo = @ssh2_sftp_stat( $sftp, $remote_path );
if ( $size === $statinfo['size'] && $mtime === $statinfo['mtime'] ) {
$results[] = $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_OK,
'File up-to-date.',
$file
);
continue;
}
}
$result = @ssh2_scp_send( $this->_ftp, $local_path, $remote_path );
if ( $result ) {
$results[] = $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_OK,
'OK',
$file
);
} else {
$results[] = $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_ERROR,
sprintf( 'Unable to upload file (%s).', $this->_get_last_error() ),
$file
);
}
}
$this->_restore_error_handler();
$this->_disconnect();
return ! $this->_is_error( $results );
}
/**
* Deletes the specified files from the remote FTP/SFTP server.
*
* This method connects to the remote server, attempts to delete each file, and then tries to remove
* the directories associated with those files. It collects the results of the delete operations
* and stores them in the results array.
*
* @param array $files An array of file data, where each entry contains the local and remote paths of the files.
* @param array $results An array that will be populated with the results of each delete operation.
*
* @return bool Returns true if all files and directories were deleted successfully, otherwise false.
*/
public function delete( $files, &$results ) {
$error = null;
if ( ! $this->_connect( $error ) ) {
$results = $this->_get_results( $files, W3TC_CDN_RESULT_HALT, $error );
return false;
}
$this->_set_error_handler();
foreach ( $files as $file ) {
$local_path = $file['local_path'];
$remote_path = $file['remote_path'];
if ( 'sftp' === $this->_config['type'] ) {
$sftp = @ssh2_sftp( $this->_ftp );
$result = @ssh2_sftp_unlink( $sftp, $remote_path );
} else {
$result = @ftp_delete( $this->_ftp, $remote_path );
}
if ( $result ) {
$results[] = $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_OK,
'OK',
$file
);
} else {
$results[] = $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_ERROR,
sprintf( 'Unable to delete file (%s).', $this->_get_last_error() ),
$file
);
}
while ( true ) {
$remote_path = dirname( $remote_path );
if ( '.' === $remote_path ) {
break;
}
if ( 'sftp' === $this->_config['type'] && ! @ssh2_sftp_rmdir( $sftp, $remote_path ) ) {
break;
} elseif ( ! @ftp_rmdir( $this->_ftp, $remote_path ) ) {
break;
}
}
}
$this->_restore_error_handler();
$this->_disconnect();
return ! $this->_is_error( $results );
}
/**
* Tests the FTP/SFTP connection and upload/download functionality.
*
* This method tests the FTP/SFTP connection by performing a series of file operations including
* creating a temporary directory, uploading a test file, deleting it, and cleaning up. If any step
* fails, an error message is returned via the $error parameter.
*
* @param string $error A reference to a variable that will be populated with an error message, if any.
*
* @return bool Returns true if the test was successful, otherwise false.
*/
public function test( &$error ) {
if ( ! parent::test( $error ) ) {
return false;
}
if ( 'sftp' === $this->_config['type'] ) {
return $this->_test_sftp( $error );
}
$rand = md5( time() );
$tmp_dir = 'test_dir_' . $rand;
$tmp_file = 'test_file_' . $rand;
$tmp_path = W3TC_CACHE_TMP_DIR . '/' . $tmp_file;
if ( ! @file_put_contents( $tmp_path, $rand ) ) {
$error = sprintf( 'Unable to create file: %s.', $tmp_path );
return false;
}
if ( ! $this->_connect( $error ) ) {
return false;
}
$this->_set_error_handler();
if ( ! @ftp_mkdir( $this->_ftp, $tmp_dir ) ) {
$error = sprintf( 'Unable to make directory: %s (%s).', $tmp_dir, $this->_get_last_error() );
@unlink( $tmp_path );
$this->_restore_error_handler();
$this->_disconnect();
return false;
}
if ( file_exists( $this->_config['docroot'] . '/' . $tmp_dir ) ) {
$error = sprintf( 'Test directory was made in your site root, not on separate FTP host or path. Change path or FTP information: %s.', $tmp_dir );
@unlink( $tmp_path );
@ftp_rmdir( $this->_ftp, $tmp_dir );
$this->_restore_error_handler();
$this->_disconnect();
return false;
}
if ( ! @ftp_chdir( $this->_ftp, $tmp_dir ) ) {
$error = sprintf( 'Unable to change directory to: %s (%s).', $tmp_dir, $this->_get_last_error() );
@unlink( $tmp_path );
@ftp_rmdir( $this->_ftp, $tmp_dir );
$this->_restore_error_handler();
$this->_disconnect();
return false;
}
if ( ! @ftp_put( $this->_ftp, $tmp_file, $tmp_path, FTP_BINARY ) ) {
$error = sprintf( 'Unable to upload file: %s (%s).', $tmp_path, $this->_get_last_error() );
@unlink( $tmp_path );
@ftp_cdup( $this->_ftp );
@ftp_rmdir( $this->_ftp, $tmp_dir );
$this->_restore_error_handler();
$this->_disconnect();
return false;
}
@unlink( $tmp_path );
if ( ! @ftp_delete( $this->_ftp, $tmp_file ) ) {
$error = sprintf( 'Unable to delete file: %s (%s).', $tmp_path, $this->_get_last_error() );
@ftp_cdup( $this->_ftp );
@ftp_rmdir( $this->_ftp, $tmp_dir );
$this->_restore_error_handler();
$this->_disconnect();
return false;
}
@ftp_cdup( $this->_ftp );
if ( ! @ftp_rmdir( $this->_ftp, $tmp_dir ) ) {
$error = sprintf( 'Unable to remove directory: %s (%s).', $tmp_dir, $this->_get_last_error() );
$this->_restore_error_handler();
$this->_disconnect();
return false;
}
$this->_restore_error_handler();
$this->_disconnect();
return true;
}
/**
* Tests SFTP-specific functionality, including directory creation and file upload/delete.
*
* This method is specifically designed to test SFTP connections by creating directories, uploading
* a test file, and performing file deletions using SFTP commands. If any operation fails, an error message
* is returned via the $error parameter.
*
* @param string $error A reference to a variable that will be populated with an error message, if any.
*
* @return bool Returns true if the SFTP test was successful, otherwise false.
*/
public function _test_sftp( &$error ) {
$rand = md5( time() );
$tmp_dir = 'test_dir_' . $rand;
$tmp_file = 'test_file_' . $rand;
$local_path = W3TC_CACHE_TMP_DIR . '/' . $tmp_file;
$remote_path = $tmp_dir . '/' . $tmp_file;
if ( ! @file_put_contents( $local_path, $rand ) ) {
$error = sprintf( 'Unable to create file: %s.', $local_path );
return false;
}
if ( ! $this->_connect( $error ) ) {
return false;
}
$sftp = @ssh2_sftp( $this->_ftp );
$this->_set_error_handler();
if ( ! @ssh2_sftp_mkdir( $sftp, $tmp_dir ) ) {
$error = sprintf( 'Unable to make directory: %s (%s).', $tmp_dir, $this->_get_last_error() );
@unlink( $local_path );
$this->_restore_error_handler();
$this->_disconnect();
return false;
}
if ( file_exists( $this->_config['docroot'] . '/' . $tmp_dir ) ) {
$error = sprintf( 'Test directory was made in your site root, not on separate FTP host or path. Change path or FTP information: %s.', $tmp_dir );
@unlink( $local_path );
@ssh2_sftp_rmdir( $sftp, $tmp_dir );
$this->_restore_error_handler();
$this->_disconnect();
return false;
}
if ( ! @ssh2_scp_send( $this->_ftp, $local_path, $remote_path ) ) {
$error = sprintf( 'Unable to upload file: %s (%s).', $local_path, $this->_get_last_error() );
@unlink( $local_path );
@ssh2_sftp_rmdir( $sftp, $tmp_dir );
$this->_restore_error_handler();
$this->_disconnect();
return false;
}
@unlink( $local_path );
if ( ! @ssh2_sftp_unlink( $sftp, $remote_path ) ) {
$error = sprintf( 'Unable to delete file: %s (%s).', $local_path, $this->_get_last_error() );
@ssh2_sftp_rmdir( $sftp, $tmp_dir );
$this->_restore_error_handler();
$this->_disconnect();
return false;
}
if ( ! @ssh2_sftp_rmdir( $sftp, $tmp_dir ) ) {
$error = sprintf( 'Unable to remove directory: %s (%s).', $tmp_dir, $this->_get_last_error() );
$this->_restore_error_handler();
$this->_disconnect();
return false;
}
$this->_restore_error_handler();
$this->_disconnect();
return true;
}
/**
* Retrieves the domains configured for the CDN.
*
* This method returns the domains associated with the CDN configuration, or an empty array if no
* domains are configured.
*
* @return array An array of domain names.
*/
public function get_domains() {
if ( ! empty( $this->_config['domain'] ) ) {
return (array) $this->_config['domain'];
}
return array();
}
/**
* Returns the headers support level for the CDN.
*
* This method returns the type of header mirroring support available for the CDN configuration.
*
* @return string The header mirroring type supported by the CDN.
*/
public function headers_support() {
return W3TC_CDN_HEADER_MIRRORING;
}
}

View File

@@ -0,0 +1,849 @@
<?php
/**
* File: CdnEngine_GoogleDrive.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class CdnEngine_GoogleDrive
*
* Google drive engine
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
* phpcs:disable WordPress.PHP.DiscouragedPHPFunctions
* phpcs:disable WordPress.WP.AlternativeFunctions
* phpcs:disable WordPress.DB.DirectDatabaseQuery
* phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
*/
class CdnEngine_GoogleDrive extends CdnEngine_Base {
/**
* Client ID
*
* @var string
*/
private $_client_id;
/**
* Refresh token
*
* @var string
*/
private $_refresh_token;
/**
* Root folder ID
*
* @var string
*/
private $_root_folder_id;
/**
* Root URL
*
* @var string
*/
private $_root_url;
/**
* Google Service Drive object
*
* @var W3TCG_Google_Service_Drive
*/
private $_service;
/**
* Tablename pathmap
*
* @var string
*/
private $_tablename_pathmap;
/**
* Callback function to handle the updated access token.
*
* This callback is invoked with the new access token whenever the token is refreshed.
*
* @var callable
*/
private $_new_access_token_callback;
/**
* Constructor to initialize the Google Drive CDN engine.
*
* @param array $config {
* Configuration options for the Google Drive CDN engine.
*
* @type string $client_id The client ID for the Google Drive API.
* @type string $refresh_token The refresh token for authentication.
* @type string $root_folder_id The root folder ID for the Google Drive.
* @type string $root_url The root URL for the Google Drive CDN.
* @type callable $new_access_token_callback Callback function for new access token.
* @type string $access_token The access token for authentication.
* }
*/
public function __construct( $config = array() ) {
parent::__construct( $config );
$this->_client_id = $config['client_id'];
$this->_refresh_token = $config['refresh_token'];
$this->_root_folder_id = $config['root_folder_id'];
$this->_root_url = rtrim( $config['root_url'], '/' ) . '/';
$this->_new_access_token_callback = $config['new_access_token_callback'];
global $wpdb;
$this->_tablename_pathmap = $wpdb->base_prefix . W3TC_CDN_TABLE_PATHMAP;
try {
$this->_init_service( $config['access_token'] );
} catch ( \Exception $e ) {
$this->_service = null;
}
}
/**
* Initializes the Google Drive service with the provided access token.
*
* @param string $access_token The access token used to authenticate with the Google Drive API.
*
* @throws \Exception If the client ID or access token is missing, or if the service initialization fails.
*/
private function _init_service( $access_token ) {
if ( empty( $this->_client_id ) || empty( $access_token ) ) {
throw new \Exception( \esc_html__( 'Service not configured.', 'w3-total-cache' ) );
}
$client = new \W3TCG_Google_Client();
$client->setClientId( $this->_client_id );
$client->setAccessToken( $access_token );
$this->_service = new \W3TCG_Google_Service_Drive( $client );
}
/**
* Refreshes the access token using the stored refresh token.
*
* @throws \Exception If the refresh request fails or returns an error.
*/
private function _refresh_token() {
$result = wp_remote_post(
W3TC_GOOGLE_DRIVE_AUTHORIZE_URL,
array(
'body' => array(
'client_id' => $this->_client_id,
'refresh_token' => $this->_refresh_token,
),
)
);
if ( is_wp_error( $result ) ) {
throw new \Exception( esc_html( $result ) );
} elseif ( 200 !== (int) $result['response']['code'] ) {
throw new \Exception( wp_kses_post( $result['body'] ) );
}
$access_token = $result['body'];
call_user_func( $this->_new_access_token_callback, $access_token );
$this->_init_service( $access_token );
}
/**
* Uploads files to Google Drive.
*
* @param array $files Array of file descriptors to upload. Each descriptor must contain the 'local_path' and 'remote_path' at a minimum.
* @param array $results Reference to an array where the upload results will be stored.
* @param bool $force_rewrite Whether to forcefully overwrite existing files on Google Drive (default: false).
* @param int $timeout_time Optional timeout time in seconds for the upload operation.
*
* @return bool|string True if the upload was successful, or 'timeout' if the upload timed out.
*/
public function upload( $files, &$results, $force_rewrite = false, $timeout_time = null ) {
if ( is_null( $this->_service ) ) {
return false;
}
$allow_refresh_token = true;
$result = true;
$files_chunks = array_chunk( $files, 20 );
foreach ( $files_chunks as $files_chunk ) {
$r = $this->_upload_chunk(
$files_chunk,
$results,
$force_rewrite,
$timeout_time,
$allow_refresh_token
);
if ( 'refresh_required' === $r ) {
$allow_refresh_token = false;
$this->_refresh_token();
$r = $this->_upload_chunk(
$files_chunk,
$results,
$force_rewrite,
$timeout_time,
$allow_refresh_token
);
}
if ( 'success' !== $r ) {
$result = false;
}
if ( 'timeout' === $r ) {
return 'timeout';
}
}
return $result;
}
/**
* Converts file properties to a Google Drive path.
*
* @param object $file The file object containing properties to convert.
*
* @return string|null The constructed path or null if no valid path properties are found.
*/
private function _properties_to_path( $file ) {
$path_pieces = array();
foreach ( $file->properties as $p ) {
$k = ( 'path' === $p->key ) ? 'path1' : $p->key;
if ( ! preg_match( '/^path[0-9]+$/', $k ) ) {
continue;
}
$path_pieces[ $k ] = $p->value;
}
if ( 0 === count( $path_pieces ) ) {
return null;
}
ksort( $path_pieces );
return join( $path_pieces );
}
/**
* Converts a path string into an array of Google Drive properties.
*
* From google drive api docs:
* Maximum of 124 bytes size per property (including both key and value) string in UTF-8 encoding.
* Maximum of 30 private properties per file from any one application.
*
* @param string $path The path to convert into Google Drive properties.
*
* @return array An array of Google Drive property objects representing the path.
*/
private function _path_to_properties( $path ) {
$chunks = str_split( $path, 55 );
$properties = array();
$i = 1;
foreach ( $chunks as $chunk ) {
$p = new \W3TCG_Google_Service_Drive_Property();
$p->key = 'path' . $i;
$p->value = $chunk;
$properties[] = $p;
++$i;
}
return $properties;
}
/**
* Uploads a chunk of files to Google Drive.
*
* @param array $files Array of file descriptors to upload in the current chunk.
* @param array $results Reference to an array where the upload results will be stored.
* @param bool $force_rewrite Whether to forcefully overwrite existing files on Google Drive.
* @param int $timeout_time Optional timeout time in seconds for the upload operation.
* @param bool $allow_refresh_token Whether to allow refreshing the access token if necessary.
*
* @return string One of the following: 'success', 'timeout', 'refresh_required', or 'with_errors'.
*
* @throws \W3TCG_Google_Auth_Exception If the file update/insert fails.
*/
private function _upload_chunk( $files, &$results, $force_rewrite, $timeout_time, $allow_refresh_token ) {
list( $result, $listed_files ) = $this->list_files_chunk( $files, $allow_refresh_token, $timeout_time );
if ( 'success' !== $result ) {
return $result;
}
$files_by_path = array();
foreach ( $listed_files as $existing_file ) {
$path = $this->_properties_to_path( $existing_file );
if ( $path ) {
$files_by_path[ $path ] = $existing_file;
}
}
// check update date and upload.
foreach ( $files as $file_descriptor ) {
$remote_path = $file_descriptor['remote_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';
}
}
list( $parent_id, $title ) = $this->remote_path_to_title( $file_descriptor['remote_path'] );
$properties = $this->_path_to_properties( $remote_path );
if ( isset( $file_descriptor['content'] ) ) {
// when content specified - just upload.
$content = $file_descriptor['content'];
} else {
$local_path = $file_descriptor['local_path'];
if ( ! file_exists( $local_path ) ) {
$results[] = $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_ERROR,
'Source file not found.',
$file_descriptor
);
continue;
}
$mtime = @filemtime( $local_path );
$p = new \W3TCG_Google_Service_Drive_Property();
$p->key = 'mtime';
$p->value = $mtime;
$properties[] = $p;
if ( ! $force_rewrite && isset( $files_by_path[ $remote_path ] ) ) {
$existing_file = $files_by_path[ $remote_path ];
$existing_size = $existing_file->fileSize; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$existing_mtime = 0;
if ( is_array( $existing_file->properties ) ) {
foreach ( $existing_file->properties as $p ) {
if ( 'mtime' === $p->key ) {
$existing_mtime = $p->value;
}
}
}
$size = @filesize( $local_path );
if ( $mtime === $existing_mtime && $size === $existing_size ) {
$results[] = $this->_get_result(
$file_descriptor['local_path'],
$remote_path,
W3TC_CDN_RESULT_OK,
'File up-to-date.',
$file_descriptor
);
continue;
}
}
$content = file_get_contents( $local_path );
}
$file = new \W3TCG_Google_Service_Drive_DriveFile();
$file->setTitle( $title );
$file->setProperties( $properties );
$parent = new \W3TCG_Google_Service_Drive_ParentReference();
$parent->setId( $parent_id );
$file->setParents( array( $parent ) );
try {
try {
// update file if there's one already or insert.
if ( isset( $files_by_path[ $remote_path ] ) ) {
$existing_file = $files_by_path[ $remote_path ];
$created_file = $this->_service->files->update(
$existing_file->id,
$file,
array(
'data' => $content,
'uploadType' => 'media',
)
);
} else {
$created_file = $this->_service->files->insert(
$file,
array(
'data' => $content,
'uploadType' => 'media',
)
);
$permission = new \W3TCG_Google_Service_Drive_Permission();
$permission->setValue( '' );
$permission->setType( 'anyone' );
$permission->setRole( 'reader' );
$this->_service->permissions->insert( $created_file->id, $permission );
}
} catch ( \W3TCG_Google_Auth_Exception $e ) {
if ( $allow_refresh_token ) {
return 'refresh_required';
}
throw $e;
}
$results[] = $this->_get_result(
$file_descriptor['local_path'],
$remote_path,
W3TC_CDN_RESULT_OK,
'OK',
$file_descriptor
);
$this->path_set_id( $remote_path, $created_file->id );
} catch ( \W3TCG_Google_Service_Exception $e ) {
$errors = $e->getErrors();
$details = '';
if ( count( $errors ) >= 1 ) {
$details = wp_json_encode( $errors );
}
delete_transient( 'w3tc_cdn_google_drive_folder_ids' );
$results[] = $this->_get_result(
$file_descriptor['local_path'],
$remote_path,
W3TC_CDN_RESULT_ERROR,
'Failed to upload file ' . $remote_path . ' ' . $details,
$file_descriptor
);
$result = 'with_errors';
continue;
} catch ( \Exception $e ) {
delete_transient( 'w3tc_cdn_google_drive_folder_ids' );
$results[] = $this->_get_result(
$file_descriptor['local_path'],
$remote_path,
W3TC_CDN_RESULT_ERROR,
'Failed to upload file ' . $remote_path,
$file_descriptor
);
$result = 'with_errors';
continue;
}
}
return $result;
}
/**
* Deletes specified files from the Google Drive.
*
* This method processes the deletion of multiple files from Google Drive in chunks. It handles token refresh
* if necessary and updates the result of the deletion process.
*
* @param array $files The list of file paths to be deleted.
* @param array $results The array to collect results of each file deletion.
*
* @return bool True on success, false on failure.
*/
public function delete( $files, &$results ) {
$allow_refresh_token = true;
$result = true;
$files_chunks = array_chunk( $files, 20 );
foreach ( $files_chunks as $files_chunk ) {
$r = $this->_delete_chunk( $files_chunk, $results, $allow_refresh_token );
if ( 'refresh_required' === $r ) {
$allow_refresh_token = false;
$this->_refresh_token();
$r = $this->_delete_chunk( $files_chunk, $results, $allow_refresh_token );
}
if ( 'success' !== $r ) {
$result = false;
}
}
return $result;
}
/**
* Deletes a chunk of files from Google Drive.
*
* This method handles the deletion of a chunk of files, processes the API response, and updates the results array.
*
* @param array $files The chunk of file paths to delete.
* @param array $results The array to collect results of each file deletion.
* @param bool $allow_refresh_token Flag to allow refreshing the token if necessary.
*
* @return string One of the following: 'success', 'with_errors', or 'refresh_required'.
*/
private function _delete_chunk( $files, &$results, $allow_refresh_token ) {
list( $result, $listed_files ) = $this->list_files_chunk( $files, $allow_refresh_token );
if ( 'success' !== $result ) {
return $result;
}
foreach ( $listed_files->items as $item ) {
try {
$this->_service->files->delete( $item->id );
$results[] = $this->_get_result(
$item->title,
$item->title,
W3TC_CDN_RESULT_OK,
'OK'
);
} catch ( \Exception $e ) {
$results[] = $this->_get_result(
'',
'',
W3TC_CDN_RESULT_ERROR,
'Failed to delete file ' . $item->title
);
$result = 'with_errors';
continue;
}
}
return $result;
}
/**
* Lists a chunk of files based on the provided file descriptors.
*
* This method lists files matching the specified descriptors, checking if the refresh token is required or if a
* timeout occurred.
*
* @param array $files The file descriptors to list.
* @param bool $allow_refresh_token Flag to allow refreshing the token if necessary.
* @param int|null $timeout_time The timeout time, if any.
*
* @return array Array containing the result status and the listed files.
*
* @throws \W3TCG_Google_Auth_Exception Throws an exception if authentication fails or the listFiles call fails.
*/
private function list_files_chunk( $files, $allow_refresh_token, $timeout_time = null ) {
$titles_filter = array();
try {
foreach ( $files as $file_descriptor ) {
list( $parent_id, $title ) = $this->remote_path_to_title( $file_descriptor['remote_path'] );
$titles_filter[] = '("' . $parent_id . '" in parents and title = "' . $title . '")';
if ( ! is_null( $timeout_time ) && time() > $timeout_time ) {
return array( 'timeout', array() );
}
}
} catch ( \W3TCG_Google_Auth_Exception $e ) {
if ( $allow_refresh_token ) {
return array( 'refresh_required', array() );
}
throw $e;
} catch ( \Exception $e ) {
return array( 'with_errors', array() );
}
// find files.
try {
try {
$listed_files = $this->_service->files->listFiles(
array( 'q' => '(' . join( ' or ', $titles_filter ) . ') and trashed = false' )
);
} catch ( \W3TCG_Google_Auth_Exception $e ) {
if ( $allow_refresh_token ) {
return array( 'refresh_required', array() );
}
throw $e;
}
} catch ( \Exception $e ) {
return array( 'with_errors', array() );
}
return array( 'success', $listed_files );
}
/**
* Converts a remote file path to its title and parent ID.
*
* This method extracts the title and parent folder ID from a remote file path.
*
* @param string $remote_path The remote file path.
*
* @return array An array containing the parent ID and file title.
*/
private function remote_path_to_title( $remote_path ) {
$title = substr( $remote_path, 1 );
$pos = strrpos( $remote_path, '/' );
if ( false === $pos ) {
$path = '';
$title = $remote_path;
} else {
$path = substr( $remote_path, 0, $pos );
$title = substr( $remote_path, $pos + 1 );
}
$title = str_replace( '"', "'", $title );
$parent_id = $this->path_to_parent_id( $this->_root_folder_id, $path );
return array( $parent_id, $title );
}
/**
* Resolves the parent folder ID for a given path.
*
* This method recursively resolves the parent folder ID for a given path within the Google Drive hierarchy.
*
* @param string $root_id The root folder ID.
* @param string $path The folder path to resolve.
*
* @return string The resolved parent folder ID.
*/
private function path_to_parent_id( $root_id, $path ) {
if ( empty( $path ) ) {
return $root_id;
}
$path = ltrim( $path, '/' );
$pos = strpos( $path, '/' );
if ( false === $pos ) {
$top_folder = $path;
$remaining_path = '';
} else {
$top_folder = substr( $path, 0, $pos );
$remaining_path = substr( $path, $pos + 1 );
}
$new_root_id = $this->parent_id_resolve_step( $root_id, $top_folder );
return $this->path_to_parent_id( $new_root_id, $remaining_path );
}
/**
* Resolves the folder ID for a given folder within a parent folder.
*
* This method checks if the folder exists, creates it if necessary, and resolves its ID.
*
* @param string $root_id The parent folder ID.
* @param string $folder The folder name.
*
* @return string The resolved folder ID.
*/
private function parent_id_resolve_step( $root_id, $folder ) {
// decode top folder.
$ids_string = get_transient( 'w3tc_cdn_google_drive_folder_ids' );
$ids = @unserialize( $ids_string );
if ( isset( $ids[ $root_id . '_' . $folder ] ) ) {
return $ids[ $root_id . '_' . $folder ];
}
// find folder.
$items = $this->_service->files->listFiles(
array(
'q' => '"' . $root_id . '" in parents and title = "' . $folder . '" and mimeType = "application/vnd.google-apps.folder" and trashed = false',
)
);
if ( count( $items ) > 0 ) {
$id = $items[0]->id;
} else {
// create folder.
$file = new \W3TCG_Google_Service_Drive_DriveFile(
array(
'title' => $folder,
'mimeType' => 'application/vnd.google-apps.folder',
)
);
$parent = new \W3TCG_Google_Service_Drive_ParentReference();
$parent->setId( $root_id );
$file->setParents( array( $parent ) );
$created_file = $this->_service->files->insert( $file );
$id = $created_file->id;
$permission = new \W3TCG_Google_Service_Drive_Permission();
$permission->setValue( '' );
$permission->setType( 'anyone' );
$permission->setRole( 'reader' );
$this->_service->permissions->insert( $id, $permission );
}
if ( ! is_array( $ids ) ) {
$ids = array();
}
$ids[ $root_id . '_' . $folder ] = $id;
set_transient( 'w3tc_cdn_google_drive_folder_ids', serialize( $ids ) );
return $id;
}
/**
* Runs a test by uploading and then deleting a test file on Google Drive.
*
* This method uploads a test file, and if successful, deletes it, returning an error if either operation fails.
*
* @param string $error The variable to store error messages if any operation fails.
*
* @return bool True on success, false on failure.
*/
public function test( &$error ) {
$test_content = '' . wp_rand();
$file = array(
'local_path' => 'n/a',
'remote_path' => '/folder/test.txt',
'content' => $test_content,
);
$results = array();
if ( ! $this->upload( array( $file ), $results ) ) {
$error = sprintf( 'Unable to upload file %s', $file['remote_path'] );
return false;
}
if ( ! $this->delete( array( $file ), $results ) ) {
$error = sprintf( 'Unable to delete file %s', $file['remote_path'] );
return false;
}
return true;
}
/**
* Returns the domains supported by the Google Drive CDN.
*
* This method returns an empty array since the current implementation does not support specific domains.
*
* @return array An empty array.
*/
public function get_domains() {
return array();
}
/**
* Returns the type of headers supported by the CDN.
*
* This method returns a constant indicating that no custom headers are supported.
*
* @return string One of the constants indicating header support (e.g., `W3TC_CDN_HEADER_NONE`).
*/
public function headers_support() {
return W3TC_CDN_HEADER_NONE;
}
/**
* Purges all cached files from the Google Drive CDN.
*
* This method does not support purging all cached files and will always return false.
*
* @param array $results The array to collect results of the purging operation.
*
* @return bool Always returns false.
*/
public function purge_all( &$results ) {
return false;
}
/**
* Sets the remote ID for a given file path.
*
* This method stores the remote ID associated with a file path in the database.
*
* @param string $path The local file path.
* @param string $id The remote file ID.
*
* @return void
*/
private function path_set_id( $path, $id ) {
global $wpdb;
$md5 = md5( $path );
if ( ! $id ) {
$sql = "INSERT INTO $this->_tablename_pathmap
(path, path_hash, remote_id)
VALUES (%s, %s, NULL)
ON DUPLICATE KEY UPDATE remote_id = NULL";
$wpdb->query( $wpdb->prepare( $sql, $path, $md5 ) );
} else {
$sql = "INSERT INTO $this->_tablename_pathmap
(path, path_hash, remote_id)
VALUES (%s, %s, %s)
ON DUPLICATE KEY UPDATE remote_id = %s";
$wpdb->query( $wpdb->prepare( $sql, $path, $md5, $id, $id ) );
}
}
/**
* Gets the remote ID associated with a file path.
*
* This method retrieves the remote ID for a file path, either from the database or by querying Google Drive.
*
* @param string $path The local file path.
* @param bool $allow_refresh_token Flag to allow refreshing the token if necessary.
*
* @return string|null The remote file ID or null if not found.
*
* @throws \W3TCG_Google_Auth_Exception Throws an exception if authentication fails or the listFiles call fails.
*/
private function path_get_id( $path, $allow_refresh_token = true ) {
global $wpdb;
$md5 = md5( $path );
$sql = "SELECT remote_id FROM $this->_tablename_pathmap WHERE path_hash = %s";
$query = $wpdb->prepare( $sql, $md5 );
$results = $wpdb->get_results( $query );
if ( count( $results ) > 0 ) {
return $results[0]->remote_id;
}
$props = $this->_path_to_properties( $path );
$q = 'trashed = false';
foreach ( $props as $prop ) {
$key = $prop->key;
$value = str_replace( "'", "\\'", $prop->value );
$q .= " and properties has { key='$key' and value='$value' and visibility='PRIVATE' }";
}
try {
$items = $this->_service->files->listFiles( array( 'q' => $q ) );
} catch ( \W3TCG_Google_Auth_Exception $e ) {
if ( $allow_refresh_token ) {
$this->_refresh_token();
return $this->path_get_id( $path, false );
}
throw $e;
}
$id = ( 0 === count( $items ) ) ? null : $items[0]->id;
$this->path_set_id( $path, $id );
return $id;
}
/**
* Formats a URL for accessing a file on Google Drive.
*
* This method returns a URL to access a file on Google Drive based on its remote ID.
*
* @param string $path The local file path.
* @param bool $allow_refresh_token Flag to allow refreshing the token if necessary.
*
* @return string|null The formatted URL or null if the ID could not be retrieved.
*/
public function format_url( $path, $allow_refresh_token = true ) {
$id = $this->path_get_id( Util_Environment::remove_query( $path ) );
if ( is_null( $id ) ) {
return null;
}
return 'https://drive.google.com/uc?id=' . $id;
}
}

View File

@@ -0,0 +1,114 @@
<?php
/**
* File: CdnEngine_Mirror.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class CdnEngine_Mirror
*
* W3 CDN Mirror Class
*/
class CdnEngine_Mirror extends CdnEngine_Base {
/**
* Constructor for the CdnEngine_Mirror class.
*
* @param array $config Optional configuration settings for the engine.
*
* @return void
*/
public function __construct( $config = array() ) {
$config = array_merge(
array(
'domain' => array(),
),
$config
);
parent::__construct( $config );
}
/**
* Uploads files to the mirror CDN.
*
* @param array $files Array of files to upload.
* @param array $results Reference to an array for storing upload results.
* @param bool $force_rewrite Whether to force overwriting existing files.
* @param int $timeout_time Optional timeout time in seconds.
*
* @return bool True on success, false otherwise.
*/
public function upload( $files, &$results, $force_rewrite = false, $timeout_time = null ) {
$results = $this->_get_results( $files, W3TC_CDN_RESULT_OK, 'OK' );
return true;
}
/**
* Deletes files from the mirror CDN.
*
* @param array $files Array of files to delete.
* @param array $results Reference to an array for storing deletion results.
*
* @return bool True on success, false otherwise.
*/
public function delete( $files, &$results ) {
$results = $this->_get_results( $files, W3TC_CDN_RESULT_OK, 'OK' );
return true;
}
/**
* Tests the connectivity and functionality of the mirror CDN.
*
* @param string $error Reference to a string for storing any error message.
*
* @return bool True if the test succeeds, false otherwise.
*/
public function test( &$error ) {
if ( ! parent::test( $error ) ) {
return false;
}
$results = array();
$files = array(
array(
'local_path' => '',
'remote_path' => 'purge_test_' . time(),
),
);
if ( ! $this->purge( $files, $results ) && isset( $results[0]['error'] ) ) {
$error = $results[0]['error'];
return false;
}
return true;
}
/**
* Retrieves the list of configured CDN domains.
*
* @return array List of configured domains.
*/
public function get_domains() {
if ( ! empty( $this->_config['domain'] ) ) {
return (array) $this->_config['domain'];
}
return array();
}
/**
* Indicates support for headers in the mirror CDN.
*
* @return int Header support constant.
*/
public function headers_support() {
return W3TC_CDN_HEADER_MIRRORING;
}
}

View File

@@ -0,0 +1,164 @@
<?php
/**
* File: CdnEngine_Mirror_Akamai.php
*
* @package W3TC
*/
namespace W3TC;
define( 'W3TC_CDN_MIRROR_AKAMAI_WSDL', 'https://ccuapi.akamai.com/ccuapi-axis.wsdl' );
define( 'W3TC_CDN_MIRROR_AKAMAI_NAMESPACE', 'http://www.akamai.com/purge' );
/**
* Class CdnEngine_Mirror_Akamai
*/
class CdnEngine_Mirror_Akamai extends CdnEngine_Mirror {
/**
* Initializes the CDN engine with the provided configuration.
*
* @param array $config Configuration settings for the CDN engine.
*
* @return void
*/
public function __construct( $config = array() ) {
$config = array_merge(
array(
'username' => '',
'password' => '',
'zone' => '',
'action' => 'invalidate',
'email_notification' => array(),
),
$config
);
parent::__construct( $config );
}
/**
* Purges a list of files from the Akamai CDN.
*
* @param array $files Array of file data to purge from the CDN.
* @param array $results Reference to an array where the purge results will be stored.
*
* @return bool True if the purge request was successful, false otherwise.
*/
public function purge( $files, &$results ) {
if ( empty( $this->_config['username'] ) ) {
$results = $this->_get_results( $files, W3TC_CDN_RESULT_HALT, __( 'Empty username.', 'w3-total-cache' ) );
return false;
}
if ( empty( $this->_config['password'] ) ) {
$results = $this->_get_results( $files, W3TC_CDN_RESULT_HALT, __( 'Empty password.', 'w3-total-cache' ) );
return false;
}
require_once W3TC_LIB_DIR . '/Nusoap/nusoap.php';
$client = new \nusoap_client(
W3TC_CDN_MIRROR_AKAMAI_WSDL,
'wsdl'
);
$error = $client->getError();
if ( $error ) {
$results = $this->_get_results(
$files,
W3TC_CDN_RESULT_HALT,
sprintf(
// Translators: 1 error message.
__(
'Constructor error (%1$s).',
'w3-total-cache'
),
$error
)
);
return false;
}
$zone = $this->_config['zone'];
$expressions = array();
foreach ( $files as $file ) {
$remote_path = $file['remote_path'];
$expressions[] = $this->_format_url( $remote_path );
}
$action = $this->_config['action'];
$email = $this->_config['email_notification'];
$email = implode( ',', $email );
$options = array(
'action=' . $action,
'domain=' . $zone,
'type=arl',
);
if ( $email ) {
$options[] = 'email-notification=' . $email;
}
$params = array(
$this->_config['username'],
$this->_config['password'],
'',
$options,
$expressions,
);
$result = $client->call( 'purgeRequest', $params, W3TC_CDN_MIRROR_AKAMAI_NAMESPACE );
if ( $client->fault ) {
$results = $this->_get_results( $files, W3TC_CDN_RESULT_HALT, __( 'Invalid response.', 'w3-total-cache' ) );
return false;
}
$result_code = $result['resultCode'];
$result_message = $result['resultMsg'];
$error = $client->getError();
if ( $error ) {
$results = $this->_get_results(
$files,
W3TC_CDN_RESULT_HALT,
sprintf(
// Translators: 1 error message.
__(
'Unable to purge (%1$s).',
'w3-total-cache'
),
$error
)
);
return false;
}
if ( $result_code >= 300 ) {
$results = $this->_get_results(
$files,
W3TC_CDN_RESULT_HALT,
sprintf(
// Translators: 1 result message.
__(
'Unable to purge (%1$s).',
'w3-total-cache'
),
$result_message
)
);
return false;
}
$results = $this->_get_results( $files, W3TC_CDN_RESULT_OK, __( 'OK', 'w3-total-cache' ) );
return true;
}
}

View File

@@ -0,0 +1,17 @@
<?php
/**
* File: CdnEngine_Mirror_Akamai.php
*
* @package W3TC
*/
namespace W3TC;
define( 'W3TC_CDN_EDGECAST_PURGE_URL', 'http://api.acdn.att.com/v2/mcc/customers/%s/edge/purge' );
/**
* Class CdnEngine_Mirror_Att
*/
class CdnEngine_Mirror_Att extends CdnEngine_Mirror_Edgecast {
}

View File

@@ -0,0 +1,169 @@
<?php
/**
* File: CdnEngine_Mirror_BunnyCdn.php
*
* @since 2.6.0
* @package W3TC
*/
namespace W3TC;
/**
* Class: CdnEngine_Mirror_BunnyCdn
*
* @since 2.6.0
*
* @extends CdnEngine_Mirror
*/
class CdnEngine_Mirror_BunnyCdn extends CdnEngine_Mirror {
/**
* Constructor.
*
* @param array $config {
* Configuration.
*
* @type string $account_api_key Account API key.
* @type string $storage_api_key Storage API key.
* @type string $stream_api_key Steam API key.
* @type int $pull_zone_id Pull zone id.
* @type string $cdn_hostname CDN hostname.
* }
*/
public function __construct( array $config = array() ) {
$config = \array_merge(
array(
'account_api_key' => '',
'storage_api_key' => '',
'stream_api_key' => '',
'pull_zone_id' => null,
'domain' => '',
),
$config
);
parent::__construct( $config );
}
/**
* Purge remote files.
*
* @since 2.6.0
*
* @param array $files Local and remote file paths.
* @param array $results Results.
*
* @return bool
*/
public function purge( $files, &$results ) {
if ( empty( $this->_config['account_api_key'] ) ) {
$results = $this->_get_results( $files, W3TC_CDN_RESULT_HALT, \__( 'Missing account API key.', 'w3-total-cache' ) );
return false;
}
if ( empty( $this->_config['cdn_hostname'] ) ) {
$results = $this->_get_results( $files, W3TC_CDN_RESULT_HALT, \__( 'Missing CDN hostname.', 'w3-total-cache' ) );
return false;
}
$url_prefixes = $this->url_prefixes();
$api = new Cdn_BunnyCdn_Api( $this->_config );
$results = array();
try {
$items = array();
foreach ( $files as $file ) {
foreach ( $url_prefixes as $prefix ) {
$items[] = array(
'url' => $prefix . '/' . $file['remote_path'],
'recursive' => true,
);
}
}
$api->purge( array( 'items' => $items ) );
$results[] = $this->_get_result( '', '', W3TC_CDN_RESULT_OK, 'OK' );
} catch ( \Exception $e ) {
$results[] = $this->_get_result( '', '', W3TC_CDN_RESULT_HALT, \__( 'Could not purge pull zone items: ', 'w3-total-cache' ) . $e->getMessage() );
}
return ! $this->_is_error( $results );
}
/**
* Purge CDN completely.
*
* @since 2.6.0
*
* @param array $results Results.
*
* @return bool
*/
public function purge_all( &$results ) {
if ( empty( $this->_config['account_api_key'] ) ) {
$results = $this->_get_results( array(), W3TC_CDN_RESULT_HALT, __( 'Missing account API key.', 'w3-total-cache' ) );
return false;
}
// Purge active pull zones: CDN & CDNFSD.
$active_zone_ids = array();
$config = Dispatcher::config();
$cdn_zone_id = $config->get_integer( 'cdn.bunnycdn.pull_zone_id' );
$cdnfsd_zone_id = $config->get_integer( 'cdnfsd.bunnycdn.pull_zone_id' );
if ( $config->get_boolean( 'cdn.enabled' ) && 'bunnycdn' === $config->get_string( 'cdn.engine' ) && $cdn_zone_id ) {
$active_ids[] = $cdn_zone_id;
}
if ( $config->get_boolean( 'cdnfsd.enabled' ) && 'bunnycdn' === $config->get_string( 'cdnfsd.engine' ) && $cdnfsd_zone_id ) {
$active_ids[] = $cdnfsd_zone_id;
}
if ( empty( $active_ids ) ) {
$results = $this->_get_results( array(), W3TC_CDN_RESULT_HALT, __( 'Missing pull zone id.', 'w3-total-cache' ) );
return false;
}
$results = array();
foreach ( $active_ids as $id ) {
$api = new Cdn_BunnyCdn_Api( array_merge( $this->_config, array( 'pull_zone_id' => $id ) ) );
try {
$api->purge_pull_zone();
$results[] = $this->_get_result( '', '' ); // W3TC_CDN_RESULT_OK.
} catch ( \Exception $e ) {
$results[] = $this->_get_result( '', '', W3TC_CDN_RESULT_HALT, \__( 'Could not purge pull zone', 'w3-total-cache' ) . '; ' . $e->getMessage() );
}
}
return ! $this->_is_error( $results );
}
/**
* Get URL prefixes.
*
* If set to "auto", then add URLs for both "http" and "https".
*
* @since 2.6.0
*
* @return array
*/
private function url_prefixes() {
$url_prefixes = array();
if ( 'auto' === $this->_config['ssl'] || 'enabled' === $this->_config['ssl'] ) {
$url_prefixes[] = 'https://' . $this->_config['cdn_hostname'];
}
if ( 'auto' === $this->_config['ssl'] || 'enabled' !== $this->_config['ssl'] ) {
$url_prefixes[] = 'http://' . $this->_config['cdn_hostname'];
}
return $url_prefixes;
}
}

View File

@@ -0,0 +1,412 @@
<?php
/**
* File: CdnEngine_Mirror_CloudFront.php
*
* @package W3TC
*/
namespace W3TC;
if ( ! defined( 'W3TC_SKIPLIB_AWS' ) ) {
require_once W3TC_DIR . '/vendor/autoload.php';
}
/**
* Class CdnEngine_Mirror_CloudFront
*
* Amazon CloudFront (mirror) CDN engine
*
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod.Found
*/
class CdnEngine_Mirror_CloudFront extends CdnEngine_Mirror {
/**
* CloudFront Client object
*
* @var CloudFrontClient
*/
private $api;
/**
* Constructor for the CDN Engine CloudFront class.
*
* @param array $config Configuration array for CloudFront client.
*
* @return void
*/
public function __construct( $config = array() ) {
parent::__construct( $config );
}
/**
* Initializes the CloudFront client API.
*
* @return bool Returns true if the CloudFront client is successfully initialized.
*
* @throws \Exception If the initialization fails.
*/
public function _init() {
if ( ! is_null( $this->api ) ) {
return;
}
if ( empty( $this->_config['key'] ) && empty( $this->_config['secret'] ) ) {
$credentials = \Aws\Credentials\CredentialProvider::defaultProvider();
} else {
$credentials = new \Aws\Credentials\Credentials(
$this->_config['key'],
$this->_config['secret']
);
}
$this->api = new \Aws\CloudFront\CloudFrontClient(
array(
'credentials' => $credentials,
'region' => 'us-east-1',
'version' => '2018-11-05',
)
);
return true;
}
/**
* Retrieves the origin for the CDN.
*
* @return string The host and port of the origin.
*/
public function _get_origin() {
return Util_Environment::host_port();
}
/**
* Purges the specified files from the CDN.
*
* @param array $files Array of files to purge from the CDN.
* @param array $results Reference to an array where the purge results will be stored.
*
* @return bool Returns true if the purge is successful, otherwise false.
*
* @throws \Exception If the purge fails.
*/
public function purge( $files, &$results ) {
try {
$this->_init();
$dist = $this->_get_distribution();
} catch ( \Exception $ex ) {
$results = $this->_get_results( $files, W3TC_CDN_RESULT_HALT, $ex->getMessage() );
return false;
}
$paths = array();
foreach ( $files as $file ) {
$remote_file = $file['remote_path'];
$paths[] = '/' . $remote_file;
}
try {
$invalidation = $this->api->createInvalidation(
array(
'DistributionId' => $dist['Id'],
'InvalidationBatch' => array(
'CallerReference' => 'w3tc-' . microtime(),
'Paths' => array(
'Items' => $paths,
'Quantity' => count( $paths ),
),
),
)
);
} catch ( \Exception $ex ) {
$results = $this->_get_results(
$files,
W3TC_CDN_RESULT_HALT,
sprintf( 'Unable to create invalidation batch (%s).', $ex->getMessage() )
);
return false;
}
$results = $this->_get_results( $files, W3TC_CDN_RESULT_OK, 'OK' );
return true;
}
/**
* Purges all files from the CDN.
*
* @param array $results Reference to an array where the purge results will be stored.
*
* @return bool Returns true if the purge is successful, otherwise false.
*/
public function purge_all( &$results ) {
return $this->purge( array( array( 'remote_path' => '*' ) ), $results );
}
/**
* Retrieves the domains associated with the CDN.
*
* @return array Array of domain names associated with the CDN.
*/
public function get_domains() {
if ( ! empty( $this->_config['cname'] ) ) {
return (array) $this->_config['cname'];
} elseif ( ! empty( $this->_config['id'] ) ) {
$domain = sprintf( '%s.cloudfront.net', $this->_config['id'] );
return array(
$domain,
);
}
return array();
}
/**
* Tests the CloudFront distribution connection and configuration.
*
* @param string $error Reference to a variable where error messages will be stored.
*
* @return bool Returns true if the test passes, otherwise false.
*/
public function test( &$error ) {
$this->_init();
/**
* Search active CF distribution
*/
$dists = $this->api->listDistributions();
if ( ! isset( $dists['DistributionList']['Items'] ) ) {
$error = 'Unable to list distributions.';
return false;
}
if ( ! count( $dists['DistributionList']['Items'] ) ) {
$error = 'No distributions found.';
return false;
}
$dist = $this->_get_distribution( $dists );
if ( 'Deployed' !== $dist['Status'] ) {
$error = sprintf( 'Distribution status is not Deployed, but "%s".', $dist['Status'] );
return false;
}
if ( ! $dist['Enabled'] ) {
$error = sprintf( 'Distribution for origin "%s" is disabled.', $origin );
return false;
}
if ( ! empty( $this->_config['cname'] ) ) {
$domains = (array) $this->_config['cname'];
$cnames = ( isset( $dist['Aliases']['Items'] ) ? (array) $dist['Aliases']['Items'] : array() );
foreach ( $domains as $domain ) {
$_domains = array_map( 'trim', explode( ',', $domain ) );
foreach ( $_domains as $_domain ) {
if ( ! in_array( $_domain, $cnames, true ) ) {
$error = sprintf( 'Domain name %s is not in distribution <acronym title="Canonical Name">CNAME</acronym> list.', $_domain );
return false;
}
}
}
} elseif ( ! empty( $this->_config['id'] ) ) {
$domain = $this->get_domain();
if ( $domain !== $dist['DomainName'] ) {
$error = sprintf( 'Distribution domain name mismatch (%s != %s).', $domain, $dist['DomainName'] );
return false;
}
}
return true;
}
/**
* Creates a new CloudFront distribution container.
*
* phpcs:disable WordPress.Arrays.MultipleStatementAlignment.DoubleArrowNotAligned
*
* @return string The ID of the newly created distribution container.
*
* @throws \Exception If the distribution creation fails.
*/
public function create_container() {
$this->_init();
// plugin cant set CNAMEs list since it CloudFront requires certificate to be specified associated with it.
$cnames = array();
// make distibution.
$origin_domain = $this->_get_origin();
try {
$result = $this->api->createDistribution(
array(
'DistributionConfig' => array(
'CallerReference' => $origin_domain,
'Comment' => 'Created by W3-Total-Cache',
'DefaultCacheBehavior' => array(
'AllowedMethods' => array(
'CachedMethods' => array(
'Items' => array(
'HEAD',
'GET',
),
'Quantity' => 2,
),
'Items' => array(
'HEAD',
'GET',
),
'Quantity' => 2,
),
'Compress' => true,
'DefaultTTL' => 86400,
'FieldLevelEncryptionId' => '',
'ForwardedValues' => array(
'Cookies' => array(
'Forward' => 'none',
),
'Headers' => array(
'Quantity' => 0,
),
'QueryString' => false,
'QueryStringCacheKeys' => array(
'Quantity' => 0,
),
),
'LambdaFunctionAssociations' => array(
'Quantity' => 0,
),
'MinTTL' => 0,
'SmoothStreaming' => false,
'TargetOriginId' => $origin_domain,
'TrustedSigners' => array(
'Enabled' => false,
'Quantity' => 0,
),
'ViewerProtocolPolicy' => 'allow-all',
),
'Enabled' => true,
'Origins' => array(
'Items' => array(
array(
'DomainName' => $origin_domain,
'Id' => $origin_domain,
'OriginPath' => '',
'CustomHeaders' => array(
'Quantity' => 0,
),
'CustomOriginConfig' => array(
'HTTPPort' => 80,
'HTTPSPort' => 443,
'OriginProtocolPolicy' => 'match-viewer',
),
),
),
'Quantity' => 1,
),
'Aliases' => array(
'Items' => $cnames,
'Quantity' => count( $cnames ),
),
),
)
);
// extract domain dynamic part stored later in a config.
$domain = $result['Distribution']['DomainName'];
$container_id = '';
if ( preg_match( '~^(.+)\.cloudfront\.net$~', $domain, $matches ) ) {
$container_id = $matches[1];
}
return $container_id;
} catch ( \Aws\Exception\AwsException $ex ) {
throw new \Exception(
\esc_html(
sprintf(
// Translators: 1 Origin domain name, 2 AWS error message.
\__( 'Unable to create distribution for origin %1$s: %2$s', 'w3-total-cache' ),
$origin_domain,
$ex->getAwsErrorMessage()
)
)
);
} catch ( \Exception $ex ) {
throw new \Exception(
\esc_html(
sprintf(
// Translators: 1 Origin domain name, 2 Error message.
\__( 'Unable to create distribution for origin %1$s: %2$s', 'w3-total-cache' ),
$origin_domain,
$ex->getMessage()
)
)
);
}
}
/**
* Retrieves the CDN's "via" information.
*
* @return string The "via" string indicating the CDN's origin.
*/
public function get_via() {
$domain = $this->get_domain();
$via = ( $domain ? $domain : 'N/A' );
return sprintf( 'Amazon Web Services: CloudFront: %s', $via );
}
/**
* Retrieves the CloudFront distribution for the origin.
*
* @param array|null $dists Optional array of distributions to search through. If null, all distributions are fetched.
*
* @return array The distribution information for the origin.
*
* @throws \Exception If no matching distribution is found.
*/
private function _get_distribution( $dists = null ) {
if ( is_null( $dists ) ) {
$dists = $this->api->listDistributions();
}
if ( ! isset( $dists['DistributionList']['Items'] ) || ! count( $dists['DistributionList']['Items'] ) ) {
throw new \Exception( \esc_html__( 'No distributions found.', 'w3-total-cache' ) );
}
$dist = false;
$origin = $this->_get_origin();
$items = $dists['DistributionList']['Items'];
foreach ( $items as $dist ) {
if ( isset( $dist['Origins']['Items'] ) ) {
foreach ( $dist['Origins']['Items'] as $o ) {
if ( isset( $o['DomainName'] ) && $o['DomainName'] === $origin ) {
return $dist;
}
}
}
}
throw new \Exception(
\esc_html(
sprintf(
// Translators: 1 Origin name.
\__( 'Distribution for origin "%1$s" not found.', 'w3-total-cache' ),
$origin
)
)
);
}
}

View File

@@ -0,0 +1,171 @@
<?php
/**
* File: CdnEngine_Mirror_Cotendo.php
*
* @package W3TC
*/
namespace W3TC;
define( 'W3TC_CDN_MIRROR_COTENDO_WSDL', 'https://api.cotendo.net/cws?wsdl' );
define( 'W3TC_CDN_MIRROR_COTENDO_ENDPOINT', 'http://api.cotendo.net/cws?ver=1.0' );
define( 'W3TC_CDN_MIRROR_COTENDO_NAMESPACE', 'http://api.cotendo.net/' );
/**
* Class CdnEngine_Mirror_Cotendo
*/
class CdnEngine_Mirror_Cotendo extends CdnEngine_Mirror {
/**
* Constructs a new instance of the class.
*
* @param array $config Configuration settings for the instance.
*
* @return void
*/
public function __construct( $config = array() ) {
$config = array_merge(
array(
'username' => '',
'password' => '',
'zones' => array(),
),
$config
);
parent::__construct( $config );
}
/**
* Purges specified files from the CDN.
*
* @param array $files List of files to purge.
* @param array $results Reference to the array where the purge results will be stored.
*
* @return bool True on success, false on failure.
*/
public function purge( $files, &$results ) {
if ( empty( $this->_config['username'] ) ) {
$results = $this->_get_results( $files, W3TC_CDN_RESULT_HALT, __( 'Empty username.', 'w3-total-cache' ) );
return false;
}
if ( empty( $this->_config['password'] ) ) {
$results = $this->_get_results( $files, W3TC_CDN_RESULT_HALT, __( 'Empty password.', 'w3-total-cache' ) );
return false;
}
if ( empty( $this->_config['zones'] ) ) {
$results = $this->_get_results( $files, W3TC_CDN_RESULT_HALT, __( 'Empty zones list.', 'w3-total-cache' ) );
return false;
}
require_once W3TC_LIB_DIR . '/Nusoap/nusoap.php';
$client = new \nusoap_client(
W3TC_CDN_MIRROR_COTENDO_WSDL,
'wsdl'
);
$error = $client->getError();
if ( $error ) {
$results = $this->_get_results(
$files,
W3TC_CDN_RESULT_HALT,
sprintf(
// Translators: 1 error message.
__(
'Constructor error (%1$s).',
'w3-total-cache'
),
$error
)
);
return false;
}
$client->authtype = 'basic';
$client->username = $this->_config['username'];
$client->password = $this->_config['password'];
$client->forceEndpoint = W3TC_CDN_MIRROR_COTENDO_ENDPOINT; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
foreach ( (array) $this->_config['zones'] as $zone ) {
$expressions = array();
foreach ( $files as $file ) {
$remote_path = $file['remote_path'];
$expressions[] = '/' . $remote_path;
}
$expression = implode( "\n", $expressions );
$params = array(
'cname' => $zone,
'flushExpression' => $expression,
'flushType' => 'hard',
);
$client->call( 'doFlush', $params, W3TC_CDN_MIRROR_COTENDO_NAMESPACE );
if ( $client->fault ) {
$results = $this->_get_results(
$files,
W3TC_CDN_RESULT_HALT,
__( 'Invalid response.', 'w3-total-cache' )
);
return false;
}
$error = $client->getError();
if ( $error ) {
$results = $this->_get_results(
$files,
W3TC_CDN_RESULT_HALT,
sprintf(
// Translators: 1 error message.
__(
'Unable to purge (%1$s).',
'w3-total-cache'
),
$error
)
);
return false;
}
}
$results = $this->_get_results(
$files,
W3TC_CDN_RESULT_OK,
__( 'OK', 'w3-total-cache' )
);
return true;
}
/**
* Purges all files from the CDN.
*
* @param array $results Reference to the array where the purge results will be stored.
*
* @return bool True on success, false on failure.
*/
public function purge_all( &$results ) {
return $this->purge(
array(
array(
'local_path' => '*',
'remote_path' => '*',
),
),
$results
);
}
}

View File

@@ -0,0 +1,186 @@
<?php
/**
* File: CdnEngine_Mirror_Edgecast.php
*
* @package W3TC
*/
namespace W3TC;
if ( ! defined( 'W3TC_CDN_EDGECAST_PURGE_URL' ) ) {
define( 'W3TC_CDN_EDGECAST_PURGE_URL', 'http://api.edgecast.com/v2/mcc/customers/%s/edge/purge' );
}
define( 'W3TC_CDN_EDGECAST_MEDIATYPE_WINDOWS_MEDIA_STREAMING', 1 );
define( 'W3TC_CDN_EDGECAST_MEDIATYPE_FLASH_MEDIA_STREAMING', 2 );
define( 'W3TC_CDN_EDGECAST_MEDIATYPE_HTTP_LARGE_OBJECT', 3 );
define( 'W3TC_CDN_EDGECAST_MEDIATYPE_HTTP_SMALL_OBJECT', 8 );
define( 'W3TC_CDN_EDGECAST_MEDIATYPE_APPLICATION_DELIVERY_NETWORK', 14 );
/**
* Class CdnEngine_Mirror_Edgecast
*
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
*/
class CdnEngine_Mirror_Edgecast extends CdnEngine_Mirror {
/**
* Constructor for the class.
*
* @param array $config Configuration array with API credentials and other settings.
*
* @return void
*/
public function __construct( $config = array() ) {
$config = array_merge(
array(
'apiid' => '',
'apikey' => '',
),
$config
);
parent::__construct( $config );
}
/**
* Purges specified files from the CDN.
*
* @param array $files Array of files to purge.
* @param array $results Reference to an array where the purge results will be stored.
*
* @return bool True if all files were purged successfully, false otherwise.
*/
public function purge( $files, &$results ) {
if ( empty( $this->_config['account'] ) ) {
$results = $this->_get_results( $files, W3TC_CDN_RESULT_HALT, __( 'Empty account #.', 'w3-total-cache' ) );
return false;
}
if ( empty( $this->_config['token'] ) ) {
$results = $this->_get_results( $files, W3TC_CDN_RESULT_HALT, __( 'Empty token.', 'w3-total-cache' ) );
return false;
}
foreach ( $files as $file ) {
$local_path = $file['local_path'];
$remote_path = $file['remote_path'];
$url = $this->format_url( $remote_path );
$error = null;
if ( $this->_purge_content( $url, W3TC_CDN_EDGECAST_MEDIATYPE_HTTP_SMALL_OBJECT, $error ) ) {
$results[] = $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_OK,
__( 'OK', 'w3-total-cache' ),
$file
);
} else {
$results[] = $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_ERROR,
sprintf(
// Translators: 1 error message.
__(
'Unable to purge (%1$s).',
'w3-total-cache'
),
$error
),
$file
);
}
}
return ! $this->_is_error( $results );
}
/**
* Purges all files from the CDN.
*
* @param array $results Reference to an array where the purge results will be stored.
*
* @return bool True if the purge was successful, false otherwise.
*/
public function purge_all( &$results ) {
return $this->purge(
array(
array(
'local_path' => '*',
'remote_path' => '*',
),
),
$results
);
}
/**
* Sends a request to purge content from the CDN.
*
* @param string $path The path of the content to purge.
* @param string $type The type of the content to purge.
* @param string $error Reference to a variable where any error message will be stored.
*
* @return bool True if the purge request was successful, false otherwise.
*/
public function _purge_content( $path, $type, &$error ) {
$url = sprintf( W3TC_CDN_EDGECAST_PURGE_URL, $this->_config['account'] );
$args = array(
'method' => 'PUT',
'user-agent' => W3TC_POWERED_BY,
'headers' => array(
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Authorization' => sprintf( 'TOK:%s', $this->_config['token'] ),
),
'body' => wp_json_encode(
array(
'MediaPath' => $path,
'MediaType' => $type,
)
),
);
$response = wp_remote_request( $url, $args );
if ( is_wp_error( $response ) ) {
$error = implode( '; ', $response->get_error_messages() );
return false;
}
switch ( $response['response']['code'] ) {
case 200:
return true;
case 400:
$error = __( 'Invalid Request Parameter', 'w3-total-cache' );
return false;
case 403:
$error = __( 'Authentication Failure or Insufficient Access Rights', 'w3-total-cache' );
return false;
case 404:
$error = __( 'Invalid Request URI', 'w3-total-cache' );
return false;
case 405:
$error = __( 'Invalid Request', 'w3-total-cache' );
return false;
case 500:
$error = __( 'Server Error', 'w3-total-cache' );
return false;
}
$error = 'Unknown error';
return false;
}
}

View File

@@ -0,0 +1,267 @@
<?php
/**
* File: CdnEngine_Mirror_RackSpaceCdn.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class CdnEngine_Mirror_RackSpaceCdn
*
* Rackspace CDN (pull) engine
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
*/
class CdnEngine_Mirror_RackSpaceCdn extends CdnEngine_Mirror {
/**
* Access state
*
* @var array
*/
private $_access_state;
/**
* Service ID
*
* @var string
*/
private $_service_id;
/**
* Domains
*
* @var array
*/
private $_domains;
/**
* CDN RackSpace API object
*
* @var Cdn_RackSpace_Api_Cdn
*/
private $_api;
/**
* New access state callback
*
* @var callable
*/
private $_new_access_state_callback;
/**
* Initializes the CdnEngine_Mirror_RackSpaceCdn instance with configuration parameters.
*
* @param array $config Configuration settings for the RackSpace CDN service.
*
* @return void
*/
public function __construct( $config = array() ) {
$config = array_merge(
array(
'user_name' => '',
'api_key' => '',
'region' => '',
'service_id' => '',
'service_access_url' => '',
'service_protocol' => 'http',
'domains' => array(),
'access_state' => '',
'new_access_state_callback' => '',
),
$config
);
$this->_service_id = $config['service_id'];
$this->_new_access_state_callback = $config['new_access_state_callback'];
// init access state.
$this->_access_state = @json_decode( $config['access_state'], true );
if ( ! is_array( $this->_access_state ) ) {
$this->_access_state = array();
}
$this->_access_state = array_merge(
array(
'access_token' => '',
'access_region_descriptor' => array(),
),
$this->_access_state
);
// cnames.
if ( 'https' !== $config['service_protocol'] && ! empty( $config['domains'] ) ) {
$this->_domains = (array) $config['domains'];
} else {
$this->_domains = array( $config['service_access_url'] );
}
// form 'ssl' parameter based on service protocol.
if ( 'https' === $config['service_protocol'] ) {
$config['ssl'] = 'enabled';
} else {
$config['ssl'] = 'disabled';
}
parent::__construct( $config );
$this->_create_api( array( $this, '_on_new_access_requested_api' ) );
}
/**
* Creates the API instance for RackSpace CDN.
*
* @param callable $new_access_required_callback_api Callback for handling access renewal.
*
* @return void
*/
private function _create_api( $new_access_required_callback_api ) {
$this->_api = new Cdn_RackSpace_Api_Cdn(
array(
'access_token' => $this->_access_state['access_token'],
'access_region_descriptor' => $this->_access_state['access_region_descriptor'],
'new_access_required' => $new_access_required_callback_api,
)
);
}
/**
* Handles the process of requesting new access tokens and region descriptors via the API.
*
* @return Cdn_RackSpace_Api_Cdn The API instance with updated access credentials.
*
* @throws \Exception If authentication or region retrieval fails.
*/
public function _on_new_access_requested_api() {
$r = Cdn_RackSpace_Api_Tokens::authenticate( $this->_config['user_name'], $this->_config['api_key'] );
if ( ! isset( $r['access_token'] ) || ! isset( $r['services'] ) ) {
throw new \Exception( \esc_html__( 'Authentication failed.', 'w3-total-cache' ) );
}
$r['regions'] = Cdn_RackSpace_Api_Tokens::cdn_services_by_region( $r['services'] );
if ( ! isset( $r['regions'][ $this->_config['region'] ] ) ) {
throw new \Exception(
\esc_html(
sprintf(
// translators: 1: Region name.
\__( 'Region %1$s not found.', 'w3-total-cache' ),
$this->_config['region']
)
)
);
}
$this->_access_state['access_token'] = $r['access_token'];
$this->_access_state['access_region_descriptor'] = $r['regions'][ $this->_config['region'] ];
$this->_create_api( array( $this, '_on_new_access_requested_second_time' ) );
if ( ! empty( $this->_new_access_state_callback ) ) {
call_user_func( $this->_new_access_state_callback, wp_json_encode( $this->_access_state ) );
}
return $this->_api;
}
/**
* Handles the fallback case when the first authentication attempt fails.
*
* @return void
*
* @throws \Exception Always throws an exception indicating authentication failure.
*/
private function _on_new_access_requested_second_time() {
throw new \Exception( \esc_html__( 'Authentication failed', 'w3-total-cache' ) );
}
/**
* Purges a list of files from the RackSpace CDN.
*
* @param array $files Array of file descriptors to purge.
* @param array $results Reference to an array for storing purge results.
*
* @return bool True on success, false if there were errors during purging.
*/
public function purge( $files, &$results ) {
$results = array();
try {
foreach ( $files as $file ) {
$url = $this->_format_url( $file['remote_path'] );
$this->_api->purge( $this->_service_id, $url );
$results[] = $this->_get_result( '', '', W3TC_CDN_RESULT_OK, 'OK' );
}
} catch ( \Exception $e ) {
$results[] = $this->_get_result(
'',
'',
W3TC_CDN_RESULT_HALT,
\__( 'Failed to purge: ', 'w3-total-cache' ) . $e->getMessage()
);
}
return ! $this->_is_error( $results );
}
/**
* Retrieves the current set of domains associated with the CDN service.
*
* @return array List of domains.
*/
public function get_domains() {
return $this->_domains;
}
/**
* Retrieves the domains associated with the RackSpace service.
*
* @return array List of domains configured in the service.
*/
public function service_domains_get() {
$service = $this->_api->service_get( $this->_service_id );
$domains = array();
if ( isset( $service['domains'] ) ) {
foreach ( $service['domains'] as $d ) {
$domains[] = $d['domain'];
}
}
return $domains;
}
/**
* Updates the domains associated with the RackSpace service.
*
* @param array $domains List of new domains to set.
*
* @return void
*/
public function service_domains_set( $domains ) {
$value = array();
foreach ( $domains as $d ) {
$v = array( 'domain' => $d );
if ( 'https' === $this->_config['service_protocol'] ) {
$v['protocol'] = 'https';
}
$value[] = $v;
}
$this->_api->service_set(
$this->_service_id,
array(
array(
'op' => 'replace',
'path' => '/domains',
'value' => $value,
),
)
);
}
}

View File

@@ -0,0 +1,467 @@
<?php
/**
* File: CdnEngine_RackSpaceCloudFiles.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class CdnEngine_RackSpaceCloudFiles
*
* Rackspace Cloud Files CDN engine
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
* phpcs:disable WordPress.WP.AlternativeFunctions
*/
class CdnEngine_RackSpaceCloudFiles extends CdnEngine_Base {
/**
* Access state
*
* @var array
*/
private $_access_state;
/**
* Container
*
* @var object
*/
private $_container;
/**
* CDN RackSpace API CloudFiles object
*
* @var Cdn_RackSpace_Api_CloudFiles
*/
private $_api_files;
/**
* CDN RackSpace API CloudFilesCdn object
*
* @var Cdn_RackSpace_Api_CloudFilesCdn
*/
private $_api_cdn;
/**
* Callback function to handle the updated access state.
*
* This callback is invoked with a JSON-encoded string containing the new access state
* whenever authentication occurs and the access state is refreshed.
*
* @var callable
*/
private $_new_access_state_callback;
/**
* Initializes the CdnEngine_RackSpaceCloudFiles class with configuration.
*
* @param array $config Configuration options for the class.
*
* @return void
*/
public function __construct( $config = array() ) {
$config = array_merge(
array(
'user_name' => '',
'api_key' => '',
'region' => '',
'container' => '',
'cname' => array(),
'access_state' => '',
),
$config
);
$this->_container = $config['container'];
$this->_new_access_state_callback = $config['new_access_state_callback'];
// init access state.
$this->_access_state = @json_decode( $config['access_state'], true );
if ( ! is_array( $this->_access_state ) ) {
$this->_access_state = array();
}
$this->_access_state = array_merge(
array(
'access_token' => '',
'access_region_descriptor' => array(),
'host_http' => '',
'host_https' => '',
),
$this->_access_state
);
parent::__construct( $config );
$this->_create_api(
array( $this, '_on_new_access_requested_api_files' ),
array( $this, '_on_new_access_requested_api_cdn' )
);
}
/**
* Creates API instances for files and CDN.
*
* @param callable $new_access_required_callback_api_files Callback for file API access requests.
* @param callable $new_access_required_callback_api_cdn Callback for CDN API access requests.
*
* @return void
*/
private function _create_api( $new_access_required_callback_api_files, $new_access_required_callback_api_cdn ) {
$this->_api_files = new Cdn_RackSpace_Api_CloudFiles(
array(
'access_token' => $this->_access_state['access_token'],
'access_region_descriptor' => $this->_access_state['access_region_descriptor'],
'new_access_required' => $new_access_required_callback_api_files,
)
);
$this->_api_cdn = new Cdn_RackSpace_Api_CloudFilesCdn(
array(
'access_token' => $this->_access_state['access_token'],
'access_region_descriptor' => $this->_access_state['access_region_descriptor'],
'new_access_required' => $new_access_required_callback_api_cdn,
)
);
}
/**
* Handles new access requests for file API.
*
* @return Cdn_RackSpace_Api_CloudFiles Instance of the file API.
*/
public function _on_new_access_requested_api_files() {
$this->_on_new_access_requested();
return $this->_api_files;
}
/**
* Handles new access requests for CDN API.
*
* @return Cdn_RackSpace_Api_CloudFilesCdn Instance of the CDN API.
*/
public function _on_new_access_requested_api_cdn() {
$this->_on_new_access_requested();
return $this->_api_cdn;
}
/**
* Processes new access requests and updates the access state.
*
* @return void
*
* @throws \Exception If authentication fails or the region is not found.
*/
private function _on_new_access_requested() {
$r = Cdn_RackSpace_Api_Tokens::authenticate( $this->_config['user_name'], $this->_config['api_key'] );
if ( ! isset( $r['access_token'] ) || ! isset( $r['services'] ) ) {
throw new \Exception( \esc_html__( 'Authentication failed.', 'w3-total-cache' ) );
}
$r['regions'] = Cdn_RackSpace_Api_Tokens::cloudfiles_services_by_region( $r['services'] );
if ( ! isset( $r['regions'][ $this->_config['region'] ] ) ) {
throw new \Exception(
\esc_html(
sprintf(
// translators: 1: Region name.
\__( 'Region %1$s not found.', 'w3-total-cache' ),
$this->_config['region']
)
)
);
}
$this->_access_state['access_token'] = $r['access_token'];
$this->_access_state['access_region_descriptor'] = $r['regions'][ $this->_config['region'] ];
$this->_create_api(
array( $this, '_on_new_access_requested_second_time' ),
array( $this, '_on_new_access_requested_second_time' )
);
$c = $this->_api_cdn->container_get( $this->_config['container'] );
$this->_access_state['host_http'] = substr( $c['x-cdn-uri'], 7 );
$this->_access_state['host_https'] = substr( $c['x-cdn-ssl-uri'], 8 );
call_user_func( $this->_new_access_state_callback, wp_json_encode( $this->_access_state ) );
}
/**
* Handles repeated access requests in case of authentication failure.
*
* @return void
*
* @throws \Exception Always throws an exception for failed authentication.
*/
private function _on_new_access_requested_second_time() {
throw new \Exception( \esc_html__( 'Authentication failed.', 'w3-total-cache' ) );
}
/**
* Formats a URL for a given file path.
*
* @param string $path The file path to format.
*
* @return string|false The formatted URL, or false if the domain is not found.
*/
public function _format_url( $path ) {
$domain = $this->get_domain( $path );
if ( $domain ) {
$scheme = $this->_get_scheme();
// it does not support '+', requires '%2B'.
$path = str_replace( '+', '%2B', $path );
$url = sprintf( '%s://%s/%s', $scheme, $domain, $path );
return $url;
}
return false;
}
/**
* Uploads files to Rackspace Cloud Files.
*
* @param array $files Array of file descriptors for upload.
* @param array $results Reference to an array for storing results.
* @param bool $force_rewrite Whether to force overwriting existing files.
* @param int|null $timeout_time Optional timeout time in seconds.
*
* @return bool True on success, false on error.
*/
public function upload( $files, &$results, $force_rewrite = false, $timeout_time = null ) {
foreach ( $files as $file ) {
$local_path = $file['local_path'];
$remote_path = $file['remote_path'];
// process at least one item before timeout so that progress goes on.
if ( ! empty( $results ) && ! is_null( $timeout_time ) && time() > $timeout_time ) {
return 'timeout';
}
if ( ! file_exists( $local_path ) ) {
$results[] = $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_ERROR,
'Source file not found.',
$file
);
continue;
}
$file_content = file_get_contents( $local_path );
$do_write = true;
// rewrite is optional, check md5.
if ( ! $force_rewrite ) {
$object_meta = null;
try {
$object_meta = $this->_api_files->object_get_meta_or_null( $this->_container, $remote_path );
} catch ( \Exception $exception ) {
$results[] = $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_ERROR,
sprintf( 'Unable to check object (%s).', $exception->getMessage() ),
$file
);
$do_write = false;
}
if ( is_array( $object_meta ) && isset( $object_meta['etag'] ) ) {
$md5_actual = md5( $file_content );
if ( $md5_actual === $object_meta['etag'] ) {
$results[] = $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_OK,
'Object up-to-date.',
$file
);
$do_write = false;
}
}
}
if ( $do_write ) {
try {
$this->_api_files->object_create(
array(
'container' => $this->_container,
'name' => $remote_path,
'content_type' => Util_Mime::get_mime_type( $local_path ),
'content' => $file_content,
)
);
$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 create object (%s).', $exception->getMessage() ),
$file
);
}
}
}
return ! $this->_is_error( $results );
}
/**
* Deletes files from Rackspace Cloud Files.
*
* @param array $files Array of file descriptors to delete.
* @param array $results Reference to an array for storing results.
*
* @return bool True on success, false on error.
*/
public function delete( $files, &$results ) {
foreach ( $files as $file ) {
$local_path = $file['local_path'];
$remote_path = $file['remote_path'];
try {
$this->_api_files->object_delete( $this->_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 object (%s).', $exception->getMessage() ),
$file
);
}
}
return ! $this->_is_error( $results );
}
/**
* Tests the connection to Rackspace Cloud Files.
*
* @param string $error Reference to store error messages, if any.
*
* @return bool True if the test succeeds, false otherwise.
*/
public function test( &$error ) {
$filename = 'test_rscf_' . md5( time() );
try {
$object = $this->_api_files->object_create(
array(
'container' => $this->_container,
'name' => $filename,
'content_type' => 'text/plain',
'content' => $filename,
)
);
} catch ( \Exception $exception ) {
$error = sprintf( 'Unable to write object (%s).', $exception->getMessage() );
return false;
}
$result = true;
try {
$r = wp_remote_get( 'http://' . $this->get_host_http() . '/' . $filename );
if ( $r['body'] !== $filename ) {
$error = 'Failed to retrieve object after storing.';
$result = false;
}
} catch ( \Exception $exception ) {
$error = sprintf( 'Unable to read object (%s).', $exception->getMessage() );
$result = false;
}
try {
$this->_api_files->object_delete( $this->_container, $filename );
} catch ( \Exception $exception ) {
$error = sprintf( 'Unable to delete object (%s).', $exception->getMessage() );
$result = false;
}
return $result;
}
/**
* Retrieves the list of available domains for the service.
*
* @return array List of domain names.
*/
public function get_domains() {
if ( Util_Environment::is_https() ) {
if ( ! empty( $this->_config['cname'] ) ) {
return (array) $this->_config['cname'];
}
return array( $this->get_host_https() );
} else {
if ( ! empty( $this->_config['cname'] ) ) {
return (array) $this->_config['cname'];
}
return array( $this->get_host_http() );
}
}
/**
* Retrieves the service descriptor including the domain used for accessing the CDN.
*
* @return string Descriptor of the service including domain used for accessing the CDN.
*/
public function get_via() {
return sprintf( 'Rackspace Cloud Files: %s', parent::get_via() );
}
/**
* Retrieves the HTTP host URL.
*
* @return string The HTTP host URL.
*/
public function get_host_http() {
if ( empty( $this->_access_state['host_http'] ) ) {
$this->_on_new_access_requested();
}
return $this->_access_state['host_http'];
}
/**
* Retrieves the HTTPS host URL.
*
* @return string The HTTPS host URL.
*/
public function get_host_https() {
if ( empty( $this->_access_state['host_https'] ) ) {
$this->_on_new_access_requested();
}
return $this->_access_state['host_https'];
}
}

View File

@@ -0,0 +1,717 @@
<?php
/**
* File: CdnEngine_S3.php
*
* @package W3TC
*/
namespace W3TC;
if ( ! defined( 'W3TC_SKIPLIB_AWS' ) ) {
require_once W3TC_DIR . '/vendor/autoload.php';
}
/**
* Class CdnEngine_S3
*
* CDN engine for S3 push type
*
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
* phpcs:disable WordPress.WP.AlternativeFunctions
*/
class CdnEngine_S3 extends CdnEngine_Base {
/**
* S3Client object
*
* @var S3Client
*/
private $api;
/**
* Retrieves a list of AWS regions supported by the CDN.
*
* @see Cdn_Core::get_region_id()
* @link https://docs.aws.amazon.com/general/latest/gr/rande.html
*
* @return array Associative array of region IDs and their corresponding names.
*/
public static function regions_list() {
return array(
'us-east-1' => \__( 'US East (N. Virginia) (default)', 'w3-total-cache' ), // Default; region not included in hostnmae.
'us-east-1-e' => \__( 'US East (N. Virginia) (long hostname)', 'w3-total-cache' ), // Explicitly included in hostname.
'us-east-2' => \__( 'US East (Ohio)', 'w3-total-cache' ),
'us-west-1' => \__( 'US West (N. California)', 'w3-total-cache' ),
'us-west-2' => \__( 'US West (Oregon)', 'w3-total-cache' ),
'af-south-1' => \__( 'Africa (Cape Town)', 'w3-total-cache' ),
'ap-east-1' => \__( 'Asia Pacific (Hong Kong)', 'w3-total-cache' ),
'ap-northeast-1' => \__( 'Asia Pacific (Tokyo)', 'w3-total-cache' ),
'ap-northeast-2' => \__( 'Asia Pacific (Seoul)', 'w3-total-cache' ),
'ap-northeast-3' => \__( 'Asia Pacific (Osaka-Local)', 'w3-total-cache' ),
'ap-south-1' => \__( 'Asia Pacific (Mumbai)', 'w3-total-cache' ),
'ap-southeast-1' => \__( 'Asia Pacific (Singapore)', 'w3-total-cache' ),
'ap-southeast-2' => \__( 'Asia Pacific (Sydney)', 'w3-total-cache' ),
'ca-central-1' => \__( 'Canada (Central)', 'w3-total-cache' ),
'cn-north-1' => \__( 'China (Beijing)', 'w3-total-cache' ),
'cn-northwest-1' => \__( 'China (Ningxia)', 'w3-total-cache' ),
'eu-central-1' => \__( 'Europe (Frankfurt)', 'w3-total-cache' ),
'eu-north-1' => \__( 'Europe (Stockholm)', 'w3-total-cache' ),
'eu-south-1' => \__( 'Europe (Milan)', 'w3-total-cache' ),
'eu-west-1' => \__( 'Europe (Ireland)', 'w3-total-cache' ),
'eu-west-2' => \__( 'Europe (London)', 'w3-total-cache' ),
'eu-west-3' => \__( 'Europe (Paris)', 'w3-total-cache' ),
'me-south-1' => \__( 'Middle East (Bahrain)', 'w3-total-cache' ),
'sa-east-1' => \__( 'South America (São Paulo)', 'w3-total-cache' ),
);
}
/**
* Initializes the CdnEngine_S3 class with a given configuration.
*
* @param array $config Configuration array for S3 integration.
*
* @return void
*/
public function __construct( $config = array() ) {
$config = array_merge(
array(
'key' => '',
'secret' => '',
'bucket' => '',
'bucket_location' => '',
'cname' => array(),
),
$config
);
parent::__construct( $config );
}
/**
* Formats a URL for a given path.
*
* @param string $path The path to format into a URL.
*
* @return string|false The formatted URL, or false if the domain could not be determined.
*/
public function _format_url( $path ) {
$domain = $this->get_domain( $path );
if ( $domain ) {
$scheme = $this->_get_scheme();
// it does not support '+', requires '%2B'.
$path = str_replace( '+', '%2B', $path );
$url = sprintf( '%s://%s/%s', $scheme, $domain, $path );
return $url;
}
return false;
}
/**
* Initializes the S3 client and validates credentials.
*
* @see Cdn_Core::get_region_id()
*
* @return void
*
* @throws \Exception If the bucket or credentials are not properly configured.
*/
public function _init() {
if ( ! is_null( $this->api ) ) {
return;
}
if ( empty( $this->_config['bucket'] ) ) {
throw new \Exception( \esc_html__( 'Empty bucket.', 'w3-total-cache' ) );
}
if ( empty( $this->_config['key'] ) && empty( $this->_config['secret'] ) ) {
$credentials = \Aws\Credentials\CredentialProvider::defaultProvider();
} else {
if ( empty( $this->_config['key'] ) ) {
throw new \Exception( \esc_html__( 'Empty access key.', 'w3-total-cache' ) );
}
if ( empty( $this->_config['secret'] ) ) {
throw new \Exception( \esc_html__( 'Empty secret key.', 'w3-total-cache' ) );
}
$credentials = new \Aws\Credentials\Credentials(
$this->_config['key'],
$this->_config['secret']
);
}
if ( isset( $this->_config['public_objects'] ) && 'enabled' === $this->_config['public_objects'] ) {
$this->_config['s3_acl'] = 'public-read';
}
$this->api = new \Aws\S3\S3Client(
array(
'credentials' => $credentials,
'region' => preg_replace( '/-e$/', '', $this->_config['bucket_location'] ),
'version' => '2006-03-01',
'use_arn_region' => true,
)
);
}
/**
* Uploads files to the S3 bucket.
*
* @param array $files List of files to upload with their paths.
* @param array $results Reference array to store upload results.
* @param bool $force_rewrite Whether to overwrite existing files.
* @param int|null $timeout_time Optional timeout time in seconds for the upload.
*
* @return bool|string Returns true if successful, false on error, or 'timeout' on timeout.
*/
public function upload( $files, &$results, $force_rewrite = false, $timeout_time = null ) {
$error = null;
try {
$this->_init();
} catch ( \Exception $ex ) {
$results = $this->_get_results( $files, W3TC_CDN_RESULT_HALT, $ex->getMessage() );
return false;
}
foreach ( $files as $file ) {
$local_path = $file['local_path'];
$remote_path = $file['remote_path'];
// process at least one item before timeout so that progress goes on.
if ( ! empty( $results ) && ! is_null( $timeout_time ) && time() > $timeout_time ) {
return 'timeout';
}
$results[] = $this->_upload( $file, $force_rewrite );
if ( $this->_config['compression'] && $this->_may_gzip( $remote_path ) ) {
$file['remote_path_gzip'] = $remote_path . $this->_gzip_extension;
$results[] = $this->_upload_gzip( $file, $force_rewrite );
}
}
return ! $this->_is_error( $results );
}
/**
* Uploads a single file to the S3 bucket.
*
* @param array $file File descriptor containing local and remote paths.
* @param bool $force_rewrite Whether to overwrite the file if it exists.
*
* @return array Result of the upload operation.
*
* @throws \Aws\Exception\AwsException If an unexpected error occurs during the upload process.
*/
private 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
);
}
try {
if ( ! $force_rewrite ) {
try {
$info = $this->api->headObject(
array(
'Bucket' => $this->_config['bucket'],
'Key' => $remote_path,
)
);
$hash = '"' . @md5_file( $local_path ) . '"';
$s3_hash = ( isset( $info['ETag'] ) ? $info['ETag'] : '' );
if ( $hash === $s3_hash ) {
return $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_OK,
'Object up-to-date.',
$file
);
}
} catch ( \Aws\Exception\AwsException $ex ) {
if ( 'NotFound' !== $ex->getAwsErrorCode() ) {
throw $ex;
}
}
}
$headers = $this->get_headers_for_file( $file );
$result = $this->_put_object(
array(
'Key' => $remote_path,
'SourceFile' => $local_path,
),
$headers
);
return $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_OK,
'OK',
$file
);
} catch ( \Exception $ex ) {
$error = sprintf( 'Unable to put object (%s).', $ex->getMessage() );
return $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_ERROR,
$error,
$file
);
}
}
/**
* Uploads a gzipped version of the file to the S3 bucket.
*
* @param array $file File descriptor containing local and remote paths.
* @param bool $force_rewrite Whether to overwrite the file if it exists.
*
* @return array Result of the upload operation.
*
* @throws \Aws\Exception\AwsException If an unexpected error occurs during the upload process.
*/
private function _upload_gzip( $file, $force_rewrite = false ) {
$local_path = $file['local_path'];
$remote_path = $file['remote_path_gzip'];
if ( ! function_exists( 'gzencode' ) ) {
return $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_ERROR,
"GZIP library doesn't exist.",
$file
);
}
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 );
if ( false === $contents ) {
return $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_ERROR,
'Unable to read file.',
$file
);
}
$data = gzencode( $contents );
try {
if ( ! $force_rewrite ) {
try {
$info = $this->api->headObject(
array(
'Bucket' => $this->_config['bucket'],
'Key' => $remote_path,
)
);
$hash = '"' . md5( $data ) . '"';
$s3_hash = ( isset( $info['ETag'] ) ? $info['ETag'] : '' );
if ( $hash === $s3_hash ) {
return $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_OK,
'Object up-to-date.',
$file
);
}
} catch ( \Aws\Exception\AwsException $ex ) {
if ( 'NotFound' !== $ex->getAwsErrorCode() ) {
throw $ex;
}
}
}
$headers = $this->get_headers_for_file( $file );
$headers['Content-Encoding'] = 'gzip';
$result = $this->_put_object(
array(
'Key' => $remote_path,
'Body' => $data,
),
$headers
);
return $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_OK,
'OK',
$file
);
} catch ( \Exception $ex ) {
$error = sprintf( 'Unable to put object (%s).', $ex->getMessage() );
return $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_ERROR,
$error,
$file
);
}
}
/**
* Uploads an object to the S3 bucket with specific headers.
*
* @param array $data Data to be uploaded, including file path and bucket details.
* @param array $headers Headers for the object being uploaded.
*
* @return \Aws\Result Result of the putObject operation.
*/
private function _put_object( $data, $headers ) {
if ( ! empty( $this->_config['s3_acl'] ) ) {
$data['ACL'] = 'public-read';
}
$data['Bucket'] = $this->_config['bucket'];
$data['ContentType'] = $headers['Content-Type'];
if ( isset( $headers['Content-Encoding'] ) ) {
$data['ContentEncoding'] = $headers['Content-Encoding'];
}
if ( isset( $headers['Cache-Control'] ) ) {
$data['CacheControl'] = $headers['Cache-Control'];
}
return $this->api->putObject( $data );
}
/**
* Deletes files from the S3 bucket.
*
* @param array $files List of files to delete with their paths.
* @param array $results Reference array to store deletion results.
*
* @return bool True if all deletions were successful, false otherwise.
*/
public function delete( $files, &$results ) {
$error = null;
try {
$this->_init();
} catch ( \Exception $ex ) {
$results = $this->_get_results( $files, W3TC_CDN_RESULT_HALT, $ex->getMessage() );
return false;
}
foreach ( $files as $file ) {
$local_path = $file['local_path'];
$remote_path = $file['remote_path'];
try {
$this->api->deleteObject(
array(
'Bucket' => $this->_config['bucket'],
'Key' => $remote_path,
)
);
$results[] = $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_OK,
'OK',
$file
);
} catch ( \Exception $ex ) {
$results[] = $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_ERROR,
sprintf( 'Unable to delete object (%s).', $ex->getMessage() ),
$file
);
}
if ( $this->_config['compression'] ) {
$remote_path_gzip = $remote_path . $this->_gzip_extension;
try {
$this->api->deleteObject(
array(
'Bucket' => $this->_config['bucket'],
'Key' => $remote_path_gzip,
)
);
$results[] = $this->_get_result(
$local_path,
$remote_path_gzip,
W3TC_CDN_RESULT_OK,
'OK',
$file
);
} catch ( \Exception $ex ) {
$results[] = $this->_get_result(
$local_path,
$remote_path_gzip,
W3TC_CDN_RESULT_ERROR,
sprintf( 'Unable to delete object (%s).', $ex->getMessage() ),
$file
);
}
}
}
return ! $this->_is_error( $results );
}
/**
* Tests the connection and configuration for the S3 bucket.
*
* @param string $error Reference to a variable to store error messages, if any.
*
* @return bool True if the test is successful, false otherwise.
*
* @throws \Exception If the bucket does not exist or if object operations fail.
*/
public function test( &$error ) {
if ( ! parent::test( $error ) ) {
return false;
}
$key = 'test_s3_' . md5( time() );
$this->_init();
$buckets = $this->api->listBuckets();
$bucket_found = false;
foreach ( $buckets['Buckets'] as $bucket ) {
if ( $bucket['Name'] === $this->_config['bucket'] ) {
$bucket_found = true;
}
}
if ( ! $bucket_found ) {
throw new \Exception(
\esc_html(
sprintf(
// translators: 1: Bucket name.
\__( 'Bucket doesn\'t exist: %1$s.', 'w3-total-cache' ),
$this->_config['bucket']
)
)
);
}
if ( ! empty( $this->_config['s3_acl'] ) ) {
$result = $this->api->putObject(
array(
'ACL' => $this->_config['s3_acl'],
'Bucket' => $this->_config['bucket'],
'Key' => $key,
'Body' => $key,
)
);
} else {
$result = $this->api->putObject(
array(
'Bucket' => $this->_config['bucket'],
'Key' => $key,
'Body' => $key,
)
);
}
$object = $this->api->getObject(
array(
'Bucket' => $this->_config['bucket'],
'Key' => $key,
)
);
if ( (string) $object['Body'] !== $key ) {
$error = 'Objects are not equal.';
$this->api->deleteObject(
array(
'Bucket' => $this->_config['bucket'],
'Key' => $key,
)
);
return false;
}
$this->api->deleteObject(
array(
'Bucket' => $this->_config['bucket'],
'Key' => $key,
)
);
return true;
}
/**
* Get the S3 bucket region id used for domains.
*
* @since 2.8.5
*
* @return string
*/
public function get_region() {
$location = $this->_config['bucket_loc_id'] ?? $this->_config['bucket_location'];
switch ( $location ) {
case 'us-east-1':
$region = '';
break;
case 'us-east-1-e':
$region = 'us-east-1.';
break;
default:
$region = $location . '.';
break;
}
return $region;
}
/**
* Retrieves the domains associated with the S3 bucket.
*
* @see self::get_region()
*
* @return array Array of domain names associated with the bucket.
*/
public function get_domains() {
$domains = array();
if ( ! empty( $this->_config['cname'] ) ) {
$domains = (array) $this->_config['cname'];
} elseif ( ! empty( $this->_config['bucket'] ) ) {
$domains = array( sprintf( '%1$s.s3.%2$samazonaws.com', $this->_config['bucket'], $this->get_region() ) );
}
return $domains;
}
/**
* Retrieves the CDN provider and bucket information.
*
* @return string Description of the provider and bucket configuration.
*/
public function get_via() {
return sprintf( 'Amazon Web Services: S3: %s', parent::get_via() );
}
/**
* Creates a new bucket in the S3 service.
*
* @return void
*
* @throws \Exception If the bucket already exists or creation fails.
*/
public function create_container() {
$this->_init();
try {
$buckets = $this->api->listBuckets();
} catch ( \Exception $ex ) {
throw new \Exception(
\esc_html(
sprintf(
// translators: 1 Error message.
\__( 'Unable to list buckets: %1$s.', 'w3-total-cache' ),
$ex->getMessage()
)
)
);
}
foreach ( $buckets['Buckets'] as $bucket ) {
if ( $bucket['Name'] === $this->_config['bucket'] ) {
throw new \Exception(
\esc_html(
sprintf(
// translators: 1 Bucket name.
\__( 'Bucket already exists: %1$s.', 'w3-total-cache' ),
$this->_config['bucket']
)
)
);
}
}
try {
$this->api->createBucket(
array(
'Bucket' => $this->_config['bucket'],
)
);
$this->api->putBucketCors(
array(
'Bucket' => $this->_config['bucket'],
'CORSConfiguration' => array(
'CORSRules' => array(
array(
'AllowedHeaders' => array( '*' ),
'AllowedMethods' => array( 'GET' ),
'AllowedOrigins' => array( '*' ),
),
),
),
)
);
} catch ( \Exception $e ) {
throw new \Exception(
\esc_html(
sprintf(
// translators: 1 Error message.
\__( 'Failed to create bucket: %1$s.', 'w3-total-cache' ),
$ex->getMessage()
)
)
);
}
}
/**
* Indicates whether the headers can be uploaded with the files.
*
* @return int W3TC_CDN_HEADER_UPLOADABLE constant indicating header support.
*/
public function headers_support() {
return W3TC_CDN_HEADER_UPLOADABLE;
}
}

View File

@@ -0,0 +1,447 @@
<?php
/**
* File: CdnEngine_S3_Compatible.php
*
* @package W3TC
*/
namespace W3TC;
if ( ! class_exists( 'S3Compatible' ) ) {
require_once W3TC_LIB_DIR . '/S3Compatible.php';
}
/**
* Class CdnEngine_S3_Compatible
*
* Amazon S3 CDN engine
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
* phpcs:disable WordPress.WP.AlternativeFunctions
*/
class CdnEngine_S3_Compatible extends CdnEngine_Base {
/**
* S3Compatible object
*
* @var S3Compatible
*/
private $_s3 = null;
/**
* Constructs the S3-compatible CDN engine instance.
*
* @param array $config Configuration options for S3-compatible storage.
*
* @return void
*/
public function __construct( $config = array() ) {
$config = array_merge(
array(
'key' => '',
'secret' => '',
'bucket' => '',
'cname' => array(),
),
$config
);
$this->_s3 = new \S3Compatible( $config['key'], $config['secret'], false, $config['api_host'] );
$this->_s3->setSignatureVersion( 'v2' );
parent::__construct( $config );
}
/**
* Formats the URL for a given file path.
*
* @param string $path The file path to format into a URL.
*
* @return string|false The formatted URL or false if the domain is unavailable.
*/
public function _format_url( $path ) {
$domain = $this->get_domain( $path );
if ( $domain ) {
$scheme = $this->_get_scheme();
// it does not support '+', requires '%2B'.
$path = str_replace( '+', '%2B', $path );
$url = sprintf( '%s://%s/%s', $scheme, $domain, $path );
return $url;
}
return false;
}
/**
* Uploads files to the S3-compatible storage.
*
* @param array $files Array of file descriptors for upload.
* @param array $results Reference to an array where upload results will be stored.
* @param bool $force_rewrite Whether to force overwriting existing files.
* @param int|null $timeout_time Optional timeout time in seconds.
*
* @return bool True if upload was successful, false otherwise.
*/
public function upload( $files, &$results, $force_rewrite = false, $timeout_time = null ) {
$error = null;
foreach ( $files as $file ) {
$local_path = $file['local_path'];
$remote_path = $file['remote_path'];
// process at least one item before timeout so that progress goes on.
if ( ! empty( $results ) && ! is_null( $timeout_time ) && time() > $timeout_time ) {
return 'timeout';
}
$results[] = $this->_upload( $file, $force_rewrite );
if ( $this->_config['compression'] && $this->_may_gzip( $remote_path ) ) {
$file['remote_path_gzip'] = $remote_path . $this->_gzip_extension;
$results[] = $this->_upload_gzip( $file, $force_rewrite );
}
}
return ! $this->_is_error( $results );
}
/**
* Uploads a single file to the S3-compatible storage.
*
* @param array $file File descriptor for upload.
* @param bool $force_rewrite Whether to force overwriting the file.
*
* @return array The result of the upload operation.
*/
public 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
);
}
if ( ! $force_rewrite ) {
$this->_set_error_handler();
$info = @$this->_s3->getObjectInfo( $this->_config['bucket'], $remote_path );
$this->_restore_error_handler();
if ( $info ) {
$hash = @md5_file( $local_path );
$s3_hash = ( isset( $info['hash'] ) ? $info['hash'] : '' );
if ( $hash === $s3_hash ) {
return $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_OK,
'Object up-to-date.',
$file
);
}
}
}
$headers = $this->get_headers_for_file( $file, array( 'ETag' => '*' ) );
$this->_set_error_handler();
$result = @$this->_s3->putObjectFile(
$local_path,
$this->_config['bucket'],
$remote_path,
\S3Compatible::ACL_PUBLIC_READ,
array(),
$headers
);
$this->_restore_error_handler();
if ( $result ) {
return $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_OK,
'OK',
$file
);
}
return $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_ERROR,
sprintf( 'Unable to put object (%s).', $this->_get_last_error() ),
$file
);
}
/**
* Uploads a gzipped version of a file to the S3-compatible storage.
*
* @param array $file File descriptor for upload.
* @param bool $force_rewrite Whether to force overwriting the file.
*
* @return array The result of the upload operation.
*/
public function _upload_gzip( $file, $force_rewrite = false ) {
$local_path = $file['local_path'];
$remote_path = $file['remote_path_gzip'];
if ( ! function_exists( 'gzencode' ) ) {
return $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_ERROR,
"GZIP library doesn't exist.",
$file
);
}
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 );
if ( false === $contents ) {
return $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_ERROR,
'Unable to read file.',
$file
);
}
$data = gzencode( $contents );
if ( ! $force_rewrite ) {
$this->_set_error_handler();
$info = @$this->_s3->getObjectInfo( $this->_config['bucket'], $remote_path );
$this->_restore_error_handler();
if ( $info ) {
$hash = md5( $data );
$s3_hash = ( isset( $info['hash'] ) ? $info['hash'] : '' );
if ( $hash === $s3_hash ) {
return $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_OK,
'Object up-to-date.',
$file
);
}
}
}
$headers = $this->get_headers_for_file( $file, array( 'ETag' => '*' ) );
$headers = array_merge(
$headers,
array(
'Vary' => 'Accept-Encoding',
'Content-Encoding' => 'gzip',
)
);
$this->_set_error_handler();
$result = @$this->_s3->putObjectString(
$data,
$this->_config['bucket'],
$remote_path,
\S3Compatible::ACL_PUBLIC_READ,
array(),
$headers
);
$this->_restore_error_handler();
if ( $result ) {
return $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_OK,
'OK',
$file
);
}
return $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_ERROR,
sprintf( 'Unable to put object (%s).', $this->_get_last_error() ),
$file
);
}
/**
* Deletes files from the S3-compatible storage.
*
* @param array $files Array of file descriptors to delete.
* @param array $results Reference to an array where deletion results will be stored.
*
* @return bool True if deletion was successful, false otherwise.
*/
public function delete( $files, &$results ) {
$error = null;
foreach ( $files as $file ) {
$local_path = $file['local_path'];
$remote_path = $file['remote_path'];
$this->_set_error_handler();
$result = @$this->_s3->deleteObject( $this->_config['bucket'], $remote_path );
$this->_restore_error_handler();
if ( $result ) {
$results[] = $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_OK,
'OK',
$file
);
} else {
$results[] = $this->_get_result(
$local_path,
$remote_path,
W3TC_CDN_RESULT_ERROR,
sprintf( 'Unable to delete object (%s).', $this->_get_last_error() ),
$file
);
}
if ( $this->_config['compression'] ) {
$remote_path_gzip = $remote_path . $this->_gzip_extension;
$this->_set_error_handler();
$result = @$this->_s3->deleteObject( $this->_config['bucket'], $remote_path_gzip );
$this->_restore_error_handler();
if ( $result ) {
$results[] = $this->_get_result(
$local_path,
$remote_path_gzip,
W3TC_CDN_RESULT_OK,
'OK',
$file
);
} else {
$results[] = $this->_get_result(
$local_path,
$remote_path_gzip,
W3TC_CDN_RESULT_ERROR,
sprintf( 'Unable to delete object (%s).', $this->_get_last_error() ),
$file
);
}
}
}
return ! $this->_is_error( $results );
}
/**
* Tests the S3-compatible storage connection.
*
* @param string $error Reference to a string where error messages will be stored.
*
* @return bool True if the connection test passes, false otherwise.
*/
public function test( &$error ) {
if ( ! parent::test( $error ) ) {
return false;
}
$string = 'test_s3_' . md5( time() );
$this->_set_error_handler();
if (
! @$this->_s3->putObjectString(
$string,
$this->_config['bucket'],
$string,
\S3Compatible::ACL_PUBLIC_READ
)
) {
$error = sprintf( 'Unable to put object (%s).', $this->_get_last_error() );
$this->_restore_error_handler();
return false;
}
$object = @$this->_s3->getObject( $this->_config['bucket'], $string );
if ( ! $object ) {
$error = sprintf( 'Unable to get object (%s).', $this->_get_last_error() );
$this->_restore_error_handler();
return false;
}
if ( (string) $object->body !== $string ) {
$error = 'Objects are not equal.';
@$this->_s3->deleteObject( $this->_config['bucket'], $string );
$this->_restore_error_handler();
return false;
}
if ( ! @$this->_s3->deleteObject( $this->_config['bucket'], $string ) ) {
$error = sprintf( 'Unable to delete object (%s).', $this->_get_last_error() );
$this->_restore_error_handler();
return false;
}
$this->_restore_error_handler();
return true;
}
/**
* Retrieves the configured domains for the S3-compatible storage.
*
* @return array List of domains.
*/
public function get_domains() {
return (array) $this->_config['cname'];
}
/**
* Retrieves a descriptive string indicating the type of CDN in use including domain.
*
* @return string The description of the CDN including domain.
*/
public function get_via() {
return sprintf( 'S3-compatible: %s', parent::get_via() );
}
/**
* Checks if the storage supports custom headers.
*
* @return int Flag indicating header support capability.
*/
public function headers_support() {
return W3TC_CDN_HEADER_UPLOADABLE;
}
}

View File

@@ -0,0 +1,631 @@
<?php
/**
* File: Cdn_AdminActions.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cdn_AdminActions
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
*/
class Cdn_AdminActions {
/**
* Config
*
* @var Config $_config
*/
private $_config = null;
/**
* Constructor for the Cdn_AdminActions class.
*
* Initializes the configuration instance used within the class.
*
* @return void
*/
public function __construct() {
$this->_config = Dispatcher::config();
}
/**
* Handles various CDN queue actions such as delete, empty, and process.
*
* Depending on the specified queue action, this method performs operations
* like deleting specific queue items, emptying the queue, or processing queued items.
* Outputs a popup with the current state of the queue.
*
* @return void
*/
public function w3tc_cdn_queue() {
$w3_plugin_cdn = Dispatcher::component( 'Cdn_Core_Admin' );
$cdn_queue_action = Util_Request::get_string( 'cdn_queue_action' );
$cdn_queue_tab = Util_Request::get_string( 'cdn_queue_tab' );
$notes = array();
switch ( $cdn_queue_tab ) {
case 'upload':
case 'delete':
case 'purge':
break;
default:
$cdn_queue_tab = 'upload';
}
switch ( $cdn_queue_action ) {
case 'delete':
$cdn_queue_id = Util_Request::get_integer( 'cdn_queue_id' );
if ( ! empty( $cdn_queue_id ) ) {
$w3_plugin_cdn->queue_delete( $cdn_queue_id );
$notes[] = __( 'File successfully deleted from the queue.', 'w3-total-cache' );
}
break;
case 'empty':
$cdn_queue_type = Util_Request::get_integer( 'cdn_queue_type' );
if ( ! empty( $cdn_queue_type ) ) {
$w3_plugin_cdn->queue_empty( $cdn_queue_type );
$notes[] = __( 'Queue successfully emptied.', 'w3-total-cache' );
}
break;
case 'process':
$w3_plugin_cdn_normal = Dispatcher::component( 'Cdn_Plugin' );
$n = $w3_plugin_cdn_normal->cron_queue_process();
$notes[] = sprintf(
// Translators: 1 number of processed queue items.
__(
'Number of processed queue items: %1$d',
'w3-total-cache'
),
$n
);
break;
}
$nonce = wp_create_nonce( 'w3tc' );
$queue = $w3_plugin_cdn->queue_get();
$title = __( 'Unsuccessful file transfer queue.', 'w3-total-cache' );
include W3TC_INC_DIR . '/popup/cdn_queue.php';
}
/**
* Displays the Media Library export popup.
*
* This method retrieves the total count of media attachments and loads
* the export popup for initiating the export process.
*
* @return void
*/
public function w3tc_cdn_export_library() {
$w3_plugin_cdn = Dispatcher::component( 'Cdn_Core_Admin' );
$total = $w3_plugin_cdn->get_attachments_count();
$title = __( 'Media Library export', 'w3-total-cache' );
include W3TC_INC_DIR . '/popup/cdn_export_library.php';
}
/**
* Flushes all CDN caches.
*
* Performs a complete purge of the CDN cache and attempts to execute
* any delayed operations. Redirects the user with a success or error message.
*
* @return void
*/
public function w3tc_cdn_flush() {
$flush = Dispatcher::component( 'CacheFlush' );
$flush->flush_all( array( 'only' => 'cdn' ) );
$status = $flush->execute_delayed_operations();
$errors = array();
foreach ( $status as $i ) {
if ( isset( $i['error'] ) ) {
$errors[] = $i['error'];
}
}
if ( empty( $errors ) ) {
Util_Admin::redirect( array( 'w3tc_note' => 'flush_cdn' ), true );
} else {
Util_Admin::redirect_with_custom_messages2( array( 'errors' => array( 'Failed to purge CDN: ' . implode( ', ', $errors ) ) ), true );
}
}
/**
* Processes the Media Library export in chunks.
*
* Exports media library files to the CDN in a paginated fashion based on
* the specified limit and offset. Returns the progress and results as a JSON response.
*
* @return void
*/
public function w3tc_cdn_export_library_process() {
$w3_plugin_cdn = Dispatcher::component( 'Cdn_Core_Admin' );
$limit = Util_Request::get_integer( 'limit' );
$offset = Util_Request::get_integer( 'offset' );
$count = null;
$total = null;
$results = array();
$w3_plugin_cdn->export_library( $limit, $offset, $count, $total, $results, time() + 120 );
$response = array(
'limit' => $limit,
'offset' => $offset,
'count' => $count,
'total' => $total,
'results' => $results,
);
echo wp_json_encode( $response );
}
/**
* Displays the Media Library import popup.
*
* Prepares the data required for importing the Media Library from the CDN,
* including the total count of posts and the CDN domain.
*
* @return void
*/
public function w3tc_cdn_import_library() {
$w3_plugin_cdn = Dispatcher::component( 'Cdn_Core_Admin' );
$common = Dispatcher::component( 'Cdn_Core' );
$cdn = $common->get_cdn();
$total = $w3_plugin_cdn->get_import_posts_count();
$cdn_host = $cdn->get_domain();
$title = __( 'Media Library import', 'w3-total-cache' );
include W3TC_INC_DIR . '/popup/cdn_import_library.php';
}
/**
* Processes the Media Library import in chunks.
*
* Imports media library files from the CDN in a paginated fashion based on
* the specified limit and offset. Returns the progress and results as a JSON response.
*
* @return void
*/
public function w3tc_cdn_import_library_process() {
$w3_plugin_cdn = Dispatcher::component( 'Cdn_Core_Admin' );
$limit = Util_Request::get_integer( 'limit' );
$offset = Util_Request::get_integer( 'offset' );
$import_external = Util_Request::get_boolean( 'cdn_import_external' );
$config_state = Dispatcher::config_state();
$config_state->set( 'cdn.import.external', $import_external );
$config_state->save();
$count = null;
$total = null;
$results = array();
@$w3_plugin_cdn->import_library( $limit, $offset, $count, $total, $results );
$response = array(
'limit' => $limit,
'offset' => $offset,
'count' => $count,
'total' => $total,
'results' => $results,
);
echo wp_json_encode( $response );
}
/**
* Displays the Modify Attachment URLs popup.
*
* Retrieves the total count of posts requiring URL renaming and loads the popup
* for initiating the domain rename process.
*
* @return void
*/
public function w3tc_cdn_rename_domain() {
$w3_plugin_cdn = Dispatcher::component( 'Cdn_Core_Admin' );
$total = $w3_plugin_cdn->get_rename_posts_count();
$title = __( 'Modify attachment URLs', 'w3-total-cache' );
include W3TC_INC_DIR . '/popup/cdn_rename_domain.php';
}
/**
* Processes the modification of attachment URLs in chunks.
*
* Updates attachment URLs to use the new CDN domain in a paginated fashion
* based on the specified limit and offset. Returns the progress and results as a JSON response.
*
* @return void
*/
public function w3tc_cdn_rename_domain_process() {
$w3_plugin_cdn = Dispatcher::component( 'Cdn_Core_Admin' );
$limit = Util_Request::get_integer( 'limit' );
$offset = Util_Request::get_integer( 'offset' );
$names = Util_Request::get_array( 'names' );
$count = null;
$total = null;
$results = array();
@$w3_plugin_cdn->rename_domain( $names, $limit, $offset, $count, $total, $results );
$response = array(
'limit' => $limit,
'offset' => $offset,
'count' => $count,
'total' => $total,
'results' => $results,
);
echo wp_json_encode( $response );
}
/**
* Handles the export of files to the Content Delivery Network (CDN).
*
* Based on the selected export type (includes, theme, minify, or custom),
* this method retrieves the corresponding files and prepares them for export.
*
* @return void
*/
public function w3tc_cdn_export() {
$w3_plugin_cdn = Dispatcher::component( 'Cdn_Plugin' );
$cdn_export_type = Util_Request::get_string( 'cdn_export_type', 'custom' );
switch ( $cdn_export_type ) {
case 'includes':
$title = __( 'Includes files export', 'w3-total-cache' );
$files = $w3_plugin_cdn->get_files_includes();
break;
case 'theme':
$title = __( 'Theme files export', 'w3-total-cache' );
$files = $w3_plugin_cdn->get_files_theme();
break;
case 'minify':
$title = __( 'Minify files export', 'w3-total-cache' );
$files = $w3_plugin_cdn->get_files_minify();
break;
case 'custom':
$title = __( 'Custom files export', 'w3-total-cache' );
$files = $w3_plugin_cdn->get_files_custom();
break;
default:
$title = __( 'Unknown files export', 'w3-total-cache' );
$files = array();
break;
}
include W3TC_INC_DIR . '/popup/cdn_export_file.php';
}
/**
* Processes the file export to the CDN.
*
* This method handles the upload of files to the CDN by constructing file descriptors,
* performing the upload, and generating a JSON-encoded response with the results.
*
* @return void
*/
public function w3tc_cdn_export_process() {
$common = Dispatcher::component( 'Cdn_Core' );
$files = Util_Request::get_array( 'files' );
$upload = array();
$results = array();
foreach ( $files as $file ) {
$local_path = $common->docroot_filename_to_absolute_path( $file );
$remote_path = $common->uri_to_cdn_uri( $common->docroot_filename_to_uri( $file ) );
$d = $common->build_file_descriptor( $local_path, $remote_path );
$d['_original_id'] = $file;
$upload[] = $d;
}
$common->upload( $upload, false, $results, time() + 5 );
$output = array();
foreach ( $results as $item ) {
$file = '';
if ( isset( $item['descriptor']['_original_id'] ) ) {
$file = $item['descriptor']['_original_id'];
}
$output[] = array(
'result' => $item['result'],
'error' => $item['error'],
'file' => $file,
);
}
$response = array(
'results' => $output,
);
echo wp_json_encode( $response );
}
/**
* Displays the CDN purge tool.
*
* This method prepares data for the CDN purge tool and includes the required popup template for user interaction.
*
* @return void
*/
public function w3tc_cdn_purge() {
$title = __( 'Content Delivery Network (CDN): Purge Tool', 'w3-total-cache' );
$results = array();
$path = ltrim( str_replace( get_home_url(), '', get_stylesheet_directory_uri() ), '/' );
include W3TC_INC_DIR . '/popup/cdn_purge.php';
}
/**
* Processes the purging of specific files from the CDN.
*
* This method collects files from the request, constructs purge descriptors, and processes the purge via the CDN component.
* Results are displayed in the purge tool.
*
* @return void
*/
public function w3tc_cdn_purge_files() {
$title = __( 'Content Delivery Network (CDN): Purge Tool', 'w3-total-cache' );
$results = array();
$files = Util_Request::get_array( 'files' );
$purge = array();
$common = Dispatcher::component( 'Cdn_Core' );
foreach ( $files as $file ) {
$local_path = $common->docroot_filename_to_absolute_path( $file );
$remote_path = $common->uri_to_cdn_uri( $common->docroot_filename_to_uri( $file ) );
$purge[] = $common->build_file_descriptor( $local_path, $remote_path );
}
if ( count( $purge ) ) {
$common->purge( $purge, $results );
} else {
$errors[] = __( 'Empty files list.', 'w3-total-cache' );
}
$path = str_replace( get_home_url(), '', get_stylesheet_directory_uri() );
include W3TC_INC_DIR . '/popup/cdn_purge.php';
}
/**
* Purges a specific attachment from the CDN.
*
* This method handles the purging of an attachment identified by its ID.
* It redirects the user with a success or error notice upon completion.
*
* @return void
*/
public function w3tc_cdn_purge_attachment() {
$results = array();
$attachment_id = Util_Request::get_integer( 'attachment_id' );
$w3_plugin_cdn = Dispatcher::component( 'Cdn_Core_Admin' );
if ( $w3_plugin_cdn->purge_attachment( $attachment_id, $results ) ) {
Util_Admin::redirect( array( 'w3tc_note' => 'cdn_purge_attachment' ), true );
} else {
Util_Admin::redirect( array( 'w3tc_error' => 'cdn_purge_attachment' ), true );
}
}
/**
* Tests the connection and configuration for a specified CDN engine.
*
* This method validates the CDN configuration by performing a test against the specified engine.
* A JSON-encoded response is generated with the result and any error message.
*
* @return void
*/
public function w3tc_cdn_test() {
$engine = Util_Request::get_string( 'engine' );
$config = Util_Request::get_array( 'config' );
// TODO: Workaround to support test case cdn/a04.
if ( 'ftp' === $engine && ! isset( $config['host'] ) ) {
$config = Util_Request::get_string( 'config' );
$config = json_decode( $config, true );
}
$config = array_merge( $config, array( 'debug' => false ) );
if ( isset( $config['domain'] ) && ! is_array( $config['domain'] ) ) {
$config['domain'] = explode( ',', $config['domain'] );
}
if ( Cdn_Util::is_engine( $engine ) ) {
$result = true;
$error = null;
} else {
$result = false;
$error = __( 'Incorrect engine ', 'w3-total-cache' ) . $engine;
}
if ( ! isset( $config['docroot'] ) ) {
$config['docroot'] = Util_Environment::document_root();
}
if ( $result ) {
if (
'google_drive' === $engine ||
'transparentcdn' === $engine ||
'rackspace_cdn' === $engine ||
'rscf' === $engine ||
'bunnycdn' === $engine ||
's3_compatible' === $engine
) {
// those use already stored w3tc config.
$w3_cdn = Dispatcher::component( 'Cdn_Core' )->get_cdn();
} else {
// those use dynamic config from the page.
$w3_cdn = CdnEngine::instance( $engine, $config );
}
@set_time_limit( $this->_config->get_integer( 'timelimit.cdn_test' ) ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
try {
if ( $w3_cdn->test( $error ) ) {
$result = true;
$error = __( 'Test passed', 'w3-total-cache' );
} else {
$result = false;
$error = sprintf(
// Translators: 1 error message.
__(
'Error: %1$s',
'w3-total-cache'
),
$error
);
}
} catch ( \Exception $ex ) {
$result = false;
$error = sprintf(
// Translators: 1 error message.
__(
'Error: %s',
'w3-total-cache'
),
$ex->getMessage()
);
}
}
$response = array(
'result' => $result,
'error' => $error,
);
echo wp_json_encode( $response );
}
/**
* Handles the creation of a CDN container.
*
* This method is responsible for creating a new container for supported CDN engines
* such as Amazon S3, CloudFront, Azure, and others. It retrieves configuration details
* from the request, attempts to create the container, and outputs a JSON-encoded response
* with the result and any error message.
*
* @return void
*/
public function w3tc_cdn_create_container() {
$engine = Util_Request::get_string( 'engine' );
$config = Util_Request::get_array( 'config' );
$config = array_merge( $config, array( 'debug' => false ) );
$container_id = '';
switch ( $engine ) {
case 's3':
case 'cf':
case 'cf2':
case 'azure':
case 'azuremi':
$w3_cdn = CdnEngine::instance( $engine, $config );
@set_time_limit( $this->_config->get_integer( 'timelimit.cdn_upload' ) ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
$result = false;
try {
$container_id = $w3_cdn->create_container();
$result = true;
$error = __( 'Created successfully.', 'w3-total-cache' );
} catch ( \Exception $ex ) {
$error = sprintf(
// Translators: 1 error message.
__(
'Error: %1$s',
'w3-total-cache'
),
$ex->getMessage()
);
}
break;
default:
$result = false;
$error = __( 'Incorrect type.', 'w3-total-cache' );
}
$response = array(
'result' => $result,
'error' => $error,
'container_id' => $container_id,
);
echo wp_json_encode( $response );
}
/**
* Redirects to the BunnyCDN signup page and tracks the signup event.
*
* This method logs the time of the BunnyCDN signup action and redirects the user
* to the BunnyCDN signup URL. Any errors during state saving are ignored.
*
* @since 2.6.0
*
* @return void
*/
public function w3tc_cdn_bunnycdn_signup() {
try {
$state = Dispatcher::config_state();
$state->set( 'track.bunnycdn_signup', time() );
$state->save();
} catch ( \Exception $ex ) {} // phpcs:ignore
Util_Environment::redirect( W3TC_BUNNYCDN_SIGNUP_URL );
}
/**
* Tests the accessibility of a given CDN URL.
*
* This private method checks the accessibility of a specified CDN URL by sending a GET request
* and verifying the response code. It returns true if the URL is accessible (HTTP 200 response),
* or false otherwise.
*
* @param string $url The URL to test.
*
* @return bool True if the URL is accessible, false otherwise.
*/
private function test_cdn_url( $url ) {
$response = wp_remote_get( $url );
if ( is_wp_error( $response ) ) {
return false;
} else {
$code = wp_remote_retrieve_response_code( $response );
return 200 === $code;
}
}
}

View File

@@ -0,0 +1,342 @@
<?php
/**
* File: Cdn_AdminNotes.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class: Cdn_AdminNotes
*
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable WordPress.DB.DirectDatabaseQuery
*/
class Cdn_AdminNotes {
/**
* Adds CDN-related notes to the provided array of admin notices.
*
* This method checks various conditions related to the CDN configuration,
* WordPress upgrades, theme changes, and other CDN settings to determine
* which admin notices should be displayed.
*
* phpcs:disable WordPress.Arrays.MultipleStatementAlignment.DoubleArrowNotAligned
*
* @param array $notes Array of existing admin notices.
*
* @return array Updated array of admin notices.
*/
public function w3tc_notes( array $notes ): array {
$config = Dispatcher::config();
$state = Dispatcher::config_state();
$cdn_engine = $config->get_string( 'cdn.engine' );
$page = Util_Request::get_string( 'page' );
if ( ! Cdn_Util::is_engine_mirror( $cdn_engine ) ) {
/**
* Show notification after theme change.
*/
if ( $state->get_boolean( 'cdn.show_note_theme_changed' ) ) {
$notes['cdn_theme_changed'] = sprintf(
// translators: 1: Button code, 2: Button code.
__( 'The active theme has changed, please %1$s now to ensure proper operation. %2$s', 'w3-total-cache' ),
Util_Ui::button_popup( __( 'upload active theme files', 'w3-total-cache' ), 'cdn_export', 'cdn_export_type=theme' ),
Util_Ui::button_hide_note2(
array(
'w3tc_default_config_state' => 'y',
'key' => 'cdn.show_note_theme_changed',
'value' => 'false',
)
)
);
}
/**
* Show notification after WP upgrade.
*/
if ( $state->get_boolean( 'cdn.show_note_wp_upgraded' ) ) {
$notes['cdn_wp_upgraded'] = sprintf(
// translators: 1: Button code, 2: Button code.
__( 'Upgraded WordPress? Please %1$s files now to ensure proper operation. %2$s', 'w3-total-cache' ),
Util_Ui::button_popup( 'upload wp-includes', 'cdn_export', 'cdn_export_type=includes' ),
Util_Ui::button_hide_note2(
array(
'w3tc_default_config_state' => 'y',
'key' => 'cdn.show_note_wp_upgraded',
'value' => 'false',
)
)
);
}
/**
* Show notification after CDN enable.
*/
if ( $state->get_boolean( 'cdn.show_note_cdn_upload' ) ||
$state->get_boolean( 'cdn.show_note_cdn_reupload' ) ) {
$cdn_upload_buttons = array();
if ( $config->get_boolean( 'cdn.includes.enable' ) ) {
$cdn_upload_buttons[] = Util_Ui::button_popup( 'wp-includes', 'cdn_export', 'cdn_export_type=includes' );
}
if ( $config->get_boolean( 'cdn.theme.enable' ) ) {
$cdn_upload_buttons[] = Util_Ui::button_popup( 'theme files', 'cdn_export', 'cdn_export_type=theme' );
}
if (
$config->get_boolean( 'minify.enabled' ) &&
$config->get_boolean( 'cdn.minify.enable' ) &&
! $config->get_boolean( 'minify.auto' )
) {
$cdn_upload_buttons[] = Util_Ui::button_popup( 'minify files', 'cdn_export', 'cdn_export_type=minify' );
}
if ( $config->get_boolean( 'cdn.custom.enable' ) ) {
$cdn_upload_buttons[] = Util_Ui::button_popup( 'custom files', 'cdn_export', 'cdn_export_type=custom' );
}
if ( $state->get_boolean( 'cdn.show_note_cdn_upload' ) ) {
$notes[] = sprintf(
// translators: 1: Button code, 2: Button code, 3: Button code.
__(
'Make sure to %1$s and upload the %2$s, files to the <acronym title="Content Delivery Network">CDN</acronym> to ensure proper operation. %3$s',
'w3-total-cache'
),
Util_Ui::button_popup( 'export the media library', 'cdn_export_library' ),
implode( ', ', $cdn_upload_buttons ),
Util_Ui::button_hide_note2(
array(
'w3tc_default_config_state' => 'y',
'key' => 'cdn.show_note_cdn_upload',
'value' => 'false',
)
)
);
}
if ( $state->get_boolean( 'cdn.show_note_cdn_reupload' ) ) {
$notes[] = sprintf(
// translators: 1: Button code, 2: Button code, 3: Button code.
__(
'Settings that affect Browser Cache settings for files hosted by the CDN have been changed. To apply the new settings %1$s and %2$s. %3$s',
'w3-total-cache'
),
Util_Ui::button_popup( __( 'export the media library', 'w3-total-cache' ), 'cdn_export_library' ),
implode( ', ', $cdn_upload_buttons ),
Util_Ui::button_hide_note2(
array(
'w3tc_default_config_state' => 'y',
'key' => 'cdn.show_note_cdn_reupload',
'value' => 'false',
)
)
);
}
}
}
/**
* Check CURL extension.
*/
if ( ! $state->get_boolean( 'cdn.hide_note_no_curl' ) && ! function_exists( 'curl_init' ) ) {
$notes[] = sprintf(
// translators: 1: Button code.
__( 'The <strong>CURL PHP</strong> extension is not available. Please install it to enable S3 or CloudFront functionality. %1$s', 'w3-total-cache' ),
Util_Ui::button_hide_note2(
array(
'w3tc_default_config_state' => 'y',
'key' => 'cdn.hide_note_no_curl',
'value' => 'true',
)
)
);
}
return $notes;
}
/**
* Adds CDN-related errors to the provided array of admin errors.
*
* This method checks for issues such as upload queue errors, configuration
* inconsistencies, and missing settings. It ensures the errors array includes
* relevant information for resolving CDN-related problems.
*
* @param array $errors Array of existing admin errors.
*
* @return array Updated array of admin errors.
*/
public function w3tc_errors( array $errors ): array {
$c = Dispatcher::config();
$state = Dispatcher::config_state();
$cdn_engine = $c->get_string( 'cdn.engine' );
if ( Cdn_Util::is_engine_push( $cdn_engine ) ) {
/**
* Show notification if upload queue is not empty.
*/
try {
$error = get_transient( 'w3tc_cdn_error' );
if ( ! $error && ! $this->_is_queue_empty() ) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure
$errors['cdn_unsuccessful_queue'] = sprintf(
// translators: 1: Button code.
__( 'The %1$s has unresolved errors. Empty the queue to restore normal operation.', 'w3-total-cache' ),
Util_Ui::button_popup( __( 'unsuccessful transfer queue', 'w3-total-cache' ), 'cdn_queue' )
);
} elseif ( $error ) {
$errors['cdn_generic'] = $error;
}
} catch ( \Exception $ex ) {
$errors[] = $ex->getMessage();
set_transient( 'w3tc_cdn_error', $ex->getMessage(), 30 );
}
/**
* Check upload settings.
*/
$upload_info = Util_Http::upload_info();
if ( ! $upload_info ) {
$upload_path = get_option( 'upload_path' );
$upload_path = trim( $upload_path );
if ( empty( $upload_path ) ) {
$upload_path = WP_CONTENT_DIR . '/uploads';
$errors['cdn_uploads_folder_empty'] = sprintf(
// translators: 1: Upload path.
__( 'The uploads directory is not available. Default WordPress directories will be created: <strong>%1$s</strong>.', 'w3-total-cache' ),
$upload_path
);
}
if ( ! Util_Environment::is_wpmu() ) {
$errors['cdn_uploads_folder_not_found'] = sprintf(
// translators: 1: Upload path, 2: Button code.
__(
'The uploads path found in the database (%1$s) is inconsistent with the actual path. Please manually adjust the upload path either in miscellaneous settings or if not using a custom path %2$s automatically to resolve the issue.',
'w3-total-cache'
),
$upload_path,
Util_Ui::button_link( __( 'update the path', 'w3-total-cache' ), Util_Ui::url( array( 'w3tc_config_update_upload_path' => 'y' ) ) )
);
}
}
}
/**
* Check CDN settings
*/
$error = '';
switch ( true ) {
case ( 'ftp' === $cdn_engine && ! count( $c->get_array( 'cdn.ftp.domain' ) ) ):
$errors['cdn_ftp_empty'] = __(
'A configuration issue prevents <acronym title="Content Delivery Network">CDN</acronym> from working: The <strong>"Replace default hostname with"</strong> field cannot be empty. Enter <acronym title="Content Delivery Network">CDN</acronym> provider hostname <a href="?page=w3tc_cdn#configuration">here</a>. <em>(This is the hostname used in order to view objects in a browser.)</em>',
'w3-total-cache'
);
break;
case ( 's3' === $cdn_engine && ( empty( $c->get_string( 'cdn.s3.key' ) ) || empty( $c->get_string( 'cdn.s3.secret' ) ) || empty( $c->get_string( 'cdn.s3.bucket' ) ) ) ):
$error = __( 'The <strong>"Access key", "Secret key" and "Bucket"</strong> fields cannot be empty.', 'w3-total-cache' );
break;
case ( 'cf' === $cdn_engine && ( empty( $c->get_string( 'cdn.cf.key' ) ) || empty( $c->get_string( 'cdn.cf.secret' ) ) || empty( $c->get_string( 'cdn.cf.bucket' ) ) || ( empty( $c->get_string( 'cdn.cf.id' ) ) && empty( $c->get_array( 'cdn.cf.cname' ) ) ) ) ):
$error = __( 'The <strong>"Access key", "Secret key", "Bucket" and "Replace default hostname with"</strong> fields cannot be empty.', 'w3-total-cache' );
break;
case ( 'cf2' === $cdn_engine && ( empty( $c->get_string( 'cdn.cf2.key' ) ) || empty( $c->get_string( 'cdn.cf2.secret' ) ) || ( empty( $c->get_string( 'cdn.cf2.id' ) ) && empty( $c->get_array( 'cdn.cf2.cname' ) ) ) ) ):
$error = __( 'The <strong>"Access key", "Secret key" and "Replace default hostname with"</strong> fields cannot be empty.', 'w3-total-cache' );
break;
case ( 'rscf' === $cdn_engine && ( empty( $c->get_string( 'cdn.rscf.user' ) ) || empty( $c->get_string( 'cdn.rscf.key' ) ) || empty( $c->get_string( 'cdn.rscf.container' ) ) ) ):
$error = __( 'The <strong>"Username", "API key", "Container" and "Replace default hostname with"</strong> fields cannot be empty.', 'w3-total-cache' );
break;
case ( 'azure' === $cdn_engine && ( empty( $c->get_string( 'cdn.azure.user' ) ) || empty( $c->get_string( 'cdn.azure.key' ) ) || empty( $c->get_string( 'cdn.azure.container' ) ) ) ):
$error = __( 'The <strong>"Account name", "Account key" and "Container"</strong> fields cannot be empty.', 'w3-total-cache' );
break;
case ( 'azuremi' === $cdn_engine && empty( getenv( 'APPSETTING_WEBSITE_SITE_NAME' ) ) ):
$error = __( 'Microsoft Azure using Managed Identities is only available for "WordPress on App Service".', 'w3-total-cache' );
break;
case ( 'azuremi' === $cdn_engine && ( empty( $c->get_string( 'cdn.azuremi.user' ) ) || empty( $c->get_string( 'cdn.azuremi.clientid' ) ) || empty( $c->get_string( 'cdn.azuremi.container' ) ) ) ):
$error = __( 'The <strong>"Account name", "Entra client ID" and "Container"</strong> fields cannot be empty.', 'w3-total-cache' );
break;
case ( 'mirror' === $cdn_engine && empty( $c->get_array( 'cdn.mirror.domain' ) ) ):
$error = __( 'The <strong>"Replace default hostname with"</strong> field cannot be empty.', 'w3-total-cache' );
break;
case ( 'cotendo' === $cdn_engine && empty( $c->get_array( 'cdn.cotendo.domain' ) ) ):
$error = __( 'The <strong>"Replace default hostname with"</strong> field cannot be empty.', 'w3-total-cache' );
break;
case ( 'edgecast' === $cdn_engine && empty( $c->get_array( 'cdn.edgecast.domain' ) ) ):
$error = __( 'The <strong>"Replace default hostname with"</strong> field cannot be empty.', 'w3-total-cache' );
break;
case ( 'att' === $cdn_engine && empty( $c->get_array( 'cdn.att.domain' ) ) ):
$error = __( 'The <strong>"Replace default hostname with"</strong> field cannot be empty.', 'w3-total-cache' );
break;
case ( 'akamai' === $cdn_engine && empty( $c->get_array( 'cdn.akamai.domain' ) ) ):
$error = 'The <strong>"Replace default hostname with"</strong> field cannot be empty.';
break;
}
if ( $error ) {
$errors['cdn_not_configured'] = __( 'A configuration issue prevents <acronym title="Content Delivery Network">CDN</acronym> from working: ', 'w3-total-cache' ) .
$error . __( ' <a href="?page=w3tc_cdn#configuration">Specify it here</a>.', 'w3-total-cache' );
}
return $errors;
}
/**
* Checks if the CDN queue is empty.
*
* This method checks if there are any items in the CDN queue by querying the corresponding table in the database.
* If the queue is empty, it returns true. If the table doesn't exist or there is another database error, an exception
* is thrown with a detailed error message.
*
* @global wpdb $wpdb WordPress database object.
*
* @throws \Exception If there is a database error or the table doesn't exist.
*
* @return bool True if the queue is empty, false otherwise.
*/
private function _is_queue_empty(): bool {
global $wpdb;
$wpdb->hide_errors();
$result = $wpdb->get_var( sprintf( 'SELECT COUNT(`id`) FROM `%s`', $wpdb->base_prefix . W3TC_CDN_TABLE_QUEUE ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$error = $wpdb->last_error;
if ( $error ) {
if ( strpos( $error, "doesn't exist" ) !== false ) {
$url = is_network_admin() ? network_admin_url( 'admin.php?page=w3tc_install' ) : admin_url( 'admin.php?page=w3tc_install' );
throw new \Exception(
sprintf(
// translators: 1: Error message, 2: Install link.
esc_html__( 'Encountered issue with CDN: %1$s. See %2$s for instructions of creating correct table.', 'w3-total-cache' ),
esc_html( $wpdb->last_error ),
'<a href="' . esc_url( $url ) . '">' . esc_html__( 'Install page', 'w3-total-cache' ) . '</a>'
)
);
} else {
throw new \Exception(
sprintf(
// translators: 1: Error message.
esc_html__( 'Encountered issue with CDN: %s.', 'w3-total-cache' ),
esc_html( $wpdb->last_error )
)
);
}
}
return empty( $result );
}
}

View File

@@ -0,0 +1,566 @@
<?php
/**
* File: Cdn_BunnyCdn_Api.php
*
* @since 2.6.0
* @package W3TC
*/
namespace W3TC;
/**
* Class: Cdn_BunnyCdn_Api
*
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
* phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter
*
* @since 2.6.0
*/
class Cdn_BunnyCdn_Api {
/**
* Account API Key.
*
* @since 2.6.0
*
* @var string
*/
private $account_api_key;
/**
* Storage API Key.
*
* @since 2.6.0
*
* @var string
*/
private $storage_api_key;
/**
* Stream API Key.
*
* @since 2.6.0
*
* @var string
*/
private $stream_api_key;
/**
* API type.
*
* One of: "account", "storage", "stream".
*
* @since 2.6.0
*
* @var string
*/
private $api_type;
/**
* Pull zone id.
*
* @since 2.6.0
*
* @var int
*/
private $pull_zone_id;
/**
* Default edge rules.
*
* @since 2.6.0
*
* @var array
*/
private static $default_edge_rules = array(
array(
'ActionType' => 15, // BypassPermaCache.
'TriggerMatchingType' => 0, // MatchAny.
'Enabled' => true,
'Triggers' => array(
array(
'Type' => 3, // UrlExtension.
'PatternMatchingType' => 0, // MatchAny.
'PatternMatches' => array( '.zip' ),
),
),
'Description' => 'Bypass PermaCache for ZIP files',
),
array(
'ActionType' => 3, // OverrideCacheTime.
'TriggerMatchingType' => 0, // MatchAny.
'ActionParameter1' => '0',
'ActionParameter2' => '',
'Enabled' => true,
'Triggers' => array(
array(
'Type' => 1, // RequestHeader.
'PatternMatchingType' => 0, // MatchAny.
'PatternMatches' => array(
'*wordpress_logged_in_*',
'*wordpress_sec_*',
),
'Parameter1' => 'Cookie',
),
),
'Description' => 'Override Cache Time if logged into WordPress',
),
array(
'ActionType' => 15, // BypassPermaCache.
'TriggerMatchingType' => 0, // MatchAny.
'Enabled' => true,
'Triggers' => array(
array(
'Type' => 1, // RequestHeader.
'PatternMatchingType' => 0, // MatchAny.
'PatternMatches' => array(
'*wordpress_logged_in_*',
'*wordpress_sec_*',
),
'Parameter1' => 'Cookie',
),
),
'Description' => 'Bypass PermaCache if logged into WordPress',
),
array(
'ActionType' => 16, // OverrideBrowserCacheTime.
'TriggerMatchingType' => 0, // MatchAny.
'ActionParameter1' => '0',
'Enabled' => true,
'Triggers' => array(
array(
'Type' => 1, // RequestHeader.
'PatternMatchingType' => 0, // MatchAny.
'PatternMatches' => array(
'*wordpress_logged_in_*',
'*wordpress_sec_*',
),
'Parameter1' => 'Cookie',
),
),
'Description' => 'Override Browser Cache Time if logged into WordPress',
),
);
/**
* Class constructor for initializing API keys and pull zone ID.
*
* @since 2.6.0
*
* @param array $config Configuration array containing API keys and pull zone ID.
*/
public function __construct( array $config ) {
$this->account_api_key = ! empty( $config['account_api_key'] ) ? $config['account_api_key'] : '';
$this->storage_api_key = ! empty( $config['storage_api_key'] ) ? $config['storage_api_key'] : '';
$this->stream_api_key = ! empty( $config['stream_api_key'] ) ? $config['stream_api_key'] : '';
$this->pull_zone_id = ! empty( $config['pull_zone_id'] ) ? $config['pull_zone_id'] : '';
}
/**
* Filters the timeout time.
*
* @since 2.6.0
*
* @param int $time The original timeout time.
*
* @return int The adjusted timeout time.
*/
public function filter_timeout_time( $time ) {
return 600;
}
/**
* Disables SSL verification for HTTPS requests.
*
* @since 2.6.0
*
* @param bool $verify Whether to enable SSL verification (defaults to false).
*
* @return bool False to disable SSL verification.
*/
public function https_ssl_verify( $verify = false ) {
return false;
}
/**
* Lists all pull zones.
*
* @since 2.6.0
*
* @link https://docs.bunny.net/reference/pullzonepublic_index
*
* @return array|WP_Error API response or error object.
*/
public function list_pull_zones() {
$this->api_type = 'account';
return $this->wp_remote_get( \esc_url( 'https://api.bunny.net/pullzone' ) );
}
/**
* Gets the details of a specific pull zone.
*
* @since 2.6.0
*
* @param int $id The pull zone ID.
*
* @link https://docs.bunny.net/reference/pullzonepublic_index2
*
* @return array|WP_Error API response or error object.
*/
public function get_pull_zone( $id ) {
$this->api_type = 'account';
return $this->wp_remote_get(
\esc_url( 'https://api.bunny.net/pullzone/id' . $id )
);
}
/**
* Adds a new pull zone.
*
* @since 2.6.0
*
* @param array $data Data for the new pull zone.
*
* @link https://docs.bunny.net/reference/pullzonepublic_add
*
* @return array|WP_Error API response or error object.
*
* @throws \Exception If the pull zone name is invalid.
*/
public function add_pull_zone( array $data ) {
$this->api_type = 'account';
if ( empty( $data['Name'] ) || ! \is_string( $data['Name'] ) ) { // A Name string is required, which is used for the CDN hostname.
throw new \Exception( \esc_html__( 'A pull zone name (string) is required.', 'w3-total-cache' ) );
}
if ( \preg_match( '[^\w\d-]', $data['Name'] ) ) { // Only letters, numbers, and dashes are allowed in the Name.
throw new \Exception( \esc_html__( 'A pull zone name (string) is required.', 'w3-total-cache' ) );
}
return $this->wp_remote_post(
'https://api.bunny.net/pullzone',
$data
);
}
/**
* Updates an existing pull zone.
*
* @since 2.6.0
*
* @param int $id The pull zone ID.
* @param array $data Data for updating the pull zone.
*
* @link https://docs.bunny.net/reference/pullzonepublic_updatepullzone
*
* @return array|WP_Error API response or error object.
*
* @throws \Exception If the pull zone ID is invalid.
*/
public function update_pull_zone( $id, array $data ) {
$this->api_type = 'account';
$id = empty( $this->pull_zone_id ) ? $id : $this->pull_zone_id;
if ( empty( $id ) || ! \is_int( $id ) ) {
throw new \Exception( \esc_html__( 'Invalid pull zone id.', 'w3-total-cache' ) );
}
return $this->wp_remote_post(
'https://api.bunny.net/pullzone/' . $id,
$data
);
}
/**
* Deletes a pull zone.
*
* @since 2.6.0
*
* @param int $id The pull zone ID.
*
* @link https://docs.bunny.net/reference/pullzonepublic_delete
*
* @return array|WP_Error API response or error object.
*
* @throws \Exception If the pull zone ID is invalid.
*/
public function delete_pull_zone( $id ) {
$this->api_type = 'account';
$id = empty( $this->pull_zone_id ) ? $id : $this->pull_zone_id;
if ( empty( $id ) || ! \is_int( $id ) ) {
throw new \Exception( \esc_html__( 'Invalid pull zone id.', 'w3-total-cache' ) );
}
return $this->wp_remote_post(
\esc_url( 'https://api.bunny.net/pullzone/' . $id ),
array(),
array( 'method' => 'DELETE' )
);
}
/**
* Adds a custom hostname to a pull zone.
*
* @since 2.6.0
*
* @param string $hostname The custom hostname to add.
* @param int|null $pull_zone_id The pull zone ID (optional).
*
* @link https://docs.bunny.net/reference/pullzonepublic_addhostname
*
* @return void
*
* @throws \Exception If the pull zone ID or hostname is invalid.
*/
public function add_custom_hostname( $hostname, $pull_zone_id = null ) {
$this->api_type = 'account';
$pull_zone_id = empty( $this->pull_zone_id ) ? $pull_zone_id : $this->pull_zone_id;
if ( empty( $pull_zone_id ) || ! \is_int( $pull_zone_id ) ) {
throw new \Exception( \esc_html__( 'Invalid pull zone id.', 'w3-total-cache' ) );
}
if ( empty( $hostname ) || ! \filter_var( $hostname, FILTER_VALIDATE_DOMAIN ) ) {
throw new \Exception( \esc_html__( 'Invalid hostname', 'w3-total-cache' ) . ' "' . \esc_html( $hostname ) . '".' );
}
$this->wp_remote_post(
\esc_url( 'https://api.bunny.net/pullzone/' . $pull_zone_id . '/addHostname' ),
array( 'Hostname' => $hostname )
);
}
/**
* Gets the default edge rules for the pull zone.
*
* @since 2.6.0
*
* @return array Default edge rules.
*/
public static function get_default_edge_rules() {
return self::$default_edge_rules;
}
/**
* Adds an edge rule to a pull zone.
*
* @since 2.6.0
*
* @param array $data Data for the edge rule.
* @param int|null $pull_zone_id The pull zone ID (optional).
*
* @return void
*
* @throws \Exception If any required parameters are missing or invalid.
*/
public function add_edge_rule( array $data, $pull_zone_id = null ) {
$this->api_type = 'account';
$pull_zone_id = empty( $this->pull_zone_id ) ? $pull_zone_id : $this->pull_zone_id;
if ( empty( $pull_zone_id ) || ! \is_int( $pull_zone_id ) ) {
throw new \Exception( \esc_html__( 'Invalid pull zone id.', 'w3-total-cache' ) );
}
if ( ! isset( $data['ActionType'] ) || ! \is_int( $data['ActionType'] ) || $data['ActionType'] < 0 ) {
throw new \Exception( \esc_html__( 'Invalid parameter "ActionType".', 'w3-total-cache' ) );
}
if ( ! isset( $data['TriggerMatchingType'] ) || ! \is_int( $data['TriggerMatchingType'] ) || $data['TriggerMatchingType'] < 0 ) {
throw new \Exception( \esc_html__( 'Invalid parameter "TriggerMatchingType".', 'w3-total-cache' ) );
}
if ( ! isset( $data['Enabled'] ) || ! \is_bool( $data['Enabled'] ) ) {
throw new \Exception( \esc_html__( 'Missing parameter "Enabled".', 'w3-total-cache' ) );
}
if ( empty( $data['Triggers'] ) ) {
throw new \Exception( \esc_html__( 'Missing parameter "Triggers".', 'w3-total-cache' ) );
}
$this->wp_remote_post(
\esc_url( 'https://api.bunny.net/pullzone/' . $pull_zone_id . '/edgerules/addOrUpdate' ),
$data
);
}
/**
* Purges the cache.
*
* @since 2.6.0
*
* @param array $data Data for the purge operation.
*
* @return array|WP_Error API response or error object.
*/
public function purge( array $data ) {
$this->api_type = 'account';
return $this->wp_remote_get(
\esc_url( 'https://api.bunny.net/purge' ),
$data
);
}
/**
* Purges the cache for a specific pull zone.
*
* @since 2.6.0
*
* @param int|null $pull_zone_id The pull zone ID (optional).
*
* @return void
*
* @throws \Exception If the pull zone ID is invalid.
*/
public function purge_pull_zone( $pull_zone_id = null ) {
$this->api_type = 'account';
$pull_zone_id = empty( $this->pull_zone_id ) ? $pull_zone_id : $this->pull_zone_id;
if ( empty( $pull_zone_id ) || ! \is_int( $pull_zone_id ) ) {
throw new \Exception( \esc_html__( 'Invalid pull zone id.', 'w3-total-cache' ) );
}
$this->wp_remote_post( \esc_url( 'https://api.bunny.net/pullzone/' . $pull_zone_id . '/purgeCache' ) );
}
/**
* Retrieves the appropriate API key based on the specified type.
*
* @since 2.6.0
*
* @param string|null $type The type of API key to retrieve ('account', 'storage', or 'stream').
*
* @return string The API key.
*
* @throws \Exception If the API key type is invalid or the key is empty.
*/
private function get_api_key( $type = null ) {
if ( empty( $type ) ) {
$type = $this->api_type;
}
if ( ! \in_array( $type, array( 'account', 'storage', 'stream' ), true ) ) {
throw new \Exception( \esc_html__( 'Invalid API type; must be one of "account", "storage", "stream".', 'w3-total-cache' ) );
}
if ( empty( $this->{$type . '_api_key'} ) ) {
throw new \Exception( \esc_html__( 'API key value is empty.', 'w3-total-cache' ) );
}
return $this->{$type . '_api_key'};
}
/**
* Decodes the API response.
*
* @since 2.6.0
*
* @param array|WP_Error $result The result returned from the API request.
*
* @return array The decoded response data.
*
* @throws \Exception If the response is not successful or fails to decode.
*/
private function decode_response( $result ) {
if ( \is_wp_error( $result ) ) {
throw new \Exception( \esc_html__( 'Failed to reach API endpoint', 'w3-total-cache' ) );
}
$response_body = @\json_decode( $result['body'], true );
// Throw an exception if the response code/status is not ok.
if ( ! \in_array( $result['response']['code'], array( 200, 201, 204 ), true ) ) {
$message = isset( $response_body['Message'] ) ? $response_body['Message'] : $result['body'];
throw new \Exception(
\esc_html( \__( 'Response code ', 'w3-total-cache' ) . $result['response']['code'] . ': ' . $message )
);
}
return \is_array( $response_body ) ? $response_body : array();
}
/**
* Sends a GET request to a specified URL with optional data parameters.
*
* This method sends a GET request using `wp_remote_get` to the specified URL, including optional query parameters.
* It also adds custom headers for API authentication and content type. Timeout and SSL verification filters
* are applied during the request process. The response is processed using `decode_response` method.
*
* @since 2.6.0
*
* @param string $url The URL to send the GET request to.
* @param array $data Optional. An associative array of data to send as query parameters. Default is an empty array.
*
* @return mixed The decoded response from the API request.
*/
private function wp_remote_get( $url, array $data = array() ) {
$api_key = $this->get_api_key();
\add_filter( 'http_request_timeout', array( $this, 'filter_timeout_time' ) );
\add_filter( 'https_ssl_verify', array( $this, 'https_ssl_verify' ) );
$result = \wp_remote_get(
$url . ( empty( $data ) ? '' : '?' . \http_build_query( $data ) ),
array(
'headers' => array(
'AccessKey' => $api_key,
'Accept' => 'application/json',
),
)
);
\remove_filter( 'https_ssl_verify', array( $this, 'https_ssl_verify' ) );
\remove_filter( 'http_request_timeout', array( $this, 'filter_timeout_time' ) );
return self::decode_response( $result );
}
/**
* Sends a POST request to a specified URL with optional data and additional arguments.
*
* This method sends a POST request using `wp_remote_post` to the specified URL, including optional data in the request body
* and additional arguments. Custom headers for API authentication, content type, and accept type are included in the request.
* Filters for request timeout and SSL verification are applied during the request process. The response is processed using
* `decode_response` method.
*
* @since 2.6.0
*
* @param string $url The URL to send the POST request to.
* @param array $data Optional. An associative array of data to send in the request body. Default is an empty array.
* @param array $args Optional. Additional arguments to customize the POST request, such as custom headers or settings. Default is an empty array.
*
* @return mixed The decoded response from the API request.
*/
private function wp_remote_post( $url, array $data = array(), array $args = array() ) {
$api_key = $this->get_api_key();
\add_filter( 'http_request_timeout', array( $this, 'filter_timeout_time' ) );
\add_filter( 'https_ssl_verify', array( $this, 'https_ssl_verify' ) );
$result = \wp_remote_post(
$url,
\array_merge(
array(
'headers' => array(
'AccessKey' => $api_key,
'Accept' => 'application/json',
'Content-Type' => 'application/json',
),
'body' => empty( $data ) ? null : \wp_json_encode( $data ),
),
$args
)
);
\remove_filter( 'https_ssl_verify', array( $this, 'https_ssl_verify' ) );
\remove_filter( 'http_request_timeout', array( $this, 'filter_timeout_time' ) );
return self::decode_response( $result );
}
}

View File

@@ -0,0 +1,224 @@
<?php
/**
* File: Cdn_BunnyCdn_Page.php
*
* @since 2.6.0
* @package W3TC
*/
namespace W3TC;
/**
* Class: Cdn_BunnyCdn_Page
*
* @since 2.6.0
*/
class Cdn_BunnyCdn_Page {
/**
* Handles the AJAX action to purge a specific URL from Bunny CDN.
*
* This method listens for the `w3tc_ajax_cdn_bunnycdn_purge_url` AJAX action and processes the URL purging request.
* It validates the URL, calls the Bunny CDN API to purge the URL, and sends a JSON response indicating success or failure.
*
* @since 2.6.0
*
* @return void
*/
public static function w3tc_ajax() {
$o = new Cdn_BunnyCdn_Page();
\add_action( 'w3tc_ajax_cdn_bunnycdn_purge_url', array( $o, 'w3tc_ajax_cdn_bunnycdn_purge_url' ) );
}
/**
* Checks if Bunny CDN is active and properly configured.
*
* This method verifies if Bunny CDN is enabled and configured correctly by checking the necessary configuration
* values, including the account API key and pull zone IDs. It returns true if Bunny CDN is active, and false otherwise.
*
* @since 2.6.0
*
* @return bool True if Bunny CDN is active, false if not.
*/
public static function is_active() {
$config = Dispatcher::config();
$cdn_enabled = $config->get_boolean( 'cdn.enabled' );
$cdn_engine = $config->get_string( 'cdn.engine' );
$cdn_zone_id = $config->get_integer( 'cdn.bunnycdn.pull_zone_id' );
$cdnfsd_enabled = $config->get_boolean( 'cdnfsd.enabled' );
$cdnfsd_engine = $config->get_string( 'cdnfsd.engine' );
$cdnfsd_zone_id = $config->get_integer( 'cdnfsd.bunnycdn.pull_zone_id' );
$account_api_key = $config->get_string( 'cdn.bunnycdn.account_api_key' );
return ( $account_api_key &&
(
( $cdn_enabled && 'bunnycdn' === $cdn_engine && $cdn_zone_id ) ||
( $cdnfsd_enabled && 'bunnycdn' === $cdnfsd_engine && $cdnfsd_zone_id )
)
);
}
/**
* Adds actions to the W3 Total Cache dashboard if Bunny CDN is active.
*
* This method appends a custom "Empty All Caches Except Bunny CDN" button to the W3 Total Cache dashboard if Bunny CDN
* is enabled. It also checks if other cache types can be emptied before enabling the button.
*
* @since 2.6.0
*
* @param array $actions List of existing actions in the dashboard.
*
* @return array Modified list of actions with the new button if Bunny CDN is active.
*/
public static function w3tc_dashboard_actions( array $actions ) {
if ( self::is_active() ) {
$modules = Dispatcher::component( 'ModuleStatus' );
$can_empty_memcache = $modules->can_empty_memcache();
$can_empty_opcode = $modules->can_empty_opcode();
$can_empty_file = $modules->can_empty_file();
$can_empty_varnish = $modules->can_empty_varnish();
$actions[] = sprintf(
'<input type="submit" class="dropdown-item" name="w3tc_bunnycdn_flush_all_except_bunnycdn" value="%1$s"%2$s>',
esc_attr__( 'Empty All Caches Except Bunny CDN', 'w3-total-cache' ),
( ! $can_empty_memcache && ! $can_empty_opcode && ! $can_empty_file && ! $can_empty_varnish ) ? ' disabled="disabled"' : ''
);
}
return $actions;
}
/**
* Enqueues scripts and localizes variables for Bunny CDN on the admin page.
*
* This method registers and enqueues the necessary JavaScript for Bunny CDN functionality in the W3 Total Cache admin
* panel. It also localizes important variables like authorization status and localized strings for use in the script.
*
* @since 2.6.0
*
* @return void
*/
public static function admin_print_scripts_w3tc_cdn() {
$config = Dispatcher::config();
$is_authorized = ! empty( $config->get_string( 'cdn.bunnycdn.account_api_key' ) ) &&
( $config->get_string( 'cdn.bunnycdn.pull_zone_id' ) || $config->get_string( 'cdnfsd.bunnycdn.pull_zone_id' ) );
\wp_register_script(
'w3tc_cdn_bunnycdn',
\plugins_url( 'Cdn_BunnyCdn_Page_View.js', W3TC_FILE ),
array( 'jquery' ),
W3TC_VERSION,
false
);
\wp_localize_script(
'w3tc_cdn_bunnycdn',
'W3TC_Bunnycdn',
array(
'is_authorized' => $is_authorized,
'lang' => array(
'empty_url' => \esc_html__( 'No URL specified', 'w3-total-cache' ),
'success_purging' => \esc_html__( 'Successfully purged URL', 'w3-total-cache' ),
'error_purging' => \esc_html__( 'Error purging URL', 'w3-total-cache' ),
'error_ajax' => \esc_html__( 'Error with AJAX', 'w3-total-cache' ),
),
)
);
\wp_enqueue_script( 'w3tc_cdn_bunnycdn' );
}
/**
* Displays the configuration settings for Bunny CDN in the W3 Total Cache settings page.
*
* This method includes the view file for Bunny CDN configuration options, allowing users to modify the Bunny CDN
* settings from the W3 Total Cache admin panel.
*
* @since 2.6.0
*
* @return void
*/
public static function w3tc_settings_cdn_boxarea_configuration() {
$config = Dispatcher::config();
include W3TC_DIR . '/Cdn_BunnyCdn_Page_View.php';
}
/**
* Displays the URL purge settings in the W3 Total Cache admin panel.
*
* This method includes the view file for managing the Bunny CDN URL purge functionality, where users can specify
* URLs to purge from Bunny CDN.
*
* @since 2.6.0
*
* @return void
*/
public static function w3tc_purge_urls_box() {
$config = Dispatcher::config();
include W3TC_DIR . '/Cdn_BunnyCdn_Page_View_Purge_Urls.php';
}
/**
* Processes the AJAX request to purge a specified URL from Bunny CDN.
*
* This method validates the provided URL, sends a purge request to the Bunny CDN API, and returns a JSON response
* indicating the success or failure of the operation. If the URL is invalid or an error occurs, a failure response
* is sent with the appropriate error message.
*
* Purging a URL will remove the file from the CDN cache and re-download it from your origin server.
* Please enter the exact CDN URL of each individual file.
* You can also purge folders or wildcard files using * inside of the URL path.
* Wildcard values are not supported if using Perma-Cache.
*
* @since 2.6.0
*
* @return void
*/
public function w3tc_ajax_cdn_bunnycdn_purge_url() {
$url = Util_Request::get_string( 'url' );
// Check if URL starts with "http", starts with a valid protocol, and passes a URL validation check.
if ( 0 !== \strpos( $url, 'http' ) || ! \preg_match( '~^http(s?)://(.+)~i', $url ) || ! \filter_var( $url, FILTER_VALIDATE_URL ) ) {
\wp_send_json_error(
array( 'error_message' => \esc_html__( 'Invalid URL', 'w3-total-cache' ) ),
400
);
}
$config = Dispatcher::config();
$account_api_key = $config->get_string( 'cdn.bunnycdn.account_api_key' );
$api = new Cdn_BunnyCdn_Api( array( 'account_api_key' => $account_api_key ) );
// Try to delete pull zone.
try {
$api->purge(
array(
'url' => \esc_url( $url, array( 'http', 'https' ) ),
'async' => true,
)
);
} catch ( \Exception $ex ) {
\wp_send_json_error( array( 'error_message' => $ex->getMessage() ), 422 );
}
\wp_send_json_success();
}
/**
* Flushes all caches except Bunny CDN and redirects to the W3 Total Cache settings page.
*
* This method flushes all caches except for Bunny CDN and redirects the user to the W3 Total Cache settings page with
* a success message.
*
* @since 2.6.0
*
* @return void
*/
public function w3tc_bunnycdn_flush_all_except_bunnycdn() {
Dispatcher::component( 'CacheFlush' )->flush_all( array( 'bunnycdn' => 'skip' ) );
Util_Admin::redirect( array( 'w3tc_note' => 'flush_all' ), true );
}
}

View File

@@ -0,0 +1,248 @@
/**
* File: Cdn_BunnyCdn_Page_View.js
*
* @since 2.6.0
* @package W3TC
*
* @global W3TC_Bunnycdn Localization array for info and language.
*/
jQuery(function($) {
/**
* Resize the popup modal.
*
* @param object o W3tc_Lightbox object.
*/
function w3tc_bunnycdn_resize(o) {
o.resize();
}
// Add event handlers.
$('body')
// Load the authorization form.
.on('click', '.w3tc_cdn_bunnycdn_authorize', function() {
W3tc_Lightbox.open({
id:'w3tc-overlay',
close: '',
width: 800,
height: 300,
url: ajaxurl +
'?action=w3tc_ajax&_wpnonce=' +
w3tc_nonce +
'&w3tc_action=cdn_bunnycdn_intro',
callback: w3tc_bunnycdn_resize
});
})
// Sanitize the account API key input value.
.on('change', '#w3tc-account-api-key', function() {
var $this = $(this);
$this.val($.trim($this.val().replace(/[^a-z0-9-]/g, '')));
})
// Load the pull zone selection form.
.on('click', '.w3tc_cdn_bunnycdn_list_pull_zones', function() {
var url = ajaxurl + '?action=w3tc_ajax&_wpnonce=' + w3tc_nonce +
'&w3tc_action=cdn_bunnycdn_list_pull_zones';
W3tc_Lightbox.load_form(url, '.w3tc_cdn_bunnycdn_form', w3tc_bunnycdn_resize);
})
// Enable/disable (readonly) add pull zone form fields based on selection.
.on('change', '#w3tc-pull-zone-id', function() {
var $selected_option = $(this).find(':selected'),
$origin = $('#w3tc-origin-url'),
$name = $('#w3tc-pull-zone-name'),
$hostnames = $('#w3tc-custom-hostnames');
if ($(this).find(':selected').val() === '') {
// Enable the add pull zone fields with suggested or entered values.
$origin.val($origin.data('suggested')).prop('readonly', false);
$name.val($name.data('suggested')).prop('readonly', false);
$hostnames.val($hostnames.data('suggested')).prop('readonly', false);
} else {
// Disable the add pull zone fields and change values using the selected option.
$origin.prop('readonly', true).val($selected_option.data('origin'));
$name.prop('readonly', true).val($selected_option.data('name'));
$hostnames.prop('readonly', true).val($selected_option.data('custom-hostnames'));
}
// Update the hidden input field for the selected pull zone id from the select option value.
$('[name="pull_zone_id"]').val($selected_option.val());
// Update the hidden input field for the selected pull zone CDN hostname from the select option value.
$('[name="cdn_hostname"]').val($selected_option.data('cdn-hostname'));
})
// Sanitize the origin URL/IP input value.
.on('change', '#w3tc-origin-url', function() {
var $this = $(this);
$this.val($.trim($this.val().toLowerCase().replace(/[^a-z0-9\.:\/-]/g, '')));
})
// Sanitize the pull zone name input value.
.on('change', '#w3tc-pull-zone-name', function() {
var $this = $(this);
$this.val($.trim($this.val().toLowerCase().replace(/[^a-z0-9-]/g, '')));
})
// Sanitize the CDN hostname input value.
.on('change', '#w3tc_bunnycdn_hostname', function() {
var $this = $(this);
$this.val($.trim($this.val().toLowerCase().replace(/(^https?:|:.+$|[^a-z0-9\.-])/g, '')));
})
// Configure pull zone.
.on('click', '.w3tc_cdn_bunnycdn_configure_pull_zone', function() {
var url = ajaxurl + '?action=w3tc_ajax&_wpnonce=' + w3tc_nonce +
'&w3tc_action=cdn_bunnycdn_configure_pull_zone';
W3tc_Lightbox.load_form(url, '.w3tc_cdn_bunnycdn_form', w3tc_bunnycdn_resize);
})
// Close the popup success modal.
.on('click', '.w3tc_cdn_bunnycdn_done', function() {
window.location = window.location + '&';
})
// Load the deauthorize form.
.on('click', '.w3tc_cdn_bunnycdn_deauthorization', function() {
W3tc_Lightbox.open({
id:'w3tc-overlay',
close: '',
width: 800,
height: 300,
url: ajaxurl +
'?action=w3tc_ajax&_wpnonce=' +
w3tc_nonce +
'&w3tc_action=cdn_bunnycdn_deauthorization',
callback: w3tc_bunnycdn_resize
});
})
// Deauthorize and optionally delete the pull zone.
.on('click', '.w3tc_cdn_bunnycdn_deauthorize', function() {
var url = ajaxurl + '?action=w3tc_ajax&_wpnonce=' + w3tc_nonce +
'&w3tc_action=cdn_bunnycdn_deauthorize';
W3tc_Lightbox.load_form(url, '.w3tc_cdn_bunnycdn_form', w3tc_bunnycdn_resize);
})
// Sanitize the purge URL list.
.on('focusout', '#w3tc-purge-urls', function () {
// Abort if Bunny CDN is not authorized.
if (! W3TC_Bunnycdn.is_authorized) {
return;
}
// Declare vars.
var $this = $(this),
$button = $('.w3tc_cdn_bunnycdn_purge_urls');
// Strip whitespace, newlines, and invalid characters.
$this.val( $this.val().replace(/^(\s)*(\r\n|\n|\r)/gm, '') );
$this.val($.trim($this.val().replace(/[^a-z0-9\.:\/\r\n*-]/g, '')));
// Enable the purge button.
$button.prop('disabled', false);
})
// Purge URLs.
.on('click', '.w3tc_cdn_bunnycdn_purge_urls', function() {
// Abort if Bunny CDN is not authorized.
if (! W3TC_Bunnycdn.is_authorized) {
return;
}
// Declare vars.
var urls_processed = 0,
list = $('#w3tc-purge-urls').val().split("\n").filter((v) => v != ''),
$messages = $('#w3tc-purge-messages'),
$this = $(this);
// Disable the button clicked and show a spinner.
$this
.prop('disabled', true)
.closest('p').addClass('lightbox-loader');
// Clear the messages div.
$messages.empty();
// Abort if nothing was submitted.
if (list.length < 1) {
$('<div/>', {
class: 'error',
text: W3TC_Bunnycdn.lang.empty_url + '.'
}).appendTo($messages);
$this.closest('p').removeClass('lightbox-loader');
return;
}
list.forEach(function(url, index, array) {
$.ajax({
method: 'POST',
url: ajaxurl,
data: {
_wpnonce: w3tc_nonce[0],
action: 'w3tc_ajax',
w3tc_action: 'cdn_bunnycdn_purge_url',
url: url
}
})
.done(function(response) {
// Possible success.
if (typeof response.success !== 'undefined') {
if (response.success) {
// Successful.
$('<div/>', {
class: 'updated',
text: W3TC_Bunnycdn.lang.success_purging + ' "' + url + '".'
}).appendTo($messages);
} else {
// Unsucessful.
$('<div/>', {
class: 'error',
text: W3TC_Bunnycdn.lang.error_purging + ' "' + url + '"; ' + response.data.error_message + '.'
}).appendTo($messages);
}
} else {
// Unknown error.
$('<div/>', {
class: 'error',
text: W3TC_Bunnycdn.lang.error_ajax + '.'
}).appendTo($messages);
}
})
.fail(function(response) {
// Failure; received a non-2xx/3xx HTTP status code.
if (typeof response.responseJSON !== 'undefined' && 'data' in response.responseJSON && 'error_message' in response.responseJSON.data) {
// An error message was passed in the response data.
$('<div/>', {
class: 'error',
text: W3TC_Bunnycdn.lang.error_purging + ' "' + url + '"; ' + response.responseJSON.data.error_message + '.'
}).appendTo($messages);
} else {
// Unknown error.
$('<div/>', {
class: 'error',
text: W3TC_Bunnycdn.lang.error_ajax + '.'
}).appendTo($messages);
}
})
.complete(function() {
urls_processed++;
// When requests are all complete, then remove the spinner.
if (urls_processed === array.length) {
$this.closest('p').removeClass('lightbox-loader');
}
});
});
});
});

View File

@@ -0,0 +1,130 @@
<?php
/**
* File: Cdn_BunnyCdn_Page_View.php
*
* Bunny CDN settings page section view.
*
* @since 2.6.0
* @package W3TC
*
* @param array $config W3TC configuration.
*/
namespace W3TC;
defined( 'W3TC' ) || die();
$account_api_key = $config->get_string( 'cdn.bunnycdn.account_api_key' );
$is_authorized = ! empty( $account_api_key ) && $config->get_string( 'cdn.bunnycdn.pull_zone_id' );
$is_unavailable = ! empty( $account_api_key ) && $config->get_string( 'cdnfsd.bunnycdn.pull_zone_id' ); // CDN is unavailable if CDN FSD is authorized for Bunny CDN.
?>
<table class="form-table">
<tr>
<th style="width: 300px;">
<label>
<?php esc_html_e( 'Account API key authorization', 'w3-total-cache' ); ?>:
</label>
</th>
<td>
<?php if ( $is_authorized ) : ?>
<input class="w3tc_cdn_bunnycdn_deauthorization button-primary" type="button" value="<?php esc_attr_e( 'Deauthorize', 'w3-total-cache' ); ?>" />
<?php else : ?>
<input class="w3tc_cdn_bunnycdn_authorize button-primary" type="button" value="<?php esc_attr_e( 'Authorize', 'w3-total-cache' ); ?>"
<?php echo ( $is_unavailable ? 'disabled' : '' ); ?> />
<?php if ( $is_unavailable ) : ?>
<div class="notice notice-info">
<p>
<?php esc_html_e( 'CDN for objects cannot be authorized if full-site delivery is already configured.', 'w3-total-cache' ); ?>
</p>
</div>
<?php endif; ?>
<?php endif; ?>
</td>
</tr>
<?php if ( $is_authorized ) : ?>
<tr>
<th><label><?php esc_html_e( 'Pull zone name:', 'w3-total-cache' ); ?></label></th>
<td class="w3tc_config_value_text">
<?php echo esc_html( $config->get_string( 'cdn.bunnycdn.name' ) ); ?>
</td>
</tr>
<tr>
<th>
<label>
<?php
echo wp_kses(
sprintf(
// translators: 1: Opening HTML acronym tag, 2: Opening HTML acronym tag, 3: Closing HTML acronym tag.
esc_html__(
'Origin %1$sURL%3$s/%2$sIP%3$s address:',
'w3-total-cache'
),
'<acronym title="' . esc_attr__( 'Universal Resource Locator', 'w3-total-cache' ) . '">',
'<acronym title="' . esc_attr__( 'Internet Protocol', 'w3-total-cache' ) . '">',
'</acronym>'
),
array(
'acronym' => array(
'title' => array(),
),
)
);
?>
</label>
</th>
<td class="w3tc_config_value_text">
<?php echo esc_html( $config->get_string( 'cdn.bunnycdn.origin_url' ) ); ?>
</td>
</tr>
<tr>
<th>
<label>
<?php
echo wp_kses(
sprintf(
// translators: 1: Opening HTML acronym tag, 2: Closing HTML acronym tag.
esc_html__(
'%1$sCDN%2$s hostname:',
'w3-total-cache'
),
'<acronym title="' . esc_attr__( 'Content Delivery Network', 'w3-total-cache' ) . '">',
'</acronym>'
),
array(
'acronym' => array(
'title' => array(),
),
)
);
?>
</label>
</th>
<td class="w3tc_config_value_text">
<input id="w3tc_bunnycdn_hostname" type="text" name="cdn__bunnycdn__cdn_hostname"
value="<?php echo esc_html( $config->get_string( 'cdn.bunnycdn.cdn_hostname' ) ); ?>" size="100" />
<p class="description">
<?php
echo wp_kses(
sprintf(
// translators: 1: Opening HTML acronym tag, 2: Closing HTML acronym tag.
esc_html__(
'The %1$sCDN%2$s hostname is used in media links on pages. For example: example.b-cdn.net',
'w3-total-cache'
),
'<acronym title="' . esc_attr__( 'Content Delivery Network', 'w3-total-cache' ) . '">',
'</acronym>'
),
array(
'acronym' => array(
'title' => array(),
),
)
);
?>
</p>
</td>
</tr>
<?php endif; ?>
</table>

View File

@@ -0,0 +1,63 @@
<?php
/**
* File: Cdn_BunnyCdn_Page_View_Purge_Urls.php
*
* Bunny CDN settings purge URLs view.
*
* @since 2.6.0
* @package W3TC
*
* @param array $config W3TC configuration.
*/
namespace W3TC;
defined( 'W3TC' ) || die;
$account_api_key = $config->get_string( 'cdn.bunnycdn.account_api_key' );
$is_authorized = ! empty( $account_api_key ) &&
( $config->get_string( 'cdn.bunnycdn.pull_zone_id' ) || $config->get_string( 'cdnfsd.bunnycdn.pull_zone_id' ) );
$placeholder = \esc_url( \home_url() . '/about-us' ) . "\r\n" . \esc_url( \home_url() . '/css/*' );
?>
<table class="form-table">
<tr>
<th style="width: 300px;">
<label>
<?php \esc_html_e( 'Purge URLs', 'w3-total-cache' ); ?>:
</label>
</th>
<td>
<textarea id="w3tc-purge-urls" class="w3tc-ignore-change" cols="60" rows="5"
placeholder="<?php echo \esc_html( $placeholder ); ?>" <?php echo ( $is_authorized ? '' : 'disabled' ); ?>></textarea>
<p><?php \esc_html_e( 'Purging a URL will remove the file from the CDN cache and re-download it from your origin server. Please enter the exact CDN URL of each individual file. You can also purge folders or wildcard files using * inside of the URL path. Wildcard values are not supported if using Perma-Cache.', 'w3-total-cache' ); ?></p>
<p>
<input class="w3tc_cdn_bunnycdn_purge_urls button-primary" type="button"
value="<?php \esc_attr_e( 'Purge URLs Now', 'w3-total-cache' ); ?>"
<?php echo ( $is_authorized ? '' : 'disabled' ); ?>/>
</p>
<?php
if ( ! $is_authorized ) :
echo wp_kses(
\sprintf(
// translators: 1: Opening HTML elements, 2: Name of the CDN service, 3: Closing HTML elements.
\esc_html__( '%1$sPlease configure %2$s in order to purge URLs.%3$s', 'w3-total-cache' ),
'<div class="notice notice-info"><p>',
'Bunny CDN',
'</p></div>'
),
array(
'div' => array(
'class' => array(),
),
'p' => array(),
)
);
else :
?>
<br />
<p><div id="w3tc-purge-messages"></div></p>
<?php endif; ?>
</td>
</tr>
</table>

View File

@@ -0,0 +1,307 @@
<?php
/**
* File: Cdn_BunnyCdn_Popup.php
*
* @since 2.6.0
* @package W3TC
*/
namespace W3TC;
/**
* Class: Cdn_BunnyCdn_Popup
*
* @since 2.6.0
*
* phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter
*/
class Cdn_BunnyCdn_Popup {
/**
* Handles the AJAX request for BunnyCDN related actions.
*
* This method registers multiple AJAX actions related to BunnyCDN,
* including displaying the intro, pulling a list of pull zones, configuring
* a pull zone, deauthorizing, and deactivating BunnyCDN.
*
* @since 2.6.0
*
* @return void
*/
public static function w3tc_ajax() {
$o = new Cdn_BunnyCdn_Popup();
\add_action(
'w3tc_ajax_cdn_bunnycdn_intro',
array( $o, 'w3tc_ajax_cdn_bunnycdn_intro' )
);
\add_action(
'w3tc_ajax_cdn_bunnycdn_list_pull_zones',
array( $o, 'w3tc_ajax_cdn_bunnycdn_list_pull_zones' )
);
\add_action(
'w3tc_ajax_cdn_bunnycdn_configure_pull_zone',
array( $o, 'w3tc_ajax_cdn_bunnycdn_configure_pull_zone' )
);
\add_action(
'w3tc_ajax_cdn_bunnycdn_deauthorization',
array( $o, 'w3tc_ajax_cdn_bunnycdn_deauthorization' )
);
\add_action(
'w3tc_ajax_cdn_bunnycdn_deauthorize',
array( $o, 'w3tc_ajax_cdn_bunnycdn_deauthorize' )
);
}
/**
* Handles the AJAX request to render the BunnyCDN introduction page.
*
* This method fetches the account API key and renders the introductory
* page for BunnyCDN configuration, including the option to provide the
* account API key if missing.
*
* @since 2.6.0
*
* @return void
*/
public function w3tc_ajax_cdn_bunnycdn_intro() {
$config = Dispatcher::config();
$account_api_key = $config->get_string( 'cdn.bunnycdn.account_api_key' );
// Ask for an account API key.
$this->render_intro(
array(
'account_api_key' => empty( $account_api_key ) ? null : $account_api_key,
)
);
}
/**
* Handles the AJAX request to list BunnyCDN pull zones.
*
* This method retrieves and displays a list of pull zones from BunnyCDN
* after authenticating with the provided account API key. If an error
* occurs, the user is prompted to reauthorize.
*
* @since 2.6.0
*
* @return void
*/
public function w3tc_ajax_cdn_bunnycdn_list_pull_zones() {
$account_api_key = Util_Request::get_string( 'account_api_key' );
$api = new Cdn_BunnyCdn_Api( array( 'account_api_key' => $account_api_key ) );
// Try to retrieve pull zones.
try {
$pull_zones = $api->list_pull_zones();
} catch ( \Exception $ex ) {
// Reauthorize: Ask for a new account API key.
$this->render_intro(
array(
'account_api_key' => empty( $account_api_key ) ? null : $account_api_key,
'error_message' => \esc_html( \__( 'Cannot list pull zones', 'w3-total-cache' ) . '; ' . $ex->getMessage() ),
)
);
}
// Save the account API key, if added or changed.
$config = Dispatcher::config();
if ( $config->get_string( 'cdn.bunnycdn.account_api_key' ) !== $account_api_key ) {
$config->set( 'cdn.bunnycdn.account_api_key', $account_api_key );
$config->save();
}
// Print the view.
$server_ip = ! empty( $_SERVER['SERVER_ADDR'] ) && \filter_var( \wp_unslash( $_SERVER['SERVER_ADDR'] ), FILTER_VALIDATE_IP ) ?
\filter_var( \wp_unslash( $_SERVER['SERVER_ADDR'] ), FILTER_SANITIZE_URL ) : null;
$details = array(
'pull_zones' => $pull_zones,
'suggested_origin_url' => \home_url(), // Suggested origin URL or IP.
'suggested_zone_name' => \substr( \str_replace( '.', '-', \wp_parse_url( \home_url(), PHP_URL_HOST ) ), 0, 60 ), // Suggested pull zone name.
'pull_zone_id' => $config->get_integer( 'cdn.bunnycdn.pull_zone_id' ),
);
include W3TC_DIR . '/Cdn_BunnyCdn_Popup_View_Pull_Zones.php';
\wp_die();
}
/**
* Handles the AJAX request to configure a BunnyCDN pull zone.
*
* This method configures an existing or new pull zone in BunnyCDN. If
* a pull zone is not selected, a new one is created. The method also
* applies default edge rules to the newly created pull zone.
*
* @since 2.6.0
*
* @see Cdn_BunnyCdn_Api::get_default_edge_rules()
*
* @return void
*/
public function w3tc_ajax_cdn_bunnycdn_configure_pull_zone() {
$config = Dispatcher::config();
$account_api_key = $config->get_string( 'cdn.bunnycdn.account_api_key' );
$pull_zone_id = Util_Request::get_integer( 'pull_zone_id' );
$origin_url = Util_Request::get_string( 'origin_url' ); // Origin URL or IP.
$name = Util_Request::get_string( 'name' ); // Pull zone name.
$cdn_hostname = Util_Request::get_string( 'cdn_hostname' ); // Pull zone CDN hostname (system).
// If not selecting a pull zone. then create a new one.
if ( empty( $pull_zone_id ) ) {
$api = new Cdn_BunnyCdn_Api( array( 'account_api_key' => $account_api_key ) );
// Try to create a new pull zone.
try {
$response = $api->add_pull_zone(
array(
'Name' => $name, // The name/hostname for the pull zone where the files will be accessible; only letters, numbers, and dashes.
'OriginUrl' => $origin_url, // Origin URL or IP (with optional port number).
'CacheErrorResponses' => true, // If enabled, bunny.net will temporarily cache error responses (304+ HTTP status codes) from your servers for 5 seconds to prevent DDoS attacks on your origin. If disabled, error responses will be set to no-cache.
'DisableCookies' => false, // Determines if the Pull Zone should automatically remove cookies from the responses.
'EnableTLS1' => false, // TLS 1.0 was deprecated in 2018.
'EnableTLS1_1' => false, // TLS 1.1 was EOL's on March 31,2020.
'ErrorPageWhitelabel' => true, // Any bunny.net branding will be removed from the error page and replaced with a generic term.
'OriginHostHeader' => \wp_parse_url( \home_url(), PHP_URL_HOST ), // Sets the host header that will be sent to the origin.
'UseStaleWhileUpdating' => true, // Serve stale content while updating. If Stale While Updating is enabled, cache will not be refreshed if the origin responds with a non-cacheable resource.
'UseStaleWhileOffline' => true, // Serve stale content if the origin is offline.
)
);
$pull_zone_id = (int) $response['Id'];
$name = $response['Name'];
$cdn_hostname = $response['Hostnames'][0]['Value'];
} catch ( \Exception $ex ) {
// Reauthorize: Ask for a new account API key.
$this->render_intro(
array(
'account_api_key' => empty( $account_api_key ) ? null : $account_api_key,
'error_message' => \esc_html( \__( 'Cannot select or add a pull zone', 'w3-total-cache' ) . '; ' . $ex->getMessage() ),
)
);
}
// Initialize an error messages array.
$error_messages = array();
// Add Edge Rules.
foreach ( Cdn_BunnyCdn_Api::get_default_edge_rules() as $edge_rule ) {
try {
$api->add_edge_rule( $edge_rule, $pull_zone_id );
} catch ( \Exception $ex ) {
$error_messages[] = sprintf(
// translators: 1: Edge Rule description/name.
\__( 'Could not add Edge Rule "%1$s".', 'w3-total-cache' ) . '; ',
\esc_html( $edge_rule['Description'] )
) . $ex->getMessage();
}
}
// Convert error messages array to a string.
$error_messages = \implode( "\r\n", $error_messages );
}
// Save configuration.
$config->set( 'cdn.bunnycdn.pull_zone_id', $pull_zone_id );
$config->set( 'cdn.bunnycdn.name', $name );
$config->set( 'cdn.bunnycdn.origin_url', $origin_url );
$config->set( 'cdn.bunnycdn.cdn_hostname', $cdn_hostname );
$config->save();
// Print success view.
include W3TC_DIR . '/Cdn_BunnyCdn_Popup_View_Configured.php';
\wp_die();
}
/**
* Handles the AJAX request for deauthorization of BunnyCDN.
*
* This method renders a page that allows the user to deauthorize BunnyCDN
* and optionally delete the pull zone associated with the current configuration.
*
* @since 2.6.0
*
* @return void
*/
public function w3tc_ajax_cdn_bunnycdn_deauthorization() {
$config = Dispatcher::config();
$origin_url = $config->get_string( 'cdn.bunnycdn.origin_url' ); // Origin URL or IP.
$name = $config->get_string( 'cdn.bunnycdn.name' ); // Pull zone name.
$cdn_hostname = $config->get_string( 'cdn.bunnycdn.cdn_hostname' ); // Pull zone CDN hostname.
$cdn_pull_zone_id = $config->get_integer( 'cdn.bunnycdn.pull_zone_id' ); // CDN pull zone id.
$cdnfsd_pull_zone_id = $config->get_integer( 'cdnfsd.bunnycdn.pull_zone_id' ); // CDN FSD pull zone id.
// Present details and ask to deauthorize and optionally delete the pull zone.
include W3TC_DIR . '/Cdn_BunnyCdn_Popup_View_Deauthorize.php';
\wp_die();
}
/**
* Handles the AJAX request to deauthorize BunnyCDN and optionally delete the pull zone.
*
* This method removes the BunnyCDN pull zone configuration and deauthorizes
* the API key. It also provides an option to delete the pull zone if requested.
*
* @since 2.6.0
*
* @return void
*/
public function w3tc_ajax_cdn_bunnycdn_deauthorize() {
$config = Dispatcher::config();
$account_api_key = $config->get_string( 'cdn.bunnycdn.account_api_key' );
$cdn_pull_zone_id = $config->get_integer( 'cdn.bunnycdn.pull_zone_id' ); // CDN pull zone id.
$cdnfsd_pull_zone_id = $config->get_integer( 'cdnfsd.bunnycdn.pull_zone_id' ); // CDN FSD pull zone id.
$delete_pull_zone = Util_Request::get_string( 'delete_pull_zone' );
// Delete pull zone, if requested.
if ( 'yes' === $delete_pull_zone ) {
$api = new Cdn_BunnyCdn_Api( array( 'account_api_key' => $account_api_key ) );
// Try to delete pull zone.
try {
$api->delete_pull_zone( $cdn_pull_zone_id );
} catch ( \Exception $ex ) {
$delete_error_message = $ex->getMessage();
}
// If the same pull zone is used for FSD, then deauthorize that too.
if ( ! empty( $cdn_pull_zone_id ) && $cdn_pull_zone_id === $cdnfsd_pull_zone_id ) {
$config->set( 'cdnfsd.bunnycdn.pull_zone_id', null );
$config->set( 'cdnfsd.bunnycdn.name', null );
$config->set( 'cdnfsd.bunnycdn.origin_url', null );
$config->set( 'cdnfsd.bunnycdn.cdn_hostname', null );
}
}
$config->set( 'cdn.bunnycdn.pull_zone_id', null );
$config->set( 'cdn.bunnycdn.name', null );
$config->set( 'cdn.bunnycdn.origin_url', null );
$config->set( 'cdn.bunnycdn.cdn_hostname', null );
$config->save();
// Print success view.
include W3TC_DIR . '/Cdn_BunnyCdn_Popup_View_Deauthorized.php';
\wp_die();
}
/**
* Renders the introductory page for BunnyCDN setup.
*
* This private method is used to render the introductory page that includes
* the BunnyCDN setup information and the option to input the account API key.
*
* @since 2.6.0
*
* @param array $details Details to pass to the view.
*
* @return void
*/
private function render_intro( array $details ) {
include W3TC_DIR . '/Cdn_BunnyCdn_Popup_View_Intro.php';
\wp_die();
}
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* File: Cdn_BunnyCdn_Popup_View_Configured.php
*
* @since 2.6.0
* @package W3TC
*/
namespace W3TC;
defined( 'W3TC' ) || die();
?>
<?php if ( ! empty( $error_messages ) ) : ?>
<div class="error">
<?php echo $error_messages; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</div>
<?php endif; ?>
<form class="w3tc_cdn_bunnycdn_form">
<div class="metabox-holder">
<?php Util_Ui::postbox_header( esc_html__( 'Success', 'w3-total-cache' ) ); ?>
<div style="text-align: center">
<?php esc_html_e( 'A pull zone has been configured successfully', 'w3-total-cache' ); ?>.<br />
</div>
<p class="submit">
<input type="button" class="w3tc_cdn_bunnycdn_done w3tc-button-save button-primary"
value="<?php esc_html_e( 'Done', 'w3-total-cache' ); ?>" />
</p>
<?php Util_Ui::postbox_footer(); ?>
</div>
</form>

View File

@@ -0,0 +1,62 @@
<?php
/**
* File: Cdnfsd_BunnyCdn_Popup_Deauthorize.php
*
* Assists to deauthorize Bunny CDN as an objects CDN and optionally delete the pull zone.
*
* @since 2.6.0
* @package W3TC
*
* @param Config $config W3TC configuration.
* @param string $origin_url Origin URL or IP.
* @param string $name Pull zone name.
* @param string $cdn_hostname CDN hostname.
* @param string $cdn_pull_zone_id CDN pull zone id.
* @param string $cdnfsd_pull_zone_id CDN FSD pull zone id.
*/
namespace W3TC;
defined( 'W3TC' ) || die;
// Determine if the same pull zone is used for CDN and CDN FSD. If so, then we'll show a message that it will deactivate both.
$is_same_zone = $cdn_pull_zone_id === $cdnfsd_pull_zone_id;
?>
<form class="w3tc_cdn_bunnycdn_form" method="post">
<input type="hidden" name="pull_zone_id" />
<div class="metabox-holder">
<?php Util_Ui::postbox_header( \esc_html__( 'Deauthorize pull zone', 'w3-total-cache' ) ); ?>
<table class="form-table">
<tr>
<td><?php \esc_html_e( 'Name', 'w3-total-cache' ); ?>:</td>
<td><?php echo \esc_html( $name ); ?></td>
</tr>
<tr>
<td><?php \esc_html_e( 'Origin URL / IP', 'w3-total-cache' ); ?>:</td>
<td><?php echo \esc_html( $origin_url ); ?></td>
</tr>
<tr>
<td><?php \esc_html_e( 'CDN hostname', 'w3-total-cache' ); ?>:</td>
<td><?php echo \esc_html( $cdn_hostname ); ?></td>
</tr>
<tr>
<td><?php \esc_html_e( 'Delete', 'w3-total-cache' ); ?>:</td>
<td>
<input id="w3tc-delete-zone" type="checkbox" name="delete_pull_zone" value="yes" /> Delete the pull zone
<?php if ( $is_same_zone ) : ?>
<p class="notice notice-warning">
<?php \esc_html_e( 'This same pull zone is used for full-site delivery. If you delete this pull zone, then full-site delivery will be deauthorized.', 'w3-total-cache' ); ?>
</p>
<?php endif; ?>
</td>
</tr>
</table>
<p class="submit">
<input type="button" class="w3tc_cdn_bunnycdn_deauthorize w3tc-button-save button-primary"
value="<?php \esc_attr_e( 'Deauthorize', 'w3-total-cache' ); ?>" />
</p>
<?php Util_Ui::postbox_footer(); ?>
</div>
</form>

View File

@@ -0,0 +1,49 @@
<?php
/**
* File: Cdnfsd_BunnyCdn_Popup_View_Deauthorized.php
*
* @since 2.6.0
* @package W3TC
*
* @param Config $config W3TC configuration.
* @param string $delete_pull_zone Delete pull zon choice ("yes").
* @param string $delete_error_message An error message if there was an error trying to delete the pull zone. String already escaped.
*/
namespace W3TC;
defined( 'W3TC' ) || die();
?>
<form class="w3tc_cdn_bunnycdn_form">
<?php if ( isset( $delete_error_message ) ) : ?>
<div class="error">
<?php
esc_html_e( 'An error occurred trying to delete the pull zone; ', 'w3-total-cache' );
echo $delete_error_message . '.'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
?>
</div>
<?php endif; ?>
<div class="metabox-holder">
<?php
Util_Ui::postbox_header(
esc_html__( 'Success', 'w3-total-cache' ) .
( isset( $delete_error_message ) ? esc_html__( ' (with an error)', 'w3-total-cache' ) : '' )
);
?>
<div style="text-align: center">
<p><?php esc_html_e( 'The objects CDN has been deauthorized', 'w3-total-cache' ); ?>.</p>
</div>
<?php if ( 'yes' === $delete_pull_zone && empty( $delete_error_message ) ) : ?>
<div style="text-align: center">
<p><?php esc_html_e( 'The pull zone has been deleted', 'w3-total-cache' ); ?>.</p>
</div>
<?php endif; ?>
<p class="submit">
<input type="button" class="w3tc_cdn_bunnycdn_done w3tc-button-save button-primary"
value="<?php esc_html_e( 'Done', 'w3-total-cache' ); ?>" />
</p>
<?php Util_Ui::postbox_footer(); ?>
</div>
</form>

View File

@@ -0,0 +1,54 @@
<?php
/**
* File: Cdn_BunnyCdn_Popup_View_Intro.php
*
* Assists with configuring Bunny CDN as an object storage CDN.
* Asks to enter an account API key from the Bunny CDN main account.
*
* @since 2.6.0
* @package W3TC
*
* @param array $details {
* Bunny CDN API configuration details.
*
* @type string $account_api_key Account API key.
* @type string $error_message Error message (optional). String already escaped.
* }
*/
namespace W3TC;
defined( 'W3TC' ) || die();
?>
<form class="w3tc_cdn_bunnycdn_form">
<?php if ( isset( $details['error_message'] ) ) : ?>
<div class="error">
<?php echo $details['error_message']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</div>
<?php endif; ?>
<div class="metabox-holder">
<?php Util_Ui::postbox_header( esc_html__( 'Bunny CDN API Configuration', 'w3-total-cache' ) ); ?>
<table class="form-table">
<tr>
<td><?php esc_html_e( 'Account API Key', 'w3-total-cache' ); ?>:</td>
<td>
<input id="w3tc-account-api-key" name="account_api_key" type="text" class="w3tc-ignore-change"
style="width: 550px" value="<?php echo esc_attr( $details['account_api_key'] ); ?>" />
<p class="description">
<?php esc_html_e( 'To obtain your account API key,', 'w3-total-cache' ); ?>
<a target="_blank" href="<?php echo esc_url( W3TC_BUNNYCDN_SETTINGS_URL ); ?>"><?php esc_html_e( 'click here', 'w3-total-cache' ); ?></a>,
<?php esc_html_e( 'log in using the main account credentials, and paste the API key into the field above.', 'w3-total-cache' ); ?>
</p>
</td>
</tr>
</table>
<p class="submit">
<input type="button"
class="w3tc_cdn_bunnycdn_list_pull_zones w3tc-button-save button-primary"
value="<?php esc_attr_e( 'Next', 'w3-total-cache' ); ?>" />
</p>
<?php Util_Ui::postbox_footer(); ?>
</div>
</form>

View File

@@ -0,0 +1,137 @@
<?php
/**
* File: Cdn_BunnyCdn_Popup_Pull_Zones.php
*
* Assists with configuring Bunny CDN as an object storage CDN.
* A pull zone selection is presented along with a form to add a new pull zone.
*
* @since 2.6.0
* @package W3TC
*
* @param string $account_api_key Account PI key.
* @parm Cdn_BunnyCdn_Api $api API class object.
* @param array $details {
* Bunny CDN API configuration details.
*
* @type array $pull_zones Pull zones.
* @type string $suggested_origin_url Suggested origin URL or IP.
* @type string $suggested_zone_name Suggested pull zone name.
* @type int $pull_zone_id Pull zone id.
* @type string $error_message Error message (optional).
* }
* @param string $server_ip Server IP address.
*/
namespace W3TC;
defined( 'W3TC' ) || die();
?>
<form class="w3tc_cdn_bunnycdn_form" method="post">
<input type="hidden" name="pull_zone_id" />
<input type="hidden" name="cdn_hostname" />
<div class="metabox-holder">
<?php Util_Ui::postbox_header( esc_html__( 'Select a pull zone', 'w3-total-cache' ) ); ?>
<table class="form-table">
<tr>
<select id="w3tc-pull-zone-id">
<option value=""<?php echo empty( $details['pull_zone_id'] ) ? ' selected' : ''; ?>>Add a new pull zone</option>
<?php
if ( ! empty( $details['pull_zones'] ) ) {
// List pull zones for selection.
foreach ( $details['pull_zones'] as $pull_zone ) {
// Skip pull zones that are disabled or suspended.
if ( ! $pull_zone['Enabled'] || $pull_zone['Suspended'] ) {
continue;
}
// Get the CDN hostname and custom hostnames.
$cdn_hostname = '?';
$custom_hostnames = array();
// Get the CDN hostname. It should be the system hostname.
foreach ( $pull_zone['Hostnames'] as $hostname ) {
if ( ! empty( $hostname['Value'] ) ) {
if ( ! empty( $hostname['IsSystemHostname'] ) ) {
// CDN hostname (system); there should only be one.
$cdn_hostname = $hostname['Value'];
} else {
// Custom hostnames; 0 or more.
$custom_hostnames[] = $hostname['Value'];
}
}
}
// Determine the origin URL/IP.
$origin_url = empty( $pull_zone['OriginUrl'] ) ? $cdn_hostname : $pull_zone['OriginUrl'];
// Determine if the current option is selected.
$is_selected = isset( $details['pull_zone_id'] ) && $details['pull_zone_id'] === $pull_zone['Id'];
// Print the select option.
?>
<option value="<?php echo esc_attr( $pull_zone['Id'] ); ?>"
<?php echo $is_selected ? ' selected' : ''; ?>
data-origin="<?php echo esc_html( $origin_url ); ?>"
data-name="<?php echo esc_attr( $pull_zone['Name'] ); ?>"
data-cdn-hostname="<?php echo esc_attr( $cdn_hostname ); ?>"
data-custom-hostnames="<?php echo esc_attr( implode( ',', $custom_hostnames ) ); ?>">
<?php echo esc_attr( $pull_zone['Name'] ); ?>
(<?php echo esc_html( $origin_url ); ?>)
</option>
<?php
// If selected, then get the origin URL/IP and pull zone name.
if ( $is_selected ) {
$selected_origin_url = $origin_url;
$selected_name = $pull_zone['Name'];
$selected_custom_hostnames = implode( "\r\n", $custom_hostnames );
}
}
}
// Determine origin URL and pull zone name for the fields below.
$field_origin_url = isset( $selected_origin_url ) ? $selected_origin_url : $details['suggested_origin_url'];
$field_name = isset( $selected_name ) ? $selected_name : $details['suggested_zone_name'];
?>
</select>
</tr>
<tr>
<td><?php esc_html_e( 'Pull Zone Name', 'w3-total-cache' ); ?>:</td>
<td>
<input id="w3tc-pull-zone-name" name="name" type="text" class="w3tc-ignore-change"
style="width: 550px" value="<?php echo esc_attr( $field_name ); ?>"
<?php echo ( empty( $details['pull_zone_id'] ) ? '' : 'readonly ' ); ?>
data-suggested="<?php echo esc_attr( $details['suggested_zone_name'] ); ?>" />
<p class="description">
<?php esc_html_e( 'Name of the pull zone (letters, numbers, and dashes). If empty, one will be automatically generated.', 'w3-total-cache' ); ?>
</p>
</td>
</tr>
<tr>
<td><?php esc_html_e( 'Origin URL / IP', 'w3-total-cache' ); ?>:</td>
<td>
<input id="w3tc-origin-url" name="origin_url" type="text" class="w3tc-ignore-change"
style="width: 550px" value="<?php echo esc_attr( $field_origin_url ); ?>"
<?php echo ( empty( $details['pull_zone_id'] ) ? '' : 'readonly ' ); ?>
data-suggested="<?php echo esc_attr( $details['suggested_origin_url'] ); ?>" />
<p class="description">
<?php
esc_html_e( 'Pull origin site URL or IP address.', 'w3-total-cache' );
if ( ! empty( $server_ip ) ) {
echo esc_html( ' ' . __( 'Detected server IP address', 'w3-total-cache' ) . ':' . $server_ip );
}
?>
</p>
</td>
</tr>
</table>
<p class="submit">
<input type="button"
class="w3tc_cdn_bunnycdn_configure_pull_zone w3tc-button-save button-primary"
value="<?php esc_attr_e( 'Apply', 'w3-total-cache' ); ?>" />
</p>
<?php Util_Ui::postbox_footer(); ?>
</div>
</form>

View File

@@ -0,0 +1,84 @@
<?php
/**
* File: Cdn_BunnyCdn_Widget.php
*
* @since 2.6.0
* @package W3TC
*/
namespace W3TC;
/**
* Class: Cdn_BunnyCdn_Widget
*
* @since 2.6.0
*/
class Cdn_BunnyCdn_Widget {
/**
* Initializes the W3TC BunnyCDN widget in the admin dashboard.
*
* This method adds the necessary actions to initialize the BunnyCDN widget on the W3TC dashboard. It creates an instance
* of the widget class, registers the required styles, and hooks the widget form display to the proper location on the admin page.
*
* @since 2.6.0
*
* @return void
*/
public static function admin_init_w3tc_dashboard() {
$o = new Cdn_BunnyCdn_Widget();
add_action( 'admin_print_styles', array( $o, 'admin_print_styles' ) );
Util_Widget::add2(
'w3tc_bunnycdn',
400,
'<div class="w3tc-widget-bunnycdn-logo"></div>',
array( $o, 'widget_form' ),
Util_Ui::admin_url( 'admin.php?page=w3tc_cdn' ),
'normal'
);
}
/**
* Displays the widget form for BunnyCDN configuration.
*
* This method checks whether the user is authorized to view the BunnyCDN widget. If authorized, it includes a view that
* shows the authorized settings. If the user is not authorized, a view indicating that they are unauthorized will be shown.
*
* @since 2.6.0
*
* @return void
*/
public function widget_form() {
$c = Dispatcher::config();
$authorized = $c->get_string( 'cdn.engine' ) === 'bunnycdn' &&
( ! empty( $c->get_integer( 'cdn.bunnycdn.pull_zone_id' ) ) || ! empty( $c->get_integer( 'cdnfsd.bunnycdn.pull_zone_id' ) ) );
if ( $authorized ) {
include __DIR__ . DIRECTORY_SEPARATOR . 'Cdn_BunnyCdn_Widget_View_Authorized.php';
} else {
include __DIR__ . DIRECTORY_SEPARATOR . 'Cdn_BunnyCdn_Widget_View_Unauthorized.php';
}
}
/**
* Enqueues the styles for the BunnyCDN widget in the admin area.
*
* This method enqueues the required CSS files for the BunnyCDN widget in the WordPress admin area. It ensures that the
* widget's styles are applied correctly on the dashboard page.
*
* @since 2.6.0
*
* @return void
*/
public function admin_print_styles() {
wp_enqueue_style( 'w3tc-widget' );
wp_enqueue_style(
'w3tc-bunnycdn-widget',
plugins_url( 'Cdn_BunnyCdn_Widget_View.css', W3TC_FILE ),
array(),
W3TC_VERSION
);
}
}

View File

@@ -0,0 +1,89 @@
.w3tc-widget-bunnycdn-logo {
width: 150px;
height: 50px;
background: url('pub/img/w3tc_bunnycdn_logo.svg') 0 8px no-repeat;
float: left;
}
.w3tc_bunnycdn_h4 {
text-align: center;
}
.w3tc_bunnycdn_summary_h4 {
text-align: center;
color: #8F8F8F;
text-align: left;
margin: 0;
}
.w3tc_bunnycdn_summary {
border-bottom: 1px solid #d2d2d2;
padding-left: 10px;
padding-right: 10px;
}
.w3tc_bunnycdn_wrapper {
margin-left: -10px;
margin-right: -10px;
}
.w3tc_bunnycdn_ul li {
margin-bottom: 3px;
padding: 0;
}
.w3tc_bunnycdn_summary_col1 {
display: block;
font-weight: bold;
width: 90px;
float:left;
}
.w3tc_bunnycdn_summary_col2 {
display: block;
font-weight: bold;
width: 80px;
float:left;
}
.w3tc_bunnycdn_tools {
margin-top:15px;
padding-left: 10px;
padding-right: 10px;
}
.w3tc_bunnycdn_tools li {
display:inline-block;
margin-right:10px;
}
.w3tc_bunnycdn_chart {
clear: both;
padding-left: 10px;
padding-right: 10px;
}
.w3tc_bunnycdn_chart p {
color:#8F8F8F;
margin:0;
}
.w3tc_bunnycdn_wrapper .button-secondary {
margin-bottom: 3px;
}
.w3tc_bunnycdn_signup_h4 {
text-align: left;
margin-bottom: 0;
padding-bottom: 0;
}
.w3tc_bunnycdn_signup p {
margin-top: 4px;
padding-top: 0;
}
.w3tc_bunnycdn_signup p span.desc {
color:#8F8F8F;
}

View File

@@ -0,0 +1,59 @@
<?php
/**
* File: Cdn_BunnyCdn_Widget_View_Authorized.php
*
* @since 2.6.0
* @package W3TC
*/
namespace W3TC;
defined( 'W3TC' ) || die();
?>
<div id="bunnycdn-widget" class="bunnycdn-widget-base w3tc_bunnycdn_content">
<div class="w3tc_bunnycdn_wrapper">
<div class="w3tc_bunnycdn_tools">
<p>
<?php
w3tc_e(
'cdn.bunnycdn.widget.v2.header',
\sprintf(
// translators: 1 HTML acronym for Content Delivery Network (CDN).
\__( 'Your website performance is enhanced with Bunny.Net\'s (%1$s) service.', 'w3-total-cache' ),
'<acronym title="' . \__( 'Content Delivery Network', 'w3-total-cache' ) . '">' . \__( 'CDN', 'w3-total-cache' ) . '</acronym>'
)
);
?>
</p>
</div>
<div class="w3tc_bunnycdn_tools">
<ul class="w3tc_bunnycdn_ul">
<li><a class="button" href="<?php echo \esc_url( \wp_nonce_url( Util_Ui::admin_url( 'admin.php?page=w3tc_dashboard&amp;w3tc_flush_cdn' ), 'w3tc' ) ); ?>"><?php \esc_html_e( 'Purge Cache', 'w3-total-cache' ); ?></a></li>
</ul>
<p>
<a target="_blank" href="<?php echo esc_url( W3TC_BUNNYCDN_CDN_URL ); ?>"><?php esc_html_e( 'Click here', 'w3-total-cache' ); ?></a>
<?php esc_html_e( 'to configure additional settings at Bunny.net.', 'w3-total-cache' ); ?>
</p>
<p>
<?php
w3tc_e(
'cdn.bunnycdn.widget.v2.existing',
\sprintf(
// translators: 1 HTML acronym for Content Delivery Network (CDN).
\__(
'If you need help configuring your %1$s, we also offer Premium Services to assist you.',
'w3-total-cache'
),
'<acronym title="' . \__( 'Content Delivery Network', 'w3-total-cache' ) . '">' . \__( 'CDN', 'w3-total-cache' ) . '</acronym>'
)
);
?>
</p>
<a class="button" href="<?php echo \esc_url( \wp_nonce_url( Util_Ui::admin_url( 'admin.php?page=w3tc_support' ), 'w3tc' ) ); ?>">
<?php \esc_html_e( 'Premium Services', 'w3-total-cache' ); ?>
</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,200 @@
<?php
/**
* File: Cdn_BunnyCdn_Widget_View_Unauthorized.php
*
* @since 2.6.0
*
* @package W3TC
*/
namespace W3TC;
defined( 'W3TC' ) || die();
?>
<div id="bunnycdn-widget" class="w3tc_bunnycdn_signup">
<?php
$cdn_engine = $c->get_string( 'cdn.engine' );
$cdn_enabled = $c->get_boolean( 'cdn.enabled' );
$cdn_name = Cache::engine_name( $cdn_engine );
$cdnfsd_engine = $c->get_string( 'cdnfsd.engine' );
$cdnfsd_enabled = $c->get_boolean( 'cdnfsd.enabled' );
$cdnfsd_name = Cache::engine_name( $cdnfsd_engine );
// Check if BunnyCDN is selected but not fully configured.
$is_bunny_cdn_incomplete = (
(
$cdn_enabled &&
'bunnycdn' === $cdn_engine &&
empty( $c->get_integer( 'cdn.bunnycdn.pull_zone_id' ) )
) ||
(
$cdnfsd_enabled &&
'bunnycdn' === $cdnfsd_engine &&
empty( $c->get_integer( 'cdnfsd.bunnycdn.pull_zone_id' ) )
)
);
// Check if a non-BunnyCDN is configured.
$is_other_cdn_configured = (
(
$cdn_enabled &&
! empty( $cdn_engine ) &&
'bunnycdn' !== $cdn_engine
) ||
(
$cdnfsd_enabled &&
! empty( $cdnfsd_engine ) &&
'bunnycdn' !== $cdnfsd_engine
)
);
if ( $is_bunny_cdn_incomplete ) {
// BunnyCDN selected but not fully configured.
?>
<p class="notice notice-error">
<?php
echo wp_kses(
sprintf(
// translators: 1 opening HTML a tag to CDN settings page, 2 closing HTML a tag.
__( 'W3 Total Cache has detected that BunnyCDN is selected but not fully configured. Please use the "Authorize" button on the %1$sCDN%2$s settings page to connect a pull zone.', 'w3-total-cache' ),
'<a href="' . esc_url_raw( Util_Ui::admin_url( 'admin.php?page=w3tc_cdn' ) ) . '">',
'</a>'
),
array(
'a' => array(
'href' => array(),
),
)
);
?>
</p>
<?php
} elseif ( $is_other_cdn_configured ) {
// A CDN is configured but it is not BunnyCDN.
?>
<p class="notice notice-error">
<?php
switch ( true ) {
case $cdn_enabled && ! empty( $cdn_engine ) && $cdnfsd_enabled && ! empty( $cdnfsd_engine ):
$cdn_label =
$cdn_name .
' <acronym title="' . __( 'Content Delivery Network', 'w3-total-cache' ) . '">' . __( 'CDN', 'w3-total-cache' ) . '</acronym>' .
' ' . __( 'and', 'w3-total-cache' ) . ' ' .
$cdnfsd_name .
' <acronym title="' . __( 'Content Delivery Network Full Site Delivery', 'w3-total-cache' ) . '">' . __( 'CDN FSD', 'w3-total-cache' ) . '</acronym>';
break;
case $cdn_enabled && ! empty( $cdn_engine ):
$cdn_label =
$cdn_name .
' <acronym title="' . __( 'Content Delivery Network', 'w3-total-cache' ) . '">' . __( 'CDN', 'w3-total-cache' ) . '</acronym>';
break;
case $cdnfsd_enabled && ! empty( $cdnfsd_engine ):
$cdn_label =
$cdnfsd_name .
' <acronym title="' . __( 'Content Delivery Network Full Site Delivery', 'w3-total-cache' ) . '">' . __( 'CDN FSD', 'w3-total-cache' ) . '</acronym>';
break;
default:
$cdn_label =
__( 'Unknown', 'w3-total-cache' ) .
' <acronym title="' . __( 'Content Delivery Network / Content Delivery Network Full Site Delivery', 'w3-total-cache' ) . '">' . __( 'CDN / CDN FSD', 'w3-total-cache' ) . '</acronym>';
break;
}
echo wp_kses(
sprintf(
// translators: 1 configured CDN/CDN FSD label.
__( 'W3 Total Cache has detected that you are using the %1$s, which is fully supported and compatible. For optimal performance and value, we recommend considering BunnyCDN as an alternative.', 'w3-total-cache' ),
$cdn_label
),
array(
'acronym' => array(
'title' => array(),
),
)
);
?>
</p>
<?php
} else {
// No CDN is configured.
?>
<p class="notice notice-error">
<?php
echo wp_kses(
sprintf(
// translators: 1 HTML acronym for Content Delivery Network (CDN).
__( 'W3 Total Cache has detected that you do not have a %1$s configured. For optimal performance and value, we recommend considering BunnyCDN.', 'w3-total-cache' ),
'<acronym title="' . __( 'Content Delivery Network', 'w3-total-cache' ) . '">' . __( 'CDN', 'w3-total-cache' ) . '</acronym>'
),
array(
'acronym' => array(
'title' => array(),
),
)
);
?>
</p>
<?php
}
?>
<p>
<?php
w3tc_e(
'cdn.bunnycdn.widget.v2.header',
\sprintf(
// translators: 1 HTML acronym for Content Delivery Network (CDN).
\__( 'Enhance your website performance by adding Bunny.Net\'s (%1$s) service to your site.', 'w3-total-cache' ),
'<acronym title="' . \__( 'Content Delivery Network', 'w3-total-cache' ) . '">' . \__( 'CDN', 'w3-total-cache' ) . '</acronym>'
)
);
?>
</p>
<h4 class="w3tc_bunnycdn_signup_h4"><?php \esc_html_e( 'New customer? Sign up now to speed up your site!', 'w3-total-cache' ); ?></h4>
<p>
<?php
w3tc_e(
'cdn.bunnycdn.widget.v2.works_magically',
\__( 'Bunny CDN works magically with W3 Total Cache to speed up your site around the world for as little as $1 per month.', 'w3-total-cache' )
);
?>
</p>
<a class="button-primary" href="<?php echo esc_url( W3TC_BUNNYCDN_SIGNUP_URL ); ?>" target="_blank">
<?php \esc_html_e( 'Sign Up Now ', 'w3-total-cache' ); ?>
</a>
<h4 class="w3tc_bunnycdn_signup_h4"><?php esc_html_e( 'Current customers', 'w3-total-cache' ); ?></h4>
<p>
<?php
w3tc_e(
'cdn.bunnycdn.widget.v2.existing',
\sprintf(
// translators: 1 HTML acronym for Content Delivery Network (CDN).
\__(
'If you\'re an existing Bunny CDN customer, enable %1$s and authorize. If you need help configuring your %1$s, we also offer Premium Services to assist you.',
'w3-total-cache'
),
'<acronym title="' . \__( 'Content Delivery Network', 'w3-total-cache' ) . '">' . \__( 'CDN', 'w3-total-cache' ) . '</acronym>'
)
);
?>
</p>
<a class="button-primary" href="<?php echo \esc_url( \wp_nonce_url( Util_Ui::admin_url( 'admin.php?page=w3tc_cdn' ), 'w3tc' ) ); ?>">
<?php \esc_html_e( 'Authorize', 'w3-total-cache' ); ?>
</a>
<a class="button" href="<?php echo \esc_url( \wp_nonce_url( Util_Ui::admin_url( 'admin.php?page=w3tc_support' ), 'w3tc' ) ); ?>">
<?php \esc_html_e( 'Premium Services', 'w3-total-cache' ); ?>
</a>
</div>

View File

@@ -0,0 +1,102 @@
<?php
/**
* File: Cdn_CacheFlush.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cdn_CacheFlush
*
* CDN cache purge object
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
*/
class Cdn_CacheFlush {
/**
* Advanced cache config
*
* @var Config
*/
private $_config = null;
/**
* Array of urls to flush
*
* @var array
*/
private $flush_operation_requested = false;
/**
* Constructor for the Cdn_CacheFlush class.
*
* Initializes the configuration by fetching it from the Dispatcher. This constructor sets up the necessary
* configuration needed for the class to function correctly.
*
* @return void
*/
public function __construct() {
$this->_config = Dispatcher::config();
}
/**
* Purges all cached content.
*
* This method triggers a purge of all cached content. It sets a flag indicating that a flush operation has been
* requested, and returns true to confirm that the operation was initiated.
*
* @return bool True if the purge operation is initiated successfully.
*/
public function purge_all() {
$this->flush_operation_requested = true;
return true;
}
/**
* Purges the cache for a specific URL.
*
* This method purges the cache for the provided URL. It parses the URL, constructs the appropriate CDN path,
* and triggers the cache purge operation through the CDN service.
*
* @param string $url The URL whose cache needs to be purged.
*
* @return void
*/
public function purge_url( $url ) {
$common = Dispatcher::component( 'Cdn_Core' );
$results = array();
$files = array();
$parsed = wp_parse_url( $url );
$local_site_path = isset( $parsed['path'] ) ? ltrim( $parsed['path'], '/' ) : '';
$remote_path = $common->uri_to_cdn_uri( $local_site_path );
$files[] = $common->build_file_descriptor( $local_site_path, $remote_path );
$this->_flushed_urls[] = $url;
$common->purge( $files, $results );
}
/**
* Performs cleanup actions after a post purge operation.
*
* This method is responsible for performing any necessary cleanup after a cache purge operation. It checks
* whether a flush operation has been requested, triggers a full CDN cache purge if necessary, and resets the
* relevant flags.
*
* @return int The number of items that were processed during the cleanup.
*/
public function purge_post_cleanup() {
if ( $this->flush_operation_requested ) {
$common = Dispatcher::component( 'Cdn_Core' );
$results = array();
$common->purge_all( $results );
$count = 999;
$this->flush_operation_requested = false;
}
return $count;
}
}

View File

@@ -0,0 +1,131 @@
<?php
/**
* File: Cdn_ConfigLabels.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cdn_ConfigLabels
*/
class Cdn_ConfigLabels {
/**
* Merges additional CDN configuration labels with the provided array.
*
* This method takes an array of configuration labels and merges them with predefined labels related to CDN functionality.
* The predefined labels include various settings for enabling and configuring the CDN, FSD (Full Site Delivery), and custom file handling.
* Each label is localized using WordPress's `__()` function to ensure proper translation support.
*
* @param array $config_labels The existing array of configuration labels to be merged with predefined labels.
*
* @return array The merged array of configuration labels.
*/
public function config_labels( $config_labels ) {
return array_merge(
$config_labels,
array(
'cdn.enabled' => '<acronym title="' . __( 'Content Delivery Network', 'w3-total-cache' ) . '">' . __( 'CDN', 'w3-total-cache' ) . '</acronym>:',
'cdn.engine' => '<acronym title="' . __( 'Content Delivery Network', 'w3-total-cache' ) . '">' . __( 'CDN', 'w3-total-cache' ) . '</acronym>' . __( ' Type:', 'w3-total-cache' ),
'cdn.debug' => '<acronym title="' . __( 'Content Delivery Network', 'w3-total-cache' ) . '">' . __( 'CDN', 'w3-total-cache' ) . '</acronym>',
'cdnfsd.debug' => '<acronym title="' . __( 'Full Site Delivery', 'w3-total-cache' ) . '">' . __( 'FSD', 'w3-total-cache' ) . '</acronym> <acronym title="' . __( 'Content Delivery Network', 'w3-total-cache' ) . '">' . __( 'CDN', 'w3-total-cache' ) . '</acronym>',
'cdn.uploads.enable' => __( 'Host attachments', 'w3-total-cache' ),
'cdn.includes.enable' => __( 'Host wp-includes/ files', 'w3-total-cache' ),
'cdn.theme.enable' => __( 'Host theme files', 'w3-total-cache' ),
'cdn.minify.enable' => wp_kses(
sprintf(
// Translators: 1 acronym for CSS, 2 acronym for JS.
__(
'Host minified %1$s and %2$s files',
'w3-total-cache'
),
'<acronym title="' . __( 'Cascading Style Sheet', 'w3-total-cache' ) . '">' . __( 'CSS', 'w3-total-cache' ) . '</acronym>',
'<acronym title="' . __( 'JavaScript', 'w3-total-cache' ) . '">' . __( 'JS', 'w3-total-cache' ) . '</acronym>'
),
array(
'acronym' => array(
'title' => array(),
),
)
),
'cdn.custom.enable' => __( 'Host custom files', 'w3-total-cache' ),
'cdn.force.rewrite' => __( 'Force over-writing of existing files', 'w3-total-cache' ),
'cdn.import.external' => __( 'Import external media library attachments', 'w3-total-cache' ),
'cdn.canonical_header' => __( 'Add canonical header', 'w3-total-cache' ),
'cdn.reject.ssl' => wp_kses(
sprintf(
// Translators: 1 acronym for CDN, 2 acroym for SSL.
__(
'Disable %1$s on %2$s pages',
'w3-total-cache'
),
'<acronym title="' . __( 'Content Delivery Network', 'w3-total-cache' ) . '">' . __( 'CDN', 'w3-total-cache' ) . '</acronym>',
'<acronym title="' . __( 'Secure Sockets Layer', 'w3-total-cache' ) . '">' . __( 'SSL', 'w3-total-cache' ) . '</acronym>'
),
array(
'acronym' => array(
'title' => array(),
),
)
),
'cdn.admin.media_library' => wp_kses(
sprintf(
// Translators: 1 acronym for CDN.
__(
'Use %1$s links for the Media Library on admin pages',
'w3-total-cache'
),
'<acronym title="' . __( 'Content Delivery Network', 'w3-total-cache' ) . '">' . __( 'CDN', 'w3-total-cache' ) . '</acronym>'
),
array(
'acronym' => array(
'title' => array(),
),
)
),
'cdn.reject.logged_roles' => wp_kses(
sprintf(
// Translators: 1 acronym for CDN.
__(
'Disable %1$s for the following roles',
'w3-total-cache'
),
'<acronym title="' . __( 'Content Delivery Network', 'w3-total-cache' ) . '">' . __( 'CDN', 'w3-total-cache' ) . '</acronym>'
),
array(
'acronym' => array(
'title' => array(),
),
)
),
'cdn.reject.uri' => wp_kses(
sprintf(
// Translators: 1 acronym for CDN.
__(
'Disable %1$s on the following pages:',
'w3-total-cache'
),
'<acronym title="' . __( 'Content Delivery Network', 'w3-total-cache' ) . '">' . __( 'CDN', 'w3-total-cache' ) . '</acronym>'
),
array(
'acronym' => array(
'title' => array(),
),
)
),
'cdn.autoupload.enabled' => __( 'Export changed files automatically', 'w3-total-cache' ),
'cdn.autoupload.interval' => __( 'Auto upload interval:', 'w3-total-cache' ),
'cdn.queue.interval' => __( 'Re-transfer cycle interval:', 'w3-total-cache' ),
'cdn.queue.limit' => __( 'Re-transfer cycle limit:', 'w3-total-cache' ),
'cdn.includes.files' => __( 'wp-includes file types to upload:', 'w3-total-cache' ),
'cdn.theme.files' => __( 'Theme file types to upload:', 'w3-total-cache' ),
'cdn.import.files' => __( 'File types to import:', 'w3-total-cache' ),
'cdn.custom.files' => __( 'Custom file list:', 'w3-total-cache' ),
'cdn.rscf.location' => __( 'Location:', 'w3-total-cache' ),
'cdn.reject.ua' => __( 'Rejected user agents:', 'w3-total-cache' ),
'cdn.reject.files' => __( 'Rejected files:', 'w3-total-cache' ),
)
);
}
}

View File

@@ -0,0 +1,988 @@
<?php
/**
* File: Cdn_Core.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class: Cdn_Core
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
* phpcs:disable WordPress.DB.DirectDatabaseQuery
*/
class Cdn_Core {
/**
* Config.
*
* @var Config
*/
private $_config = null;
/**
* Debug.
*
* @var bool
*/
private $debug;
/**
* Constructor method for initializing the class.
*
* @return void
*/
public function __construct() {
$this->_config = Dispatcher::config();
$this->debug = $this->_config->get_boolean( 'cdn.debug' );
}
/**
* Adds a file to the CDN queue.
*
* @param string $local_path Local file path.
* @param string $remote_path Remote file path.
* @param int $command Command type (upload, delete, etc.).
* @param string $last_error Last error message, if any.
*
* @return bool True if the file was successfully added or already exists.
*/
public function queue_add( $local_path, $remote_path, $command, $last_error ) {
global $wpdb;
$table = $wpdb->base_prefix . W3TC_CDN_TABLE_QUEUE;
$rows = $wpdb->get_results(
$wpdb->prepare(
'SELECT id, command FROM ' . $table . ' WHERE local_path = %s AND remote_path = %s', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$local_path,
$remote_path
)
);
$already_exists = false;
foreach ( $rows as $row ) {
if ( $row->command !== $command ) {
$wpdb->query(
$wpdb->prepare(
'DELETE FROM ' . $table . ' WHERE id = %d', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$row->id
)
);
} else {
$already_exists = true;
}
}
if ( $already_exists ) {
return true;
}
// Insert if not yet there.
return $wpdb->query(
$wpdb->prepare(
'INSERT INTO ' . $table . ' (local_path, remote_path, command, last_error, date) VALUES (%s, %s, %d, %s, NOW())', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$local_path,
$remote_path,
$command,
$last_error
)
);
}
/**
* Retrieves a list of files to be uploaded based on a local file path.
*
* @param string $file Local file path.
*
* @return array Array of file descriptors for upload.
*/
public function get_files_for_upload( $file ) {
$files = array();
$upload_info = Util_Http::upload_info();
if ( $upload_info ) {
$file = $this->normalize_attachment_file( $file );
$local_file = $upload_info['basedir'] . '/' . $file;
$parsed = wp_parse_url( rtrim( $upload_info['baseurl'], '/' ) . '/' . $file );
$local_uri = $parsed['path'];
$remote_uri = $this->uri_to_cdn_uri( $local_uri );
$remote_file = ltrim( $remote_uri, '/' );
$files[] = $this->build_file_descriptor( $local_file, $remote_file );
}
return $files;
}
/**
* Retrieves a list of size-specific files for upload based on the attachment file and its sizes.
*
* @param string $attached_file Path to the attached file.
* @param array $sizes Array of sizes for the attached file.
*
* @return array Array of file descriptors for each size.
*/
public function _get_sizes_files( $attached_file, $sizes ) {
$files = array();
$base_dir = Util_File::dirname( $attached_file );
foreach ( (array) $sizes as $size ) {
if ( isset( $size['file'] ) ) {
if ( $base_dir ) {
$file = $base_dir . '/' . $size['file'];
} else {
$file = $size['file'];
}
$files = array_merge( $files, $this->get_files_for_upload( $file ) );
}
}
return $files;
}
/**
* Retrieves all files associated with an attachment, including its sizes and metadata.
*
* @param array $metadata Metadata for the attachment.
*
* @return array Array of file descriptors for the attachment and its sizes.
*/
public function get_metadata_files( $metadata ) {
$files = array();
if ( isset( $metadata['file'] ) && isset( $metadata['sizes'] ) ) {
$files = array_merge( $files, $this->_get_sizes_files( $metadata['file'], $metadata['sizes'] ) );
}
return $files;
}
/**
* Retrieves a list of files associated with a given attachment.
*
* @param int $attachment_id The ID of the attachment.
*
* @return array Array of file descriptors for the attachment and its sizes.
*/
public function get_attachment_files( $attachment_id ) {
$files = array();
// Get attached file.
$attached_file = get_post_meta( $attachment_id, '_wp_attached_file', true );
if ( ! empty( $attached_file ) ) {
$files = array_merge( $files, $this->get_files_for_upload( $attached_file ) );
// Get backup sizes files.
$attachment_backup_sizes = get_post_meta( $attachment_id, '_wp_attachment_backup_sizes', true );
if ( is_array( $attachment_backup_sizes ) ) {
$files = array_merge( $files, $this->_get_sizes_files( $attached_file, $attachment_backup_sizes ) );
}
}
// Get files from metadata.
$attachment_metadata = get_post_meta( $attachment_id, '_wp_attachment_metadata', true );
if ( is_array( $attachment_metadata ) ) {
$files = array_merge( $files, $this->get_metadata_files( $attachment_metadata ) );
}
return $files;
}
/**
* Uploads files to the CDN.
*
* @param array $files List of files to upload.
* @param bool $queue_failed Whether to queue failed uploads.
* @param array $results Array to store the results of the upload.
* @param int $timeout_time Optional timeout time for the upload.
*
* @return bool True if the upload was successful, false otherwise.
*/
public function upload( $files, $queue_failed, &$results, $timeout_time = null ) {
if ( $this->debug ) {
Util_Debug::log( 'cdn', 'upload: ' . wp_json_encode( $files, JSON_PRETTY_PRINT ) );
}
$cdn = $this->get_cdn();
$force_rewrite = $this->_config->get_boolean( 'cdn.force.rewrite' );
@set_time_limit( $this->_config->get_integer( 'timelimit.cdn_upload' ) ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
$engine = $this->_config->get_string( 'cdn.engine' );
$return = $cdn->upload( $files, $results, $force_rewrite, $timeout_time );
if ( ! $return && $queue_failed ) {
foreach ( $results as $result ) {
if ( W3TC_CDN_RESULT_OK !== $result['result'] ) {
$this->queue_add( $result['local_path'], $result['remote_path'], W3TC_CDN_COMMAND_UPLOAD, $result['error'] );
}
}
}
return $return;
}
/**
* Deletes files from the CDN.
*
* @param array $files List of files to delete.
* @param bool $queue_failed Whether to queue failed deletions.
* @param array $results Array to store the results of the deletion.
*
* @return bool True if the deletion was successful, false otherwise.
*/
public function delete( $files, $queue_failed, &$results ) {
$cdn = $this->get_cdn();
@set_time_limit( $this->_config->get_integer( 'timelimit.cdn_delete' ) ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
$return = $cdn->delete( $files, $results );
if ( $this->debug ) {
Util_Debug::log( 'cdn', 'delete: ' . wp_json_encode( $files, JSON_PRETTY_PRINT ) );
}
if ( ! $return && $queue_failed ) {
foreach ( $results as $result ) {
if ( W3TC_CDN_RESULT_OK !== $result['result'] ) {
$this->queue_add( $result['local_path'], $result['remote_path'], W3TC_CDN_COMMAND_DELETE, $result['error'] );
}
}
}
return $return;
}
/**
* Purges files from the CDN.
*
* @param array $files List of files to purge.
* @param array $results Array to store the results of the purge.
*
* @return bool True if the purge was successful, false otherwise.
*/
public function purge( $files, &$results ) {
if ( $this->debug ) {
Util_Debug::log( 'cdn', 'purge: ' . wp_json_encode( $files, JSON_PRETTY_PRINT ) );
}
/**
* Purge varnish servers before mirror purging.
*/
if ( Cdn_Util::is_engine_mirror( $this->_config->get_string( 'cdn.engine' ) ) && $this->_config->get_boolean( 'varnish.enabled' ) ) {
$varnish = Dispatcher::component( 'Varnish_Flush' );
foreach ( $files as $file ) {
$remote_path = $file['remote_path'];
$varnish->flush_url( network_site_url( $remote_path ) );
}
}
/**
* Purge CDN.
*/
$cdn = $this->get_cdn();
@set_time_limit( $this->_config->get_integer( 'timelimit.cdn_purge' ) ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
$return = $cdn->purge( $files, $results );
if ( ! $return ) {
foreach ( $results as $result ) {
if ( W3TC_CDN_RESULT_OK !== $result['result'] ) {
$this->queue_add( $result['local_path'], $result['remote_path'], W3TC_CDN_COMMAND_PURGE, $result['error'] );
}
}
}
return $return;
}
/**
* Purges all files from the CDN.
*
* @param array $results Array to store the results of the purge.
*
* @return bool True if the purge was successful, false otherwise.
*/
public function purge_all( &$results ) {
$cdn = $this->get_cdn();
@set_time_limit( $this->_config->get_integer( 'timelimit.cdn_purge' ) ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
return $cdn->purge_all( $results );
}
/**
* Adds a URL to the CDN upload queue.
*
* @param string $url URL to be queued for upload.
*
* @return void
*/
public function queue_upload_url( $url ) {
$docroot_filename = Util_Environment::url_to_docroot_filename( $url );
if ( is_null( $docroot_filename ) ) {
return;
}
$filename = Util_Environment::docroot_to_full_filename( $docroot_filename );
$a = \wp_parse_url( $url );
$remote_filename = $this->uri_to_cdn_uri( $a['path'] );
$this->queue_add( $filename, $remote_filename, W3TC_CDN_COMMAND_UPLOAD, 'Pending' );
}
/**
* Normalizes the attachment file path.
*
* @param string $file The file path to normalize.
*
* @return string The normalized file path.
*/
public function normalize_attachment_file( $file ) {
$upload_info = Util_Http::upload_info();
if ( $upload_info ) {
$file = ltrim( str_replace( $upload_info['basedir'], '', $file ), '/\\' );
$matches = null;
if ( preg_match( '~(\d{4}/\d{2}/)?[^/]+$~', $file, $matches ) ) {
$file = $matches[0];
}
}
return $file;
}
/**
* Retrieves the CDN configuration based on the specified CDN engine.
*
* This method checks the current configuration settings and returns an array
* containing the appropriate configuration for the specified CDN engine.
* The configuration details are dependent on the engine selected, such as
* Akamai, Cloudflare, or S3, and include settings such as API keys, domain,
* SSL configurations, and compression options. The method caches the configuration
* after the first retrieval for subsequent calls.
*
* @return array|null The CDN configuration array or null if not configured.
*/
public function get_cdn() {
static $cdn = null;
if ( is_null( $cdn ) ) {
$c = $this->_config;
$engine = $c->get_string( 'cdn.engine' );
$compression = ( $c->get_boolean( 'browsercache.enabled' ) && $c->get_boolean( 'browsercache.html.compression' ) );
switch ( $engine ) {
case 'akamai':
$engine_config = array(
'username' => $c->get_string( 'cdn.akamai.username' ),
'password' => $c->get_string( 'cdn.akamai.password' ),
'zone' => $c->get_string( 'cdn.akamai.zone' ),
'domain' => $c->get_array( 'cdn.akamai.domain' ),
'ssl' => $c->get_string( 'cdn.akamai.ssl' ),
'email_notification' => $c->get_array( 'cdn.akamai.email_notification' ),
'compression' => false,
);
break;
case 'att':
$engine_config = array(
'account' => $c->get_string( 'cdn.att.account' ),
'token' => $c->get_string( 'cdn.att.token' ),
'domain' => $c->get_array( 'cdn.att.domain' ),
'ssl' => $c->get_string( 'cdn.att.ssl' ),
'compression' => false,
);
break;
case 'azure':
$engine_config = array(
'user' => $c->get_string( 'cdn.azure.user' ),
'key' => $c->get_string( 'cdn.azure.key' ),
'container' => $c->get_string( 'cdn.azure.container' ),
'cname' => $c->get_array( 'cdn.azure.cname' ),
'ssl' => $c->get_string( 'cdn.azure.ssl' ),
'compression' => false,
);
break;
case 'azuremi':
$engine_config = array(
'user' => $c->get_string( 'cdn.azuremi.user' ),
'clientid' => $c->get_string( 'cdn.azuremi.clientid' ),
'container' => $c->get_string( 'cdn.azuremi.container' ),
'cname' => $c->get_array( 'cdn.azuremi.cname' ),
'ssl' => $c->get_string( 'cdn.azuremi.ssl' ),
'compression' => false,
);
break;
case 'cf':
$engine_config = array(
'key' => $c->get_string( 'cdn.cf.key' ),
'secret' => $c->get_string( 'cdn.cf.secret' ),
'bucket' => $c->get_string( 'cdn.cf.bucket' ),
'bucket_location' => self::get_region_id( $c->get_string( 'cdn.cf.bucket.location' ) ),
'bucket_loc_id' => $c->get_string( 'cdn.cf.bucket.location' ),
'id' => $c->get_string( 'cdn.cf.id' ),
'cname' => $c->get_array( 'cdn.cf.cname' ),
'ssl' => $c->get_string( 'cdn.cf.ssl' ),
'public_objects' => $c->get_string( 'cdn.cf.public_objects' ),
'compression' => $compression,
);
break;
case 'cf2':
$engine_config = array(
'key' => $c->get_string( 'cdn.cf2.key' ),
'secret' => $c->get_string( 'cdn.cf2.secret' ),
'id' => $c->get_string( 'cdn.cf2.id' ),
'cname' => $c->get_array( 'cdn.cf2.cname' ),
'ssl' => $c->get_string( 'cdn.cf2.ssl' ),
'compression' => false,
);
break;
case 'cotendo':
$engine_config = array(
'username' => $c->get_string( 'cdn.cotendo.username' ),
'password' => $c->get_string( 'cdn.cotendo.password' ),
'zones' => $c->get_array( 'cdn.cotendo.zones' ),
'domain' => $c->get_array( 'cdn.cotendo.domain' ),
'ssl' => $c->get_string( 'cdn.cotendo.ssl' ),
'compression' => false,
);
break;
case 'edgecast':
$engine_config = array(
'account' => $c->get_string( 'cdn.edgecast.account' ),
'token' => $c->get_string( 'cdn.edgecast.token' ),
'domain' => $c->get_array( 'cdn.edgecast.domain' ),
'ssl' => $c->get_string( 'cdn.edgecast.ssl' ),
'compression' => false,
);
break;
case 'ftp':
$engine_config = array(
'host' => $c->get_string( 'cdn.ftp.host' ),
'type' => $c->get_string( 'cdn.ftp.type' ),
'user' => $c->get_string( 'cdn.ftp.user' ),
'pass' => $c->get_string( 'cdn.ftp.pass' ),
'path' => $c->get_string( 'cdn.ftp.path' ),
'pasv' => $c->get_boolean( 'cdn.ftp.pasv' ),
'domain' => $c->get_array( 'cdn.ftp.domain' ),
'ssl' => $c->get_string( 'cdn.ftp.ssl' ),
'compression' => false,
'docroot' => Util_Environment::document_root(),
);
break;
case 'google_drive':
$state = Dispatcher::config_state();
$engine_config = array(
'client_id' => $c->get_string( 'cdn.google_drive.client_id' ),
'access_token' => $state->get_string( 'cdn.google_drive.access_token' ),
'refresh_token' => $c->get_string( 'cdn.google_drive.refresh_token' ),
'root_url' => $c->get_string( 'cdn.google_drive.folder.url' ),
'root_folder_id' => $c->get_string( 'cdn.google_drive.folder.id' ),
'new_access_token_callback' => array( $this, 'on_google_drive_new_access_token' ),
);
break;
case 'mirror':
$engine_config = array(
'domain' => $c->get_array( 'cdn.mirror.domain' ),
'ssl' => $c->get_string( 'cdn.mirror.ssl' ),
'compression' => false,
);
break;
case 'rackspace_cdn':
$state = Dispatcher::config_state();
$engine_config = array(
'user_name' => $c->get_string( 'cdn.rackspace_cdn.user_name' ),
'api_key' => $c->get_string( 'cdn.rackspace_cdn.api_key' ),
'region' => $c->get_string( 'cdn.rackspace_cdn.region' ),
'service_access_url' => $c->get_string( 'cdn.rackspace_cdn.service.access_url' ),
'service_id' => $c->get_string( 'cdn.rackspace_cdn.service.id' ),
'service_protocol' => $c->get_string( 'cdn.rackspace_cdn.service.protocol' ),
'domains' => $c->get_array( 'cdn.rackspace_cdn.domains' ),
'access_state' => $state->get_string( 'cdn.rackspace_cdn.access_state' ),
'new_access_state_callback' => array( $this, 'on_rackspace_cdn_new_access_state' ),
);
break;
case 'rscf':
$state = Dispatcher::config_state();
$engine_config = array(
'user_name' => $c->get_string( 'cdn.rscf.user' ),
'api_key' => $c->get_string( 'cdn.rscf.key' ),
'region' => $c->get_string( 'cdn.rscf.location' ),
'container' => $c->get_string( 'cdn.rscf.container' ),
'cname' => $c->get_array( 'cdn.rscf.cname' ),
'ssl' => $c->get_string( 'cdn.rscf.ssl' ),
'compression' => false,
'access_state' => $state->get_string( 'cdn.rackspace_cf.access_state' ),
'new_access_state_callback' => array( $this, 'on_rackspace_cf_new_access_state' ),
);
break;
case 's3':
$engine_config = array(
'key' => $c->get_string( 'cdn.s3.key' ),
'secret' => $c->get_string( 'cdn.s3.secret' ),
'bucket' => $c->get_string( 'cdn.s3.bucket' ),
'bucket_location' => self::get_region_id( $c->get_string( 'cdn.s3.bucket.location' ) ),
'bucket_loc_id' => $c->get_string( 'cdn.s3.bucket.location' ),
'cname' => $c->get_array( 'cdn.s3.cname' ),
'ssl' => $c->get_string( 'cdn.s3.ssl' ),
'public_objects' => $c->get_string( 'cdn.s3.public_objects' ),
'compression' => $compression,
);
break;
case 's3_compatible':
$engine_config = array(
'key' => $c->get_string( 'cdn.s3.key' ),
'secret' => $c->get_string( 'cdn.s3.secret' ),
'bucket' => $c->get_string( 'cdn.s3.bucket' ),
'cname' => $c->get_array( 'cdn.s3.cname' ),
'ssl' => $c->get_string( 'cdn.s3.ssl' ),
'compression' => $compression,
'api_host' => $c->get_string( 'cdn.s3_compatible.api_host' ),
);
break;
case 'bunnycdn':
$engine_config = array(
'account_api_key' => $c->get_string( 'cdn.bunnycdn.account_api_key' ),
'storage_api_key' => $c->get_string( 'cdn.bunnycdn.storage_api_key' ),
'stream_api_key' => $c->get_string( 'cdn.bunnycdn.stream_api_key' ),
'pull_zone_id' => $c->get_integer( 'cdn.bunnycdn.pull_zone_id' ),
'domain' => $c->get_string( 'cdn.bunnycdn.cdn_hostname' ),
);
break;
default:
$engine_config = array();
break;
}
$engine_config = array_merge(
$engine_config,
array(
'debug' => $c->get_boolean( 'cdn.debug' ),
'headers' => apply_filters( 'w3tc_cdn_config_headers', array() ),
)
);
$cdn = CdnEngine::instance( $engine, $engine_config );
}
return $cdn;
}
/**
* Handles the storage of a new Google Drive access token in the configuration state.
*
* This method sets the provided Google Drive access token into the configuration state
* for later use, ensuring that the token is saved and accessible for operations related
* to Google Drive integration.
*
* @param string $access_token The new access token for Google Drive.
*
* @return void
*/
public function on_google_drive_new_access_token( $access_token ) {
$state = Dispatcher::config_state();
$state->set( 'cdn.google_drive.access_token', $access_token );
$state->save();
}
/**
* Handles the storage of a new Rackspace CDN access state in the configuration state.
*
* This method sets the provided access state into the configuration state for Rackspace CDN,
* ensuring that the state is saved and accessible for future operations related to Rackspace CDN.
*
* @param string $access_state The new access state for Rackspace CDN.
*
* @return void
*/
public function on_rackspace_cdn_new_access_state( $access_state ) {
$state = Dispatcher::config_state();
$state->set( 'cdn.rackspace_cdn.access_state', $access_state );
$state->save();
}
/**
* Handles the storage of a new Rackspace Cloud Files access state in the configuration state.
*
* This method sets the provided access state into the configuration state for Rackspace Cloud Files,
* ensuring that the state is saved and accessible for future operations related to Rackspace Cloud Files.
*
* @param string $access_state The new access state for Rackspace Cloud Files.
*
* @return void
*/
public function on_rackspace_cf_new_access_state( $access_state ) {
$state = Dispatcher::config_state();
$state->set( 'cdn.rackspace_cf.access_state', $access_state );
$state->save();
}
/**
* Converts a file path to its corresponding URI, removing multisite-specific path components.
*
* This method transforms a given file path to a URI by stripping off any multisite subsite path
* and ensuring the result is a valid URI format.
*
* @param string $file The file path to convert.
*
* @return string The corresponding URI for the file.
*/
public function docroot_filename_to_uri( $file ) {
$file = ltrim( $file, '/' );
// Translate multisite subsite uploads paths.
return str_replace( basename( WP_CONTENT_DIR ) . '/blogs.dir/' . Util_Environment::blog_id() . '/', '', $file );
}
/**
* Converts a file path to its absolute filesystem path.
*
* This method takes a relative file path and returns its absolute path based on the document root,
* ensuring proper handling of directory separators for different environments.
*
* @param string $file The file path to convert.
*
* @return string The absolute filesystem path.
*/
public function docroot_filename_to_absolute_path( $file ) {
if ( is_file( $file ) ) {
return $file;
}
if ( '/' !== DIRECTORY_SEPARATOR ) {
$file = str_replace( '/', DIRECTORY_SEPARATOR, $file );
}
return rtrim( Util_Environment::document_root(), '/\\' ) . DIRECTORY_SEPARATOR . ltrim( $file, '/\\' );
}
/**
* Converts a local URI to a corresponding CDN URI based on the environment and configuration.
*
* This method converts a local URI to a CDN URI by considering various conditions such as
* the use of WordPress multisite and specific CDN engine configurations.
*
* @param string $local_uri The local URI to convert.
*
* @return string The corresponding CDN URI.
*/
public function uri_to_cdn_uri( $local_uri ) {
$local_uri = ltrim( $local_uri, '/' );
$remote_uri = $local_uri;
if ( Util_Environment::is_wpmu() && defined( 'DOMAIN_MAPPING' ) && DOMAIN_MAPPING ) {
$remote_uri = str_replace( site_url(), '', $local_uri );
}
$engine = $this->_config->get_string( 'cdn.engine' );
if ( Cdn_Util::is_engine_mirror( $engine ) ) {
if ( Util_Environment::is_wpmu() && strpos( $local_uri, 'files' ) === 0 ) {
$upload_dir = Util_Environment::wp_upload_dir();
$remote_uri = $this->abspath_to_relative_path( dirname( $upload_dir['basedir'] ) ) . '/' . $local_uri;
}
} elseif ( Util_Environment::is_wpmu() &&
! Util_Environment::is_wpmu_subdomain() &&
Util_Environment::is_using_master_config() &&
Cdn_Util::is_engine_push( $engine ) ) {
/**
* In common config mode files are uploaded for network home url so mirror will not contain /subblog/ path in uri.
* Since upload process is not blog-specific and wp-content/plugins/../*.jpg files are common.
*/
$home = trim( home_url( '', 'relative' ), '/' ) . '/';
$network_home = trim( network_home_url( '', 'relative' ), '/' ) . '/';
if ( $home !== $network_home && substr( $local_uri, 0, strlen( $home ) ) === $home ) {
$remote_uri = $network_home . substr( $local_uri, strlen( $home ) );
}
}
return apply_filters( 'w3tc_uri_cdn_uri', ltrim( $remote_uri, '/' ) );
}
/**
* Converts a local URL and path to a full CDN URL.
*
* This method combines a base CDN URL with the corresponding CDN path, resulting in a full URL
* that points to the resource on the CDN. The path is first converted to a CDN URI using
* the `uri_to_cdn_uri()` method.
*
* @param string $url The base URL to convert.
* @param string $path The relative path to the resource.
*
* @return string|null The full CDN URL, or null if the conversion fails.
*/
public function url_to_cdn_url( $url, $path ) {
$cdn = $this->get_cdn();
$remote_path = $this->uri_to_cdn_uri( $path );
$new_url = $cdn->format_url( $remote_path );
if ( ! $new_url ) {
return null;
}
$is_engine_mirror = Cdn_Util::is_engine_mirror( $this->_config->get_string( 'cdn.engine' ) );
$new_url = apply_filters( 'w3tc_cdn_url', $new_url, $url, $is_engine_mirror );
return $new_url;
}
/**
* Converts an absolute filesystem path to a relative path based on the document root.
*
* This method removes the document root from the provided path, returning a relative path
* that can be used in a URL or CDN context.
*
* @param string $path The absolute filesystem path to convert.
*
* @return string The corresponding relative path.
*/
public function abspath_to_relative_path( $path ) {
return str_replace( Util_Environment::document_root(), '', $path );
}
/**
* Converts a relative path to a full URL based on the site's home domain.
*
* This method constructs a full URL from a relative path by appending the path to the
* site's home domain root URL.
*
* @param string $path The relative path to convert.
*
* @return string The corresponding full URL.
*/
public function relative_path_to_url( $path ) {
return rtrim( Util_Environment::home_domain_root_url(), '/' ) . '/' .
$this->docroot_filename_to_uri( ltrim( $path, '/' ) );
}
/**
* Builds a file descriptor array with local and remote paths, as well as the original URL.
*
* This method creates an array that describes a file, including its local and remote paths
* and the original URL for the file. The array is filtered through the `w3tc_build_cdn_file_array` filter.
*
* @param string $local_path The local path of the file.
* @param string $remote_path The remote path of the file.
*
* @return array The file descriptor array with local, remote, and URL information.
*/
public function build_file_descriptor( $local_path, $remote_path ) {
$file = array(
'local_path' => $local_path,
'remote_path' => $remote_path,
'original_url' => $this->relative_path_to_url( $local_path ),
);
return apply_filters( 'w3tc_build_cdn_file_array', $file );
}
/**
* Returns the region ID corresponding to a given bucket location.
*
* This static method translates a given bucket location into its corresponding region ID.
* For example, it converts 'us-east-1-e' to 'us-east-1'.
*
* @since 2.7.2
*
* @param string $bucket_location The location of the bucket.
*
* @return string The corresponding region ID.
*/
public static function get_region_id( $bucket_location ) {
switch ( $bucket_location ) {
case 'us-east-1-e':
$region = 'us-east-1';
break;
default:
$region = $bucket_location;
break;
}
return $region;
}
/**
* Is the configured CDN authorized?
*
* @since 2.8.5
*
* @return bool
*/
public function is_cdn_authorized() {
switch ( $this->_config->get_string( 'cdn.engine' ) ) {
case 'akamai':
$is_cdn_authorized = ! empty( $this->_config->get_string( 'cdn.akamai.username' ) ) &&
! empty( $this->_config->get_string( 'cdn.akamai.password' ) ) &&
! empty( $this->_config->get_string( 'cdn.akamai.zone' ) );
break;
case 'att':
$is_cdn_authorized = ! empty( $this->_config->get_string( 'cdn.att.account' ) ) &&
! empty( $this->_config->get_string( 'cdn.att.token' ) );
break;
case 'azure':
$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' ) ) &&
! empty( $this->_config->get_array( 'cdn.azure.cname' ) );
break;
case 'azuremi':
$is_cdn_authorized = ! empty( $this->_config->get_string( 'cdn.azuremi.user' ) ) &&
! empty( $this->_config->get_string( 'cdn.azuremi.clientid' ) ) &&
! empty( $this->_config->get_string( 'cdn.azure.container' ) ) &&
! empty( $this->_config->get_array( 'cdn.azure.cname' ) );
break;
case 'bunnycdn':
$is_cdn_authorized = ! empty( $this->_config->get_string( 'cdn.bunnycdn.account_api_key' ) ) &&
! empty( $this->_config->get_string( 'cdn.bunnycdn.pull_zone_id' ) );
break;
case 'cf':
$is_cdn_authorized = ! empty( $this->_config->get_string( 'cdn.cf.key' ) ) &&
! empty( $this->_config->get_string( 'cdn.cf.secret' ) ) &&
! empty( $this->_config->get_string( 'cdn.cf.bucket' ) ) &&
! empty( $this->_config->get_string( 'cdn.cf.bucket.location' ) );
break;
case 'cf2':
$is_cdn_authorized = ! empty( $this->_config->get_string( 'cdn.cf2.key' ) ) &&
! empty( $this->_config->get_string( 'cdn.cf2.secret' ) ) &&
! empty( $this->_config->get_string( 'cdn.cf2.id' ) );
break;
case 'cotendo':
$is_cdn_authorized = ! empty( $this->_config->get_string( 'cdn.cotendo.username' ) ) &&
! empty( $this->_config->get_string( 'cdn.cotendo.password' ) ) &&
! empty( $this->_config->get_array( 'cdn.cotendo.domain' ) ) &&
! empty( $this->_config->get_array( 'cdn.cotendo.zones' ) );
break;
case 'edgecast':
$is_cdn_authorized = ! empty( $this->_config->get_string( 'cdn.edgecast.account' ) ) &&
! empty( $this->_config->get_string( 'cdn.edgecast.token' ) ) &&
! empty( $this->_config->get_array( 'cdn.edgecast.domain' ) );
break;
case 'ftp':
$is_cdn_authorized = ! empty( $this->_config->get_string( 'cdn.ftp.host' ) ) &&
! empty( $this->_config->get_string( 'cdn.ftp.type' ) ) &&
! empty( $this->_config->get_string( 'cdn.ftp.user' ) ) &&
! empty( $this->_config->get_string( 'cdn.ftp.pass' ) );
break;
case 'google_drive':
$is_cdn_authorized = ! empty( $this->_config->get_string( 'cdn.google_drive.client_id' ) ) &&
! empty( $this->_config->get_string( 'cdn.google_drive.refresh_token' ) ) &&
! empty( $this->_config->get_string( 'cdn.google_drive.folder.id' ) );
break;
case 'mirror':
$is_cdn_authorized = ! empty( $this->_config->get_array( 'cdn.mirror.domain' ) );
break;
case 'rackspace_cdn':
$is_cdn_authorized = ! empty( $this->_config->get_string( 'cdn.rackspace_cdn.user_name' ) ) &&
! empty( $this->_config->get_string( 'cdn.rackspace_cdn.api_key' ) ) &&
! empty( $this->_config->get_string( 'cdn.rackspace_cdn.region' ) ) &&
! empty( $this->_config->get_string( 'cdn.rackspace_cdn.service.id' ) );
break;
case 'rscf':
$is_cdn_authorized = ! empty( $this->_config->get_string( 'cdn.rscf.user' ) ) &&
! empty( $this->_config->get_string( 'cdn.rscf.key' ) ) &&
! empty( $this->_config->get_string( 'cdn.rscf.container' ) );
break;
case 's3':
case 's3_compatible':
$is_cdn_authorized = ! empty( $this->_config->get_string( 'cdn.s3.key' ) ) &&
! empty( $this->_config->get_string( 'cdn.s3.secret' ) ) &&
! empty( $this->_config->get_string( 'cdn.s3.bucket' ) ) &&
! empty( $this->_config->get_string( 'cdn.s3.bucket.location' ) );
break;
default:
$is_cdn_authorized = false;
break;
}
return $is_cdn_authorized;
}
/**
* Is the configured CDN FSD authorized?
*
* @since 2.8.5
*
* @return bool
*/
public function is_cdnfsd_authorized() {
$cloudflare_config = $this->_config->get_array( 'cloudflare' );
switch ( $this->_config->get_string( 'cdnfsd.engine' ) ) {
case 'bunnycdn':
$is_cdnfsd_authorized = ! empty( $this->_config->get_string( 'cdn.bunnycdn.account_api_key' ) ) &&
! empty( $this->_config->get_string( 'cdnfsd.bunnycdn.pull_zone_id' ) );
break;
case 'cloudflare':
$is_cdnfsd_authorized = ! empty( $cloudflare_config['email'] ) &&
! empty( $cloudflare_config['key'] ) &&
! empty( $cloudflare_config['zone_id'] ) &&
! empty( $cloudflare_config['zone_name'] );
break;
case 'cloudfront':
$is_cdnfsd_authorized = ! empty( $this->_config->get_string( 'cdnfsd.cloudfront.access_key' ) ) &&
! empty( $this->_config->get_string( 'cdnfsd.cloudfront.secret_key' ) ) &&
! empty( $this->_config->get_string( 'cdnfsd.cloudfront.distribution_id' ) );
break;
case 'transparentcdn':
$is_cdnfsd_authorized = ! empty( $this->_config->get_string( 'cdnfsd.transparentcdn.client_id' ) ) &&
! empty( $this->_config->get_string( 'cdnfsd.transparentcdn.client_secret' ) ) &&
! empty( $this->_config->get_string( 'cdnfsd.transparentcdn.company_id' ) );
break;
default:
$is_cdnfsd_authorized = false;
break;
}
return $is_cdnfsd_authorized;
}
}

View File

@@ -0,0 +1,970 @@
<?php
/**
* File: Cdn_Core_Admin.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cdn_Core_Admin
*
* W3 Total Cache CDN Plugin
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
* phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
* phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited
* phpcs:disable WordPress.DB.DirectDatabaseQuery
* phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
*/
class Cdn_Core_Admin {
/**
* Config
*
* @var Config
*/
private $_config = null;
/**
* Constructor for the Cdn_Core_Admin class.
*
* Initializes the configuration by dispatching the configuration object.
*
* @return void
*/
public function __construct() {
$this->_config = Dispatcher::config();
}
/**
* Purges the CDN files associated with an attachment.
*
* This method retrieves the files associated with the given attachment ID and purges them from the CDN.
*
* @param int $attachment_id The ID of the attachment to purge.
* @param array $results Reference to an array that will store the purge results.
*
* @return bool True on success, false on failure.
*/
public function purge_attachment( $attachment_id, &$results ) {
$common = Dispatcher::component( 'Cdn_Core' );
$files = $common->get_attachment_files( $attachment_id );
return $common->purge( $files, $results );
}
/**
* Updates the queue entry with the latest error message.
*
* This method updates the `last_error` field and the `date` for a specific queue item.
*
* @param int $queue_id The ID of the queue item to update.
* @param string $last_error The error message to store.
*
* @return int|false The number of affected rows, or false on failure.
*/
public function queue_update( $queue_id, $last_error ) {
global $wpdb;
$sql = sprintf( 'UPDATE %s SET last_error = "%s", date = NOW() WHERE id = %d', $wpdb->base_prefix . W3TC_CDN_TABLE_QUEUE, esc_sql( $last_error ), $queue_id );
return $wpdb->query( $sql );
}
/**
* Deletes a queue item from the database.
*
* This method removes a specific queue entry identified by its ID.
*
* @param int $queue_id The ID of the queue item to delete.
*
* @return int|false The number of affected rows, or false on failure.
*/
public function queue_delete( $queue_id ) {
global $wpdb;
$sql = sprintf( 'DELETE FROM %s WHERE id = %d', $wpdb->base_prefix . W3TC_CDN_TABLE_QUEUE, $queue_id );
return $wpdb->query( $sql );
}
/**
* Empties the queue based on a command.
*
* This method deletes all queue entries that match the specified command.
*
* @param int $command The command identifier to filter the queue entries.
*
* @return int|false The number of affected rows, or false on failure.
*/
public function queue_empty( $command ) {
global $wpdb;
$sql = sprintf( 'DELETE FROM %s WHERE command = %d', $wpdb->base_prefix . W3TC_CDN_TABLE_QUEUE, $command );
return $wpdb->query( $sql );
}
/**
* Retrieves a list of queue items from the database.
*
* This method fetches queue entries, optionally limiting the number of results returned.
*
* @param int|null $limit The maximum number of queue entries to retrieve, or null for no limit.
*
* @return array An associative array of queue items, grouped by command.
*/
public function queue_get( $limit = null ) {
global $wpdb;
$sql = sprintf( 'SELECT * FROM %s%s ORDER BY date', $wpdb->base_prefix, W3TC_CDN_TABLE_QUEUE );
if ( $limit ) {
$sql .= sprintf( ' LIMIT %d', $limit );
}
$results = $wpdb->get_results( $sql );
$queue = array();
if ( $results ) {
foreach ( (array) $results as $result ) {
$queue[ $result->command ][] = $result;
}
}
return $queue;
}
/**
* Processes items in the queue and performs the respective CDN actions.
*
* This method processes the queued commands (upload, delete, or purge) and interacts with the CDN to perform
* the necessary operations on the files. The results are handled accordingly, updating the queue.
*
* @param int $limit The maximum number of items to process from the queue.
*
* @return int The number of items successfully processed.
*/
public function queue_process( $limit ) {
$items = 0;
$commands = $this->queue_get( $limit );
$force_rewrite = $this->_config->get_boolean( 'cdn.force.rewrite' );
if ( count( $commands ) ) {
$common = Dispatcher::component( 'Cdn_Core' );
$cdn = $common->get_cdn();
foreach ( $commands as $command => $queue ) {
$files = array();
$results = array();
$map = array();
foreach ( $queue as $result ) {
$files[] = $common->build_file_descriptor( $result->local_path, $result->remote_path );
$map[ $result->local_path ] = $result->id;
++$items;
}
switch ( $command ) {
case W3TC_CDN_COMMAND_UPLOAD:
foreach ( $files as $file ) {
$local_file_name = $file['local_path'];
$remote_file_name = $file['remote_path'];
if ( ! file_exists( $local_file_name ) ) {
Dispatcher::create_file_for_cdn( $local_file_name );
}
}
$cdn->upload( $files, $results, $force_rewrite );
foreach ( $results as $result ) {
if ( W3TC_CDN_RESULT_OK === $result['result'] ) {
Dispatcher::on_cdn_file_upload( $result['local_path'] );
}
}
break;
case W3TC_CDN_COMMAND_DELETE:
$cdn->delete( $files, $results );
break;
case W3TC_CDN_COMMAND_PURGE:
$cdn->purge( $files, $results );
break;
}
foreach ( $results as $result ) {
if ( W3TC_CDN_RESULT_OK === $result['result'] ) {
$this->queue_delete( $map[ $result['local_path'] ] );
} else {
$this->queue_update( $map[ $result['local_path'] ], $result['error'] );
}
}
}
}
return $items;
}
/**
* Exports library attachments to a CDN.
*
* This method retrieves a list of attachment files and their metadata from the WordPress database, processes
* each attachment to generate a file descriptor, and uploads the files to a Content Delivery Network (CDN).
* It also handles pagination via the limit and offset parameters and updates the provided count and total values
* with the number of results and the total attachment count respectively.
*
* @param int $limit The number of attachments to retrieve. If set to 0, no limit is applied.
* @param int $offset The offset for retrieving attachments. Defaults to 0.
* @param int $count The variable to store the number of attachments retrieved.
* @param int $total The variable to store the total number of attachments.
* @param array $results The variable to store the results of the upload process.
* @param int $timeout_time The timeout duration for the upload request in seconds. Defaults to 0 (no timeout).
*
* @return void
*/
public function export_library( $limit, $offset, &$count, &$total, &$results, $timeout_time = 0 ) {
global $wpdb;
$count = 0;
$total = 0;
$upload_info = Util_Http::upload_info();
if ( $upload_info ) {
$sql = sprintf(
'SELECT
pm.meta_value AS file,
pm2.meta_value AS metadata
FROM
%sposts AS p
LEFT JOIN
%spostmeta AS pm ON p.ID = pm.post_ID AND pm.meta_key = "_wp_attached_file"
LEFT JOIN
%spostmeta AS pm2 ON p.ID = pm2.post_ID AND pm2.meta_key = "_wp_attachment_metadata"
WHERE
p.post_type = "attachment" AND (pm.meta_value IS NOT NULL OR pm2.meta_value IS NOT NULL)
GROUP BY
p.ID
ORDER BY
p.ID',
$wpdb->prefix,
$wpdb->prefix,
$wpdb->prefix
);
if ( $limit ) {
$sql .= sprintf( ' LIMIT %d', $limit );
if ( $offset ) {
$sql .= sprintf( ' OFFSET %d', $offset );
}
}
$posts = $wpdb->get_results( $sql );
if ( $posts ) {
$count = count( $posts );
$total = $this->get_attachments_count();
$files = array();
$common = Dispatcher::component( 'Cdn_Core' );
foreach ( $posts as $post ) {
$post_files = array();
if ( $post->file ) {
$file = $common->normalize_attachment_file( $post->file );
$local_file = $upload_info['basedir'] . '/' . $file;
$remote_file = ltrim( $upload_info['baseurlpath'] . $file, '/' );
$post_files[] = $common->build_file_descriptor( $local_file, $remote_file );
}
if ( $post->metadata ) {
$metadata = @unserialize( $post->metadata );
$post_files = array_merge( $post_files, $common->get_metadata_files( $metadata ) );
}
$post_files = apply_filters( 'w3tc_cdn_add_attachment', $post_files );
$files = array_merge( $files, $post_files );
}
$common = Dispatcher::component( 'Cdn_Core' );
$common->upload( $files, false, $results, $timeout_time );
}
}
}
/**
* Import external files into the media library.
*
* This method processes posts with links or images, checking if the external files exist in the media library.
* If the files do not exist, it downloads or copies them to the server, inserts them as attachments, and updates
* the post content to reference the new media URLs. Logs the results of the import process.
*
* phpcs:disable WordPress.Arrays.MultipleStatementAlignment
*
* @param int $limit The number of posts to process.
* @param int $offset The offset for the posts to process.
* @param int $count The number of posts processed.
* @param int $total The total number of posts to import.
* @param array $results An array to hold the results of the import process, including file paths and errors.
*
* @return void
*/
public function import_library( $limit, $offset, &$count, &$total, &$results ) {
global $wpdb;
$count = 0;
$total = 0;
$results = array();
$upload_info = Util_Http::upload_info();
$uploads_use_yearmonth_folders = get_option( 'uploads_use_yearmonth_folders' );
$document_root = Util_Environment::document_root();
@set_time_limit( $this->_config->get_integer( 'timelimit.cdn_import' ) ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
if ( $upload_info ) {
/**
* Search for posts with links or images
*/
$sql = sprintf(
'SELECT
ID,
post_content,
post_date
FROM
%sposts
WHERE
post_status = "publish"
AND (post_type = "post" OR post_type = "page")
AND (post_content LIKE "%%src=%%"
OR post_content LIKE "%%href=%%")',
$wpdb->prefix
);
if ( $limit ) {
$sql .= sprintf( ' LIMIT %d', $limit );
if ( $offset ) {
$sql .= sprintf( ' OFFSET %d', $offset );
}
}
$posts = $wpdb->get_results( $sql );
if ( $posts ) {
$count = count( $posts );
$total = $this->get_import_posts_count();
$regexp = '~(' . $this->get_regexp_by_mask( $this->_config->get_string( 'cdn.import.files' ) ) . ')$~';
$config_state = Dispatcher::config_state();
$import_external = $config_state->get_boolean( 'cdn.import.external' );
foreach ( $posts as $post ) {
$matches = null;
$replaced = array();
$attachments = array();
$post_content = $post->post_content;
/**
* Search for all link and image sources
*/
if ( preg_match_all( '~(href|src)=[\'"]?([^\'"<>\s]+)[\'"]?~', $post_content, $matches, PREG_SET_ORDER ) ) {
foreach ( $matches as $match ) {
list( $search, $attribute, $origin ) = $match;
/**
* Check if $search is already replaced
*/
if ( isset( $replaced[ $search ] ) ) {
continue;
}
$error = '';
$result = false;
$src = Util_Environment::normalize_file_minify( $origin );
$dst = '';
/**
* Check if file exists in the library
*/
if ( stristr( $origin, $upload_info['baseurl'] ) === false ) {
/**
* Check file extension
*/
$check_src = $src;
if ( Util_Environment::is_url( $check_src ) ) {
$qpos = strpos( $check_src, '?' );
if ( false !== $qpos ) {
$check_src = substr( $check_src, 0, $qpos );
}
}
if ( preg_match( $regexp, $check_src ) ) {
/**
* Check for already uploaded attachment
*/
if ( isset( $attachments[ $src ] ) ) {
list( $dst, $dst_url ) = $attachments[ $src ];
$result = true;
} else {
if ( $uploads_use_yearmonth_folders ) {
$upload_subdir = gmdate( 'Y/m', strtotime( $post->post_date ) );
$upload_dir = sprintf( '%s/%s', $upload_info['basedir'], $upload_subdir );
$upload_url = sprintf( '%s/%s', $upload_info['baseurl'], $upload_subdir );
} else {
$upload_subdir = '';
$upload_dir = $upload_info['basedir'];
$upload_url = $upload_info['baseurl'];
}
$src_filename = pathinfo( $src, PATHINFO_FILENAME );
$src_extension = pathinfo( $src, PATHINFO_EXTENSION );
/**
* Get available filename
*/
for ( $i = 0; ; $i++ ) {
$dst = sprintf( '%s/%s%s%s', $upload_dir, $src_filename, ( $i ? $i : '' ), ( $src_extension ? '.' . $src_extension : '' ) );
if ( ! file_exists( $dst ) ) {
break;
}
}
$dst_basename = basename( $dst );
$dst_url = sprintf( '%s/%s', $upload_url, $dst_basename );
$dst_path = ltrim( str_replace( $document_root, '', Util_Environment::normalize_path( $dst ) ), '/' );
if ( $upload_subdir ) {
Util_File::mkdir( $upload_subdir, 0777, $upload_info['basedir'] );
}
$download_result = false;
/**
* Check if file is remote URL
*/
if ( Util_Environment::is_url( $src ) ) {
/**
* Download file
*/
if ( $import_external ) {
$download_result = Util_Http::download( $src, $dst );
if ( ! $download_result ) {
$error = 'Unable to download file';
}
} else {
$error = 'External file import is disabled';
}
} else {
/**
* Otherwise copy file from local path
*/
$src_path = $document_root . '/' . urldecode( $src );
if ( file_exists( $src_path ) ) {
$download_result = @copy( $src_path, $dst );
if ( ! $download_result ) {
$error = 'Unable to copy file';
}
} else {
$error = 'Source file doesn\'t exists';
}
}
/**
* Check if download or copy was successful
*/
if ( $download_result ) {
$title = $dst_basename;
$guid = ltrim( $upload_info['baseurlpath'] . $title, ',' );
$mime_type = Util_Mime::get_mime_type( $dst );
$GLOBALS['wp_rewrite'] = new \WP_Rewrite();
/**
* Insert attachment
*/
$id = wp_insert_attachment(
array(
'post_mime_type' => $mime_type,
'guid' => $guid,
'post_title' => $title,
'post_content' => '',
'post_parent' => $post->ID,
),
$dst
);
if ( ! is_wp_error( $id ) ) {
/**
* Generate attachment metadata and upload to CDN
*/
require_once ABSPATH . 'wp-admin/includes/image.php';
wp_update_attachment_metadata( $id, wp_generate_attachment_metadata( $id, $dst ) );
$attachments[ $src ] = array(
$dst,
$dst_url,
);
$result = true;
} else {
$error = 'Unable to insert attachment';
}
}
}
/**
* If attachment was successfully created then replace links
*/
if ( $result ) {
$replace = sprintf( '%s="%s"', $attribute, $dst_url );
// replace $search with $replace.
$post_content = str_replace( $search, $replace, $post_content );
$replaced[ $search ] = $replace;
$error = 'OK';
}
} else {
$error = 'File type rejected';
}
} else {
$error = 'File already exists in the media library';
}
/**
* Add new entry to the log file
*/
$results[] = array(
'src' => $src,
'dst' => $dst_path,
'result' => $result,
'error' => $error,
);
}
}
/**
* If post content was chenged then update DB
*/
if ( $post_content !== $post->post_content ) {
wp_update_post(
array(
'ID' => $post->ID,
'post_content' => $post_content,
)
);
}
}
}
}
}
/**
* Renames domain URLs in post content.
*
* This method searches for URLs in published posts and pages that match a given set of domain names, and renames
* them with the new base URL. The renaming process involves identifying `src` and `href` attributes within post
* content and replacing any matched URLs that reference the old domain with the new base URL, updating the post
* content accordingly.
*
* @param array $names An array of domain names to be renamed.
* @param int $limit The maximum number of posts to process at once (optional).
* @param int $offset The offset for pagination (optional).
* @param int $count The number of posts processed.
* @param int $total The total number of posts that require renaming.
* @param array $results An array of results showing old and new URLs with status.
*
* @return void
*/
public function rename_domain( $names, $limit, $offset, &$count, &$total, &$results ) {
global $wpdb;
@set_time_limit( $this->_config->get_integer( 'timelimit.domain_rename' ) ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
$count = 0;
$total = 0;
$results = array();
$upload_info = Util_Http::upload_info();
foreach ( $names as $index => $name ) {
$names[ $index ] = str_ireplace( 'www.', '', $name );
}
if ( $upload_info ) {
$sql = sprintf(
'SELECT
ID,
post_content,
post_date
FROM
%sposts
WHERE
post_status = "publish"
AND (post_type = "post" OR post_type = "page")
AND (post_content LIKE "%%src=%%"
OR post_content LIKE "%%href=%%")',
$wpdb->prefix
);
if ( $limit ) {
$sql .= sprintf( ' LIMIT %d', $limit );
if ( $offset ) {
$sql .= sprintf( ' OFFSET %d', $offset );
}
}
$posts = $wpdb->get_results( $sql );
if ( $posts ) {
$count = count( $posts );
$total = $this->get_rename_posts_count();
$names_quoted = array_map( array( '\W3TC\Util_Environment', 'preg_quote' ), $names );
foreach ( $posts as $post ) {
$matches = null;
$post_content = $post->post_content;
$regexp = '~(href|src)=[\'"]?(https?://(www\.)?(' . implode( '|', $names_quoted ) . ')' . Util_Environment::preg_quote( $upload_info['baseurlpath'] ) . '([^\'"<>\s]+))[\'"]~';
if ( preg_match_all( $regexp, $post_content, $matches, PREG_SET_ORDER ) ) {
foreach ( $matches as $match ) {
$old_url = $match[2];
$new_url = sprintf( '%s/%s', $upload_info['baseurl'], $match[5] );
$post_content = str_replace( $old_url, $new_url, $post_content );
$results[] = array(
'old' => $old_url,
'new' => $new_url,
'result' => true,
'error' => 'OK',
);
}
}
if ( $post_content !== $post->post_content ) {
wp_update_post(
array(
'ID' => $post->ID,
'post_content' => $post_content,
)
);
}
}
}
}
}
/**
* Retrieves the count of attachments in the WordPress database.
*
* This method queries the WordPress database to count the number of attachments that are stored in the posts table,
* ensuring that there is an associated `_wp_attached_file` or `_wp_attachment_metadata` meta key. This is useful for
* tracking the number of media files stored in the system.
*
* @return int The total number of attachments.
*/
public function get_attachments_count() {
global $wpdb;
$sql = sprintf(
'SELECT COUNT(DISTINCT p.ID)
FROM %sposts AS p
LEFT JOIN %spostmeta AS pm ON p.ID = pm.post_ID
AND
pm.meta_key = "_wp_attached_file"
LEFT JOIN %spostmeta AS pm2 ON p.ID = pm2.post_ID
AND
pm2.meta_key = "_wp_attachment_metadata"
WHERE
p.post_type = "attachment"
AND
(pm.meta_value IS NOT NULL OR pm2.meta_value IS NOT NULL)',
$wpdb->prefix,
$wpdb->prefix,
$wpdb->prefix
);
return $wpdb->get_var( $sql );
}
/**
* Retrieves the count of posts and pages that contain media references (e.g., `src` or `href` attributes).
*
* This method counts the number of posts and pages in the WordPress database that have `src` or `href` attributes
* in their content, which typically indicate the presence of media (e.g., images, links, or other assets).
*
* @return int The total number of posts and pages with media references.
*/
public function get_import_posts_count() {
global $wpdb;
$sql = sprintf(
'SELECT
COUNT(*)
FROM
%sposts
WHERE
post_status = "publish"
AND
(post_type = "post"
OR
post_type = "page")
AND
(post_content LIKE "%%src=%%"
OR
post_content LIKE "%%href=%%")',
$wpdb->prefix
);
return $wpdb->get_var( $sql );
}
/**
* Retrieves the count of posts and pages that require domain renaming based on media references.
*
* This method is essentially a shortcut to `get_import_posts_count()` and returns the number of posts and pages
* that need their URLs updated during a domain rename operation.
*
* @return int The total number of posts and pages to be renamed.
*/
public function get_rename_posts_count() {
return $this->get_import_posts_count();
}
/**
* Generates a regular expression pattern based on a given mask.
*
* This method takes a mask (e.g., wildcard pattern) and converts it into a valid regular expression, where `*`
* and `?` are replaced by patterns that match any sequence of characters. This allows for more flexible matching
* of domain names or other patterns within content.
*
* @param string $mask The mask to convert into a regular expression.
*
* @return string The generated regular expression pattern.
*/
public function get_regexp_by_mask( $mask ) {
$mask = trim( $mask );
$mask = Util_Environment::preg_quote( $mask );
$mask = str_replace(
array(
'\*',
'\?',
';',
),
array(
'@ASTERISK@',
'@QUESTION@',
'|',
),
$mask
);
$regexp = str_replace(
array(
'@ASTERISK@',
'@QUESTION@',
),
array(
'[^\\?\\*:\\|"<>]*',
'[^\\?\\*:\\|"<>]',
),
$mask
);
return $regexp;
}
/**
* Adds custom actions to the media row in the WordPress admin interface.
*
* This method adds a custom action link to the media file row in the WordPress admin, allowing an admin user to
* purge the media file from the CDN. The link includes a nonce for security purposes.
*
* @param array $actions An array of existing action links for the media.
* @param WP_Post $post The current post object representing the media item.
*
* @return array The updated array of action links.
*/
public function media_row_actions( $actions, $post ) {
$actions = array_merge(
$actions,
array(
'cdn_purge' => sprintf(
'<a href="%s">' . __( 'Purge from CDN', 'w3-total-cache' ) . '</a>',
wp_nonce_url(
sprintf(
'admin.php?page=w3tc_dashboard&w3tc_cdn_purge_attachment&attachment_id=%d',
$post->ID
),
'w3tc'
)
),
)
);
return $actions;
}
/**
* Checks if the CDN is running based on the current configuration.
*
* This method verifies the CDN engine configuration and ensures that all necessary
* settings (like API keys, buckets, domains, etc.) are provided and valid. It checks
* for different CDN engines, including FTP, S3, Cloudflare (CF), Azure, and others.
* The method returns true if the CDN is correctly configured and operational,
* and false otherwise.
*
* @return bool True if the CDN is running, false otherwise.
*/
public function is_running() {
/**
* CDN
*/
$running = true;
/**
* Check CDN settings
*/
$cdn_engine = $this->_config->get_string( 'cdn.engine' );
switch ( true ) {
case (
'ftp' === $cdn_engine &&
! count( $this->_config->get_array( 'cdn.ftp.domain' ) )
):
$running = false;
break;
case (
's3' === $cdn_engine &&
(
'' === $this->_config->get_string( 'cdn.s3.key' ) ||
'' === $this->_config->get_string( 'cdn.s3.secret' ) ||
'' === $this->_config->get_string( 'cdn.s3.bucket' )
)
):
$running = false;
break;
case (
'cf' === $cdn_engine &&
(
'' === $this->_config->get_string( 'cdn.cf.key' ) ||
'' === $this->_config->get_string( 'cdn.cf.secret' ) ||
'' === $this->_config->get_string( 'cdn.cf.bucket' ) ||
(
'' === $this->_config->get_string( 'cdn.cf.id' ) &&
! count( $this->_config->get_array( 'cdn.cf.cname' ) )
)
)
):
$running = false;
break;
case (
'cf2' === $cdn_engine &&
(
'' === $this->_config->get_string( 'cdn.cf2.key' ) ||
'' === $this->_config->get_string( 'cdn.cf2.secret' ) ||
(
'' === $this->_config->get_string( 'cdn.cf2.id' ) &&
! count( $this->_config->get_array( 'cdn.cf2.cname' ) )
)
)
):
$running = false;
break;
case (
'rscf' === $cdn_engine &&
(
'' === $this->_config->get_string( 'cdn.rscf.user' ) ||
'' === $this->_config->get_string( 'cdn.rscf.key' ) ||
'' === $this->_config->get_string( 'cdn.rscf.container' ) ||
! count( $this->_config->get_array( 'cdn.rscf.cname' ) )
)
):
$running = false;
break;
case (
'azure' === $cdn_engine &&
(
'' === $this->_config->get_string( 'cdn.azure.user' ) ||
'' === $this->_config->get_string( 'cdn.azure.key' ) ||
'' === $this->_config->get_string( 'cdn.azure.container' )
)
):
$running = false;
break;
case (
'azuremi' === $cdn_engine &&
(
'' === $this->_config->get_string( 'cdn.azuremi.user' ) ||
'' === $this->_config->get_string( 'cdn.azuremi.clientid' ) ||
'' === $this->_config->get_string( 'cdn.azuremi.container' )
)
):
$running = false;
break;
case (
'mirror' === $cdn_engine &&
! count( $this->_config->get_array( 'cdn.mirror.domain' ) )
):
$running = false;
break;
case (
'cotendo' === $cdn_engine &&
! count( $this->_config->get_array( 'cdn.cotendo.domain' ) )
):
$running = false;
break;
case (
'edgecast' === $cdn_engine &&
! count( $this->_config->get_array( 'cdn.edgecast.domain' ) )
):
$running = false;
break;
case (
'att' === $cdn_engine &&
! count( $this->_config->get_array( 'cdn.att.domain' ) )
):
$running = false;
break;
case (
'akamai' === $cdn_engine &&
! count( $this->_config->get_array( 'cdn.akamai.domain' ) )
):
$running = false;
break;
}
return $running;
}
}

View File

@@ -0,0 +1,580 @@
<?php
/**
* File: Cdn_Environment.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cdn_Environment
*
* phpcs:disable WordPress.DB.DirectDatabaseQuery
* phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
* phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter
*/
class Cdn_Environment {
/**
* Constructor for the Cdn_Environment class.
*
* Initializes the class by adding filters to the 'w3tc_browsercache_rules_section_extensions' and
* 'w3tc_browsercache_rules_section' hooks. This allows the class to modify browser cache rules
* when certain conditions are met.
*
* @return void
*/
public function __construct() {
add_filter( 'w3tc_browsercache_rules_section_extensions', array( $this, 'w3tc_browsercache_rules_section_extensions' ), 10, 3 );
add_filter( 'w3tc_browsercache_rules_section', array( $this, 'w3tc_browsercache_rules_section' ), 10, 3 );
}
/**
* Handles environment adjustments when a request is made from the WordPress admin area.
*
* This method checks the configuration and forces all checks if necessary, then updates the rules
* based on the CDN configuration. If there are any exceptions during this process, they are
* collected and thrown at the end.
*
* @param Config $config The configuration object containing the settings to be checked.
* @param bool $force_all_checks Whether to force all checks, bypassing any cached results.
*
* @return void
*
* @throws Util_Environment_Exception Environment exception.
*/
public function fix_on_wpadmin_request( $config, $force_all_checks ) {
$exs = new Util_Environment_Exceptions();
if ( $config->get_boolean( 'config.check' ) || $force_all_checks ) {
if ( $config->get_boolean( 'cdn.enabled' ) ) {
$this->rules_add( $config, $exs );
} else {
$this->rules_remove( $exs );
}
}
if ( count( $exs->exceptions() ) > 0 ) {
throw $exs;
}
}
/**
* Handles CDN-related changes triggered by an event.
*
* This method processes the CDN settings based on the given event. It may unschedule or schedule cron jobs
* related to queue processing and file uploads based on configuration changes.
*
* @param Config $config The configuration object containing the CDN settings.
* @param string $event The event that triggered the action.
* @param Config|null $old_config The old configuration values before the update.
*
* @return void
*
* @throws Util_Environment_Exception Environment exception.
*/
public function fix_on_event( $config, $event, $old_config = null ) {
if ( $config->get_boolean( 'cdn.enabled' ) && ! Cdn_Util::is_engine_mirror( $config->get_string( 'cdn.engine' ) ) ) {
if ( null !== $old_config && $config->get_integer( 'cdn.queue.interval' ) !== $old_config->get_integer( 'cdn.queue.interval' ) ) {
$this->unschedule_queue_process();
}
if ( ! wp_next_scheduled( 'w3_cdn_cron_queue_process' ) ) {
wp_schedule_event( time(), 'w3_cdn_cron_queue_process', 'w3_cdn_cron_queue_process' );
}
} else {
$this->unschedule_queue_process();
}
if (
$config->get_boolean( 'cdn.enabled' ) &&
$config->get_boolean( 'cdn.autoupload.enabled' ) &&
! Cdn_Util::is_engine_mirror( $config->get_string( 'cdn.engine' ) )
) {
if ( null !== $old_config && $config->get_integer( 'cdn.autoupload.interval' ) !== $old_config->get_integer( 'cdn.autoupload.interval' ) ) {
$this->unschedule_upload();
}
if ( ! wp_next_scheduled( 'w3_cdn_cron_upload' ) ) {
wp_schedule_event( time(), 'w3_cdn_cron_upload', 'w3_cdn_cron_upload' );
}
} else {
$this->unschedule_upload();
}
$exs = new Util_Environment_Exceptions();
if ( $config->get_boolean( 'cdn.enabled' ) ) {
try {
$this->handle_tables(
'activate' === $event, // drop state on activation.
true
);
} catch ( \Exception $ex ) {
$exs->push( $ex );
}
}
if ( count( $exs->exceptions() ) > 0 ) {
throw $exs;
}
}
/**
* Handles necessary changes after the deactivation of the CDN.
*
* This method ensures that CDN-related rules and database tables are cleaned up and removed
* when the CDN module is deactivated.
*
* @return void
*
* @throws Util_Environment_Exception Environment exception.
*/
public function fix_after_deactivation() {
$exs = new Util_Environment_Exceptions();
$this->rules_remove( $exs );
$this->handle_tables( true, false );
if ( count( $exs->exceptions() ) > 0 ) {
throw $exs;
}
}
/**
* Retrieves the rules required for the CDN based on the configuration.
*
* This method generates and returns the necessary CDN rewrite rules, including those for FTP
* and browser cache, based on the current configuration.
*
* @param Config $config The configuration object containing the CDN settings.
*
* @return array|null The CDN rewrite rules or null if the CDN is not enabled.
*/
public function get_required_rules( $config ) {
if ( ! $config->get_boolean( 'cdn.enabled' ) ) {
return null;
}
$rewrite_rules = array();
$rules = $this->rules_generate( $config );
if ( strlen( $rules ) > 0 ) {
if ( 'ftp' === $config->get_string( 'cdn.engine' ) ) {
$common = Dispatcher::component( 'Cdn_Core' );
$domain = $common->get_cdn()->get_domain();
$cdn_rules_path = sprintf( 'ftp://%s/%s', $domain, Util_Rule::get_cdn_rules_path() );
$rewrite_rules[] = array(
'filename' => $cdn_rules_path,
'content' => $rules,
);
}
$path = Util_Rule::get_browsercache_rules_cache_path();
$rewrite_rules[] = array(
'filename' => $path,
'content' => $rules,
);
}
return $rewrite_rules;
}
/**
* Retrieves instructions for configuring the CDN based on the current settings.
*
* This method returns the instructions for database setup when the CDN module is enabled.
*
* @param Config $config The configuration object containing the CDN settings.
*
* @return array|null The instructions for configuring the CDN, or null if CDN is not enabled.
*/
public function get_instructions( $config ) {
if ( ! $config->get_boolean( 'cdn.enabled' ) ) {
return null;
}
$instructions = array();
$instructions[] = array(
'title' => __( 'CDN module: Required Database SQL', 'w3-total-cache' ),
'content' => $this->generate_table_sql(),
'area' => 'database',
);
return $instructions;
}
/**
* Generates CDN rules specific to FTP configuration.
*
* This method generates CDN rules specifically for FTP configurations, adding additional
* logic if required based on the configuration.
*
* @param Config $config The configuration object containing the CDN settings.
*
* @return string The generated FTP-specific CDN rules.
*/
public function rules_generate_for_ftp( $config ) {
return $this->rules_generate( $config, true );
}
/**
* Handles the creation and removal of necessary database tables for the CDN.
*
* This method drops existing tables and creates new ones as required for storing CDN queue
* and path map data. The method also handles charset and collation settings for the tables.
*
* @param bool $drop Whether to drop the existing tables.
* @param bool $create Whether to create the tables if necessary.
*
* @return void
*
* @throws Util_Environment_Exception Environment exception.
*/
private function handle_tables( $drop, $create ) {
global $wpdb;
$tablename_queue = $wpdb->base_prefix . W3TC_CDN_TABLE_QUEUE;
$tablename_map = $wpdb->base_prefix . W3TC_CDN_TABLE_PATHMAP;
if ( $drop ) {
$sql = "DROP TABLE IF EXISTS `$tablename_queue`;";
$wpdb->query( $sql );
$sql = "DROP TABLE IF EXISTS `$tablename_map`;";
$wpdb->query( $sql );
}
if ( ! $create ) {
return;
}
$charset_collate = '';
if ( ! empty( $wpdb->charset ) ) {
$charset_collate = "DEFAULT CHARACTER SET $wpdb->charset";
}
if ( ! empty( $wpdb->collate ) ) {
$charset_collate .= " COLLATE $wpdb->collate";
}
$sql = "CREATE TABLE IF NOT EXISTS `$tablename_queue` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`local_path` varchar(500) NOT NULL DEFAULT '',
`remote_path` varchar(500) NOT NULL DEFAULT '',
`command` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '1 - Upload, 2 - Delete, 3 - Purge',
`last_error` varchar(150) NOT NULL DEFAULT '',
`date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (`id`),
KEY `date` (`date`)
) $charset_collate;";
$wpdb->query( $sql );
if ( ! $wpdb->result ) {
throw new Util_Environment_Exception(
esc_html(
sprintf(
// Translators: 1 Tablename for queue.
__( 'Can\'t create table %1$s.', 'w3-total-cache' ),
$tablename_queue
)
)
);
}
$sql = "CREATE TABLE IF NOT EXISTS `$tablename_map` (
-- Relative file path.
-- For reference, not actually used for finding files.
path TEXT NOT NULL,
-- MD5 hash of remote path, used for finding files.
path_hash VARCHAR(32) CHARACTER SET ascii NOT NULL,
type tinyint(1) NOT NULL DEFAULT '0',
-- Google Drive: document identifier
remote_id VARCHAR(200) CHARACTER SET ascii,
PRIMARY KEY (path_hash),
KEY `remote_id` (`remote_id`)
) $charset_collate";
$wpdb->query( $sql );
if ( ! $wpdb->result ) {
throw new Util_Environment_Exception(
esc_html(
sprintf(
// Translators: 1 Tablename for map.
__( 'Can\'t create table %1$s.', 'w3-total-cache' ),
$tablename_map
)
)
);
}
}
/**
* Generates the SQL query to create the CDN queue table.
*
* This method constructs the SQL query needed to create the CDN queue table in the database. It includes the table's
* columns and their types, as well as any necessary character set and collation settings. The query ensures that the
* table is dropped if it exists and creates a new one with the appropriate schema.
*
* @return string The SQL query to create the CDN queue table.
*/
private function generate_table_sql() {
global $wpdb;
$charset_collate = '';
if ( ! empty( $wpdb->charset ) ) {
$charset_collate = "DEFAULT CHARACTER SET $wpdb->charset";
}
if ( ! empty( $wpdb->collate ) ) {
$charset_collate .= " COLLATE $wpdb->collate";
}
$sql = sprintf( 'DROP TABLE IF EXISTS `%s%s`;', $wpdb->base_prefix, W3TC_CDN_TABLE_QUEUE );
$sql .= "\n" . sprintf(
"CREATE TABLE IF NOT EXISTS `%s%s` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`local_path` varchar(500) NOT NULL DEFAULT '',
`remote_path` varchar(500) NOT NULL DEFAULT '',
`command` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '1 - Upload, 2 - Delete, 3 - Purge',
`last_error` varchar(150) NOT NULL DEFAULT '',
`date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (`id`),
KEY `date` (`date`)
) $charset_collate;",
$wpdb->base_prefix,
W3TC_CDN_TABLE_QUEUE
);
return $sql;
}
/**
* Unschedules the cron job for processing the CDN queue.
*
* This method checks if the cron job for processing the CDN queue is scheduled. If it is, it clears the scheduled hook
* to stop the cron job from running in the future.
*
* @return void
*/
private function unschedule_queue_process() {
if ( wp_next_scheduled( 'w3_cdn_cron_queue_process' ) ) {
wp_clear_scheduled_hook( 'w3_cdn_cron_queue_process' );
}
}
/**
* Unschedules the cron job for uploading to the CDN.
*
* This method checks if the cron job for uploading files to the CDN is scheduled. If it is, it clears the scheduled hook
* to stop the cron job from running in the future.
*
* @return void
*/
private function unschedule_upload() {
if ( wp_next_scheduled( 'w3_cdn_cron_upload' ) ) {
wp_clear_scheduled_hook( 'w3_cdn_cron_upload' );
}
}
/**
* Adds CDN-related rules to the configuration.
*
* This method adds CDN-related rules to the specified configuration. It leverages the `Util_Rule` class to insert these
* rules into the appropriate cache file. The rules are wrapped with markers to ensure they are added in the correct position.
*
* @param object $config The configuration object containing CDN settings.
* @param array $exs The existing rules to which the new rules will be added.
*
* @return void
*/
private function rules_add( $config, $exs ) {
Util_Rule::add_rules(
$exs,
Util_Rule::get_browsercache_rules_cache_path(),
$this->rules_generate( $config ),
W3TC_MARKER_BEGIN_CDN,
W3TC_MARKER_END_CDN,
array(
W3TC_MARKER_BEGIN_MINIFY_CORE => 0,
W3TC_MARKER_BEGIN_PGCACHE_CORE => 0,
W3TC_MARKER_BEGIN_BROWSERCACHE_CACHE => 0,
W3TC_MARKER_BEGIN_WORDPRESS => 0,
W3TC_MARKER_END_PGCACHE_CACHE => strlen( W3TC_MARKER_END_PGCACHE_CACHE ) + 1,
W3TC_MARKER_END_MINIFY_CACHE => strlen( W3TC_MARKER_END_MINIFY_CACHE ) + 1,
)
);
}
/**
* Removes CDN-related rules from the configuration.
*
* This method removes any CDN-related rules from the specified configuration. It uses the `Util_Rule` class to remove
* these rules, which are wrapped with markers to identify their location in the configuration file.
*
* @param array $exs The existing rules from which the CDN-related rules will be removed.
*
* @return void
*/
private function rules_remove( $exs ) {
Util_Rule::remove_rules(
$exs,
Util_Rule::get_browsercache_rules_cache_path(),
W3TC_MARKER_BEGIN_CDN,
W3TC_MARKER_END_CDN
);
}
/**
* Generates the appropriate rules based on the environment.
*
* This method generates rules for the CDN based on the server environment. It detects whether the server is using Nginx,
* LiteSpeed, or Apache and calls the corresponding method to generate the rules for that environment.
*
* @param object $config The configuration object containing CDN settings.
* @param bool $cdnftp Whether FTP settings should be included in the rules.
*
* @return string The generated CDN rules for the environment.
*/
private function rules_generate( $config, $cdnftp = false ) {
if ( Util_Environment::is_nginx() ) {
$o = new Cdn_Environment_Nginx( $config );
return $o->generate( $cdnftp );
} elseif ( Util_Environment::is_litespeed() ) {
$o = new Cdn_Environment_LiteSpeed( $config );
return $o->generate( $cdnftp );
} else {
return $this->rules_generate_apache( $config, $cdnftp );
}
}
/**
* Generates Apache-specific CDN rules.
*
* This method generates the CDN rules specifically for Apache-based environments. It adds rules for canonical URLs, CORS,
* and other CDN-specific headers.
*
* @param object $config The configuration object containing CDN settings.
* @param bool $cdnftp Whether FTP settings should be included in the rules.
*
* @return string The generated Apache-specific CDN rules.
*/
private function rules_generate_apache( $config, $cdnftp ) {
$rules = '';
if ( $config->get_boolean( 'cdn.canonical_header' ) ) {
$rules .= $this->canonical( $cdnftp, $config->get_boolean( 'cdn.cors_header' ) );
}
if ( $config->get_boolean( 'cdn.cors_header' ) ) {
$rules .= $this->allow_origin( $cdnftp );
}
if ( strlen( $rules ) > 0 ) {
$rules = W3TC_MARKER_BEGIN_CDN . "\n" . $rules . W3TC_MARKER_END_CDN . "\n";
}
return $rules;
}
/**
* Generates the canonical URL rule.
*
* This method generates the Apache or Nginx rewrite rules to enforce canonical URLs for the CDN. It ensures that requests
* are properly redirected to the correct HTTP or HTTPS version of the URL.
*
* @param bool $cdnftp Whether FTP settings should be included in the rule.
* @param bool $cors_header Whether CORS headers should be included in the rule.
*
* @return string The generated canonical URL rule.
*/
private function canonical( $cdnftp = false, $cors_header = true ) {
$mime_types = include W3TC_INC_DIR . '/mime/other.php';
$extensions = array_keys( $mime_types );
$extensions_lowercase = array_map( 'strtolower', $extensions );
$extensions_uppercase = array_map( 'strtoupper', $extensions );
$host = $cdnftp ? Util_Environment::home_url_host() : '%{HTTP_HOST}';
$rules = '<FilesMatch "\\.(' . implode( '|', array_merge( $extensions_lowercase, $extensions_uppercase ) ) . ")$\">\n";
$rules .= " <IfModule mod_rewrite.c>\n";
$rules .= " RewriteEngine On\n";
$rules .= " RewriteCond %{HTTPS} !=on\n";
$rules .= " RewriteRule .* - [E=CANONICAL:http://$host%{REQUEST_URI},NE]\n";
$rules .= " RewriteCond %{HTTPS} =on\n";
$rules .= " RewriteRule .* - [E=CANONICAL:https://$host%{REQUEST_URI},NE]\n";
$rules .= " </IfModule>\n";
$rules .= " <IfModule mod_headers.c>\n";
$rules .= ' Header set Link "<%{CANONICAL}e>; rel=\"canonical\""' . "\n";
$rules .= " </IfModule>\n";
$rules .= "</FilesMatch>\n";
return $rules;
}
/**
* Generates the CORS allow origin rule.
*
* This method generates the Apache or Nginx header rule for allowing cross-origin resource sharing (CORS). It sets the
* `Access-Control-Allow-Origin` header to allow any origin, which is useful for CDN assets.
*
* @param bool $cdnftp Whether FTP settings should be included in the rule.
*
* @return string The generated CORS allow origin rule.
*/
private function allow_origin( $cdnftp = false ) {
$r = "<IfModule mod_headers.c>\n";
$r .= " Header set Access-Control-Allow-Origin \"*\"\n";
$r .= "</IfModule>\n";
if ( ! $cdnftp ) {
return $r;
} else {
return "<FilesMatch \"\.(ttf|ttc|otf|eot|woff|woff2|font.css)$\">\n" . $r . "</FilesMatch>\n";
}
}
/**
* Adds browser cache extension rules for CDN in the configuration.
*
* This method adds browser caching rules for CDN extensions to the specified configuration. It detects the server type
* (Nginx or LiteSpeed) and generates the appropriate extension rules based on the server.
*
* @param array $extensions The existing list of extensions to be cached.
* @param object $config The configuration object containing CDN settings.
* @param string $section The section of the configuration to add the rules to.
*
* @return array The updated list of extensions with CDN-related rules.
*/
public function w3tc_browsercache_rules_section_extensions( $extensions, $config, $section ) {
if ( Util_Environment::is_nginx() ) {
$o = new Cdn_Environment_Nginx( $config );
$extensions = $o->w3tc_browsercache_rules_section_extensions( $extensions, $section );
} elseif ( Util_Environment::is_litespeed() ) {
$o = new Cdn_Environment_LiteSpeed( $config );
$extensions = $o->w3tc_browsercache_rules_section_extensions( $extensions, $section );
}
return $extensions;
}
/**
* Adds browser cache section rules for CDN in the configuration.
*
* This method adds browser caching rules for the CDN to the specified section in the configuration. It works specifically
* for LiteSpeed servers, ensuring that the correct rules are added for CDN caching.
*
* @param string $section_rules The existing section rules to be updated.
* @param object $config The configuration object containing CDN settings.
* @param string $section The section of the configuration to add the rules to.
*
* @return string The updated section rules with CDN-related browser cache rules.
*/
public function w3tc_browsercache_rules_section( $section_rules, $config, $section ) {
if ( Util_Environment::is_litespeed() ) {
$o = new Cdn_Environment_LiteSpeed( $config );
$section_rules = $o->w3tc_browsercache_rules_section( $section_rules, $section );
}
return $section_rules;
}
}

View File

@@ -0,0 +1,162 @@
<?php
/**
* File: Cdn_Environment_LiteSpeed.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cdn_Environment_LiteSpeed
*
* CDN rules generation for LiteSpeed
*
* phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter
*/
class Cdn_Environment_LiteSpeed {
/**
* Config.
*
* @var Config
*/
private $c;
/**
* Constructor for the Cdn_Environment_LiteSpeed class.
*
* This constructor initializes the object with the provided configuration.
* It is typically called when an instance of the class is created to set up the configuration.
*
* @param object $config The configuration object that contains necessary settings.
*
* @return void
*/
public function __construct( $config ) {
$this->c = $config;
}
/**
* Generates CDN configuration rules for the LiteSpeed server.
*
* This method generates and returns the LiteSpeed configuration rules for handling fonts and headers, including
* canonical and CORS headers. It processes the provided CDN FTP configuration and applies filters to modify the rules.
*
* @param object $cdnftp The CDN FTP object used for generating the canonical header.
*
* @return string The generated CDN configuration rules.
*/
public function generate( $cdnftp ) {
$section_rules = array(
'other' => array(),
'add_header' => array(),
);
if ( $this->c->get_boolean( 'cdn.cors_header' ) ) {
$section_rules['add_header'][] = 'set Access-Control-Allow-Origin "*"';
}
$canonical_header = $this->generate_canonical( $cdnftp );
if ( ! empty( $canonical_header ) ) {
$section_rules['add_header'][] = $canonical_header;
}
if ( empty( $section_rules['add_header'] ) ) {
return '';
}
$section_rules = apply_filters( 'w3tc_cdn_rules_section', $section_rules, $this->c );
$context_rules[] = ' extraHeaders <<<END_extraHeaders';
foreach ( $section_rules['add_header'] as $line ) {
$context_rules[] = ' ' . $line;
}
$context_rules[] = ' END_extraHeaders';
$rules = array();
$rules[] = 'context exp:^.*(ttf|ttc|otf|eot|woff|woff2|font.css)$ {';
$rules[] = ' location $DOC_ROOT/$0';
$rules[] = ' allowBrowse 1';
$rules[] = implode( "\n", $context_rules );
$rules[] = '}';
return W3TC_MARKER_BEGIN_CDN . "\n" . implode( "\n", $rules ) . "\n" . W3TC_MARKER_END_CDN . "\n";
}
/**
* Generates the canonical header for the CDN configuration.
*
* This method generates a canonical header to be used in the CDN configuration if the 'cdn.canonical_header' setting
* is enabled in the configuration. It constructs a 'Link' header with the canonical URL based on the home URL.
*
* @param bool $cdnftp Optional. A flag to include CDN FTP in the canonical header generation. Defaults to false.
*
* @return string|null The canonical header string or null if not generated.
*/
public function generate_canonical( $cdnftp = false ) {
if ( ! $this->c->get_boolean( 'cdn.canonical_header' ) ) {
return null;
}
$home_url = get_home_url();
$parse_url = @parse_url( $home_url ); // phpcs:ignore
if ( ! isset( $parse_url['host'] ) ) {
return null;
}
return "set Link '<" . $parse_url['scheme'] . '://' . $parse_url['host'] . '%{REQUEST_URI}e>; rel="canonical"' . "'";
/* phpcs:ignore Squiz.PHP.CommentedOutCode.Found
$rules .= " RewriteRule .* - [E=CANONICAL:https://$host%{REQUEST_URI},NE]\n";
$rules .= " </IfModule>\n";
$rules .= " <IfModule mod_headers.c>\n";
$rules .= ' Header set Link "<%{CANONICAL}e>; rel=\"canonical\""' . "\n";
return 'set Link "<%{CANONICAL}e>; rel=\"canonical\""' . "\n";
*/
}
/**
* Modifies the extensions for the browser cache rules section.
*
* This method adjusts the list of file extensions for which browser cache rules should be applied. If CORS headers
* are enabled, certain font file types (e.g., ttf, otf, eot, woff, woff2) are removed from the extensions list.
*
* @param array $extensions The array of extensions for which rules are applied.
* @param string $section The section to which these rules apply.
*
* @return array The modified array of extensions.
*/
public function w3tc_browsercache_rules_section_extensions( $extensions, $section ) {
// CDN adds own rules for those extensions.
if ( $this->c->get_boolean( 'cdn.cors_header' ) ) {
unset( $extensions['ttf|ttc'] );
unset( $extensions['otf'] );
unset( $extensions['eot'] );
unset( $extensions['woff'] );
unset( $extensions['woff2'] );
}
return $extensions;
}
/**
* Modifies the browser cache rules section with the canonical header.
*
* This method adds the canonical header to the section rules if the 'cdn.canonical_header' setting is enabled.
* It modifies the provided section rules to include the generated canonical header.
*
* @param array $section_rules The current set of section rules.
* @param string $section The section to which the rules apply.
*
* @return array The modified set of section rules with the canonical header included.
*/
public function w3tc_browsercache_rules_section( $section_rules, $section ) {
$canonical_header = $this->generate_canonical();
if ( ! empty( $canonical_header ) ) {
$section_rules['add_header'][] = $canonical_header;
}
return $section_rules;
}
}

View File

@@ -0,0 +1,115 @@
<?php
/**
* File: Cdn_Environment_Nginx.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cdn_Environment_Nginx
*
* CDN rules generation for Nginx
*
* phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter
*/
class Cdn_Environment_Nginx {
/**
* Config.
*
* @var Config
*/
private $c;
/**
* Constructor for initializing the CDN environment with the given configuration.
*
* This constructor initializes the object with the provided configuration. The configuration is typically
* an array or object that contains various settings required to generate the appropriate Nginx rules and
* configurations for the CDN environment.
*
* @param mixed $config The configuration object or array used to initialize the CDN environment.
*/
public function __construct( $config ) {
$this->c = $config;
}
/**
* Generates Nginx configuration rules for the CDN environment.
*
* This method generates the necessary Nginx rules for the CDN environment based on the configuration and the
* presence of specific CDN settings such as CORS headers and canonical headers. The generated rules are returned
* as a string, ready to be used in the Nginx configuration file.
*
* @param mixed $cdnftp The CDN FTP configuration object used to generate canonical headers.
*
* @return string The generated Nginx configuration rules for the CDN environment.
*/
public function generate( $cdnftp ) {
$rules = '';
$rule = $this->generate_canonical( $cdnftp );
if ( ! empty( $rule ) ) {
$rules = $rule . "\n";
}
if ( $this->c->get_boolean( 'cdn.cors_header' ) ) {
$rules_a = Dispatcher::nginx_rules_for_browsercache_section( $this->c, 'other', true );
$rules_a[] = 'add_header Access-Control-Allow-Origin "*";';
$rules .= "location ~ \\.(ttf|ttc|otf|eot|woff|woff2|font.css)\$ {\n " . implode( "\n ", $rules_a ) . "\n}\n";
}
if ( strlen( $rules ) > 0 ) {
$rules = W3TC_MARKER_BEGIN_CDN . "\n" . $rules . W3TC_MARKER_END_CDN . "\n";
}
return $rules;
}
/**
* Generates the canonical header rule for Nginx.
*
* This method generates the canonical header rule to be added to the Nginx configuration. The canonical header
* is used to indicate the preferred version of a resource in case there are multiple versions available.
* The rule is only generated if the 'cdn.canonical_header' setting is enabled in the configuration.
*
* @param bool $cdnftp Whether to use the FTP configuration to determine the home URL.
*
* @return string|null The canonical header rule, or null if the rule is not enabled.
*/
public function generate_canonical( $cdnftp = false ) {
if ( ! $this->c->get_boolean( 'cdn.canonical_header' ) ) {
return null;
}
$home = ( $cdnftp ? Util_Environment::home_url_host() : '$host' );
return 'add_header Link "<$scheme://' . $home . '$request_uri>; rel=\"canonical\"";';
}
/**
* Modifies the list of extensions for the browser cache rules section based on the CDN configuration.
*
* This method modifies the list of file extensions that are included in the browser cache rules section of the
* Nginx configuration. If the CDN settings specify that CORS headers should be applied, certain font file
* extensions (such as ttf, otf, woff) are excluded from the list of extensions.
*
* @param array $extensions The list of file extensions to be included in the browser cache rules.
* @param string $section The section of the configuration where the extensions are being applied.
*
* @return array The modified list of extensions for the browser cache rules section.
*/
public function w3tc_browsercache_rules_section_extensions( $extensions, $section ) {
// CDN adds own rules for those extensions.
if ( $this->c->get_boolean( 'cdn.cors_header' ) ) {
unset( $extensions['ttf|ttc'] );
unset( $extensions['otf'] );
unset( $extensions['eot'] );
unset( $extensions['woff'] );
unset( $extensions['woff2'] );
}
return $extensions;
}
}

View File

@@ -0,0 +1,153 @@
<?php
/**
* File: Cdn_GeneralPage_View.php
*
* @package W3TC
*/
namespace W3TC;
defined( 'W3TC' ) || die;
Util_Ui::postbox_header_tabs(
wp_kses(
sprintf(
// translators: 1 opening HTML acronym tag, 2 closing HTML acronym tag.
__(
'%1$sCDN%2$s',
'w3-total-cache'
),
'<acronym title="' . __( 'Content Delivery Network', 'w3-total-cache' ) . '">',
'</acronym>'
),
array(
'acronym' => array(
'title' => array(),
),
)
),
esc_html__(
'Content Delivery Network (CDN) is a powerful feature that can significantly enhance the performance of
your WordPress website. By leveraging a distributed network of servers located worldwide, a CDN helps
deliver your website\'s static files, such as images, CSS, and JavaScript, to visitors more efficiently.
This reduces the latency and improves the loading speed of your website, resulting in a faster and
smoother browsing experience for your users. With W3 Total Cache\'s CDN integration, you can easily
configure and connect your website to a CDN service of your choice, unleashing the full potential of
your WordPress site\'s speed optimization.',
'w3-total-cache'
),
'',
'cdn',
Util_UI::admin_url( 'admin.php?page=w3tc_cdn' ),
'w3tc_premium_services'
);
Util_Ui::config_overloading_button(
array(
'key' => 'cdn.configuration_overloaded',
)
);
?>
<div id="w3tc-bunnycdn-ad-general">
<?php
if ( ! $cdn_enabled ) {
echo wp_kses(
sprintf(
// translators: 1 opening HTML strong tag, 2 closing HTML strong tag,
// translators: 3 HTML input for Bunny CDN sign up, 4 HTML img tag for Bunny CDN white logo.
__(
'%1$sLooking for a top rated CDN Provider? Try Bunny CDN.%2$s%3$s%4$s',
'w3-total-cache'
),
'<strong>',
'</strong>',
Util_Ui::button_link(
__( 'Sign up now to enjoy a special offer!', 'w3-total-cache' ),
esc_url( W3TC_BUNNYCDN_SIGNUP_URL ),
true,
'w3tc-bunnycdn-promotion-button',
'w3tc-bunnycdn-promotion-button'
),
'<img class="w3tc-bunnycdn-icon-white" src="' . esc_url( plugins_url( '/pub/img/w3tc_bunnycdn_icon_white.png', W3TC_FILE ) ) . '" alt="Bunny CDN Icon White">'
),
array(
'strong' => array(),
'img' => array(
'class' => array(),
'src' => array(),
'alt' => array(),
'width' => array(),
'height' => array(),
),
'input' => array(
'type' => array(),
'name' => array(),
'class' => array(),
'value' => array(),
'onclick' => array(),
),
)
);
}
?>
</div>
<table class="form-table">
<?php
Util_Ui::config_item(
array(
'key' => 'cdn.enabled',
'control' => 'checkbox',
'checkbox_label' => __( 'Enable', 'w3-total-cache' ),
'description' => wp_kses(
sprintf(
// translators: 1 opening HTML acronym tag, 2 closing HTML acronym tag,
// translators: 3 opening HTML acronym tag, 4 closing acronym tag.
__(
'Theme files, media library attachments, %1$sCSS%2$s, and %3$sJS%4$s files will load quickly for site visitors.',
'w3-total-cache'
),
'<acronym title="' . __( 'Cascading Style Sheet', 'w3-total-cache' ) . '">',
'</acronym>',
'<acronym title="' . __( 'JavaScript', 'w3-total-cache' ) . '">',
'</acronym>'
),
array(
'acronym' => array(
'title' => array(),
),
)
),
)
);
Util_Ui::config_item(
array(
'key' => 'cdn.engine',
'control' => 'selectbox',
'selectbox_values' => $engine_values,
'selectbox_optgroups' => $engine_optgroups,
'description' => wp_kses(
sprintf(
// translators: 1 opening HTML acronym tag, 2 closing HTML acronym tag.
__(
'Select the %1$sCDN%2$s type you wish to use.',
'w3-total-cache'
),
'<acronym title="' . __( 'Content Delivery Network', 'w3-total-cache' ) . '">',
'</acronym>'
),
array(
'acronym' => array(
'title' => array(),
),
)
),
)
);
?>
</table>
<?php
do_action( 'w3tc_settings_general_boxarea_cdn_footer' );
?>
<?php Util_Ui::postbox_footer(); ?>

View File

@@ -0,0 +1,111 @@
<?php
/**
* File: Cdn_GoogleDrive_AdminActions.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cdn_GoogleDrive_AdminActions
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
*/
class Cdn_GoogleDrive_AdminActions {
/**
* Config.
*
* @var Config
*/
private $_config = null;
/**
* Initializes the class and configures necessary settings.
*
* This constructor retrieves the configuration settings using the Dispatcher class
* and stores it in the class property for further use.
*
* @return void
*/
public function __construct() {
$this->_config = Dispatcher::config();
}
/**
* Handles the return from Google Drive authentication and renders the authentication view.
*
* This method is invoked after a user has completed the Google Drive authentication process.
* It initializes the `Cdn_GoogleDrive_Popup_AuthReturn` view, renders it, and then exits the script
* to prevent further execution.
*
* @return void
*/
public function w3tc_cdn_google_drive_auth_return() {
$view = new Cdn_GoogleDrive_Popup_AuthReturn();
$view->render();
exit();
}
/**
* Sets up the Google Drive authentication and folder configuration.
*
* This method handles the Google Drive authorization by retrieving client details (e.g., client ID,
* access token, refresh token) from the request. It sets up the Google Client, requests folder details
* from the Google Drive API, creates a new folder if necessary, and saves the configuration to the plugin's settings.
* Additionally, it sets the folder permissions and redirects the user back to the CDN settings page.
*
* @return void
*/
public function w3tc_cdn_google_drive_auth_set() {
// thanks wp core for wp_magic_quotes hell.
$client_id = Util_Request::get_string( 'client_id' );
$access_token = Util_Request::get_string( 'access_token' );
$refresh_token = Util_Request::get_string( 'refresh_token' );
$client = new \W3TCG_Google_Client();
$client->setClientId( $client_id );
$client->setAccessToken( $access_token );
// get folder details.
$service = new \W3TCG_Google_Service_Drive( $client );
if ( empty( Util_Request::get_string( 'folder' ) ) ) {
$file = new \W3TCG_Google_Service_Drive_DriveFile(
array(
'title' => Util_Request::get_string( 'folder_new' ),
'mimeType' => 'application/vnd.google-apps.folder',
)
);
$created_file = $service->files->insert( $file );
$used_folder_id = $created_file->id;
} else {
$used_folder_id = Util_Request::get_string( 'folder' );
}
$permission = new \W3TCG_Google_Service_Drive_Permission();
$permission->setValue( '' );
$permission->setType( 'anyone' );
$permission->setRole( 'reader' );
$service->permissions->insert( $used_folder_id, $permission );
$used_folder = $service->files->get( $used_folder_id );
// save new configuration.
delete_transient( 'w3tc_cdn_google_drive_folder_ids' );
$this->_config->set( 'cdn.google_drive.client_id', $client_id );
$this->_config->set( 'cdn.google_drive.refresh_token', $refresh_token );
$this->_config->set( 'cdn.google_drive.folder.id', $used_folder->id );
$this->_config->set( 'cdn.google_drive.folder.title', $used_folder->title );
$this->_config->set( 'cdn.google_drive.folder.url', $used_folder->webViewLink ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$this->_config->save();
$cs = Dispatcher::config_state();
$cs->set( 'cdn.google_drive.access_token', $access_token );
$cs->save();
wp_safe_redirect( 'admin.php?page=w3tc_cdn', false );
}
}

View File

@@ -0,0 +1,74 @@
<?php
/**
* File: Cdn_GoogleDrive_Page.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class: Cdn_GoogleDrive_Page
*/
class Cdn_GoogleDrive_Page {
/**
* Enqueues scripts for the Google Drive CDN integration in the WordPress admin.
*
* This method enqueues the JavaScript file necessary for the Google Drive CDN integration and localizes script
* data with authorization URLs and a return URL. It also handles the Google OAuth callback by passing relevant
* data through to the script.
*
* @return void
*/
public static function admin_print_scripts_w3tc_cdn() {
wp_enqueue_script(
'w3tc_cdn_google_drive',
plugins_url( 'Cdn_GoogleDrive_Page_View.js', W3TC_FILE ),
array( 'jquery' ),
'1.0',
false
);
$path = 'admin.php?page=w3tc_cdn';
$return_url = self_admin_url( $path );
wp_localize_script(
'w3tc_cdn_google_drive',
'w3tc_cdn_google_drive_url',
array( W3TC_GOOGLE_DRIVE_AUTHORIZE_URL . '?return_url=' . rawurlencode( $return_url ) )
);
// it's return from google oauth.
if ( ! empty( Util_Request::get_string( 'oa_client_id' ) ) ) {
$path = wp_nonce_url( 'admin.php', 'w3tc' ) .
'&page=w3tc_cdn&w3tc_cdn_google_drive_auth_return';
foreach ( $_GET as $key => $value ) { // phpcs:ignore
if ( substr( $key, 0, 3 ) === 'oa_' ) {
$path .= '&' . rawurlencode( $key ) . '=' . rawurlencode( Util_Request::get_string( $key ) );
}
}
$popup_url = self_admin_url( $path );
wp_localize_script(
'w3tc_cdn_google_drive',
'w3tc_cdn_google_drive_popup_url',
array( $popup_url )
);
}
}
/**
* Displays the configuration settings for the Google Drive CDN integration in the W3 Total Cache settings page.
*
* This method loads the Google Drive CDN settings section in the W3 Total Cache settings page by including a
* specific view file and passing configuration data.
*
* @return void
*/
public static function w3tc_settings_cdn_boxarea_configuration() {
$config = Dispatcher::config();
require W3TC_DIR . '/Cdn_GoogleDrive_Page_View.php';
}
}

View File

@@ -0,0 +1,20 @@
jQuery(function($) {
$('.w3tc_cdn_google_drive_authorize').click(function() {
window.location = w3tc_cdn_google_drive_url[0];
});
if (window.w3tc_cdn_google_drive_popup_url) {
W3tc_Lightbox.open({
id:'w3tc-overlay',
close: '',
width: 800,
height: 500,
url: w3tc_cdn_google_drive_popup_url[0],
callback: function(lightbox) {
lightbox.resize();
},
onClose: function() {
}
});
}
});

View File

@@ -0,0 +1,46 @@
<?php
/**
* File: Cdn_GoogleDrive_Page_View.php
*
* @package W3TC
*/
namespace W3TC;
if ( ! defined( 'W3TC' ) ) {
die();
}
$refresh_token = $config->get_string( 'cdn.google_drive.refresh_token' );
?>
<tr>
<th style="width: 300px;"><label><?php esc_html_e( 'Authorize:', 'w3-total-cache' ); ?></label></th>
<td>
<?php if ( empty( $refresh_token ) ) : ?>
<input class="w3tc_cdn_google_drive_authorize button" type="button"
value="<?php esc_attr_e( 'Authorize', 'w3-total-cache' ); ?>" />
<?php else : ?>
<input class="w3tc_cdn_google_drive_authorize button" type="button"
value="<?php esc_attr_e( 'Reauthorize', 'w3-total-cache' ); ?>" />
<?php endif ?>
</td>
</tr>
<?php if ( ! empty( $refresh_token ) ) : ?>
<tr>
<th><label for="cdn_s3_bucket"><?php esc_html_e( 'Folder:', 'w3-total-cache' ); ?></label></th>
<td>
<a href="<?php echo esc_url( $config->get_string( 'cdn.google_drive.folder.url' ) ); ?>">/<?php echo esc_html( $config->get_string( 'cdn.google_drive.folder.title' ) ); ?></a>
</td>
</tr>
<tr>
<th colspan="2">
<input id="cdn_test"
class="button {type: 'google_drive', nonce: '<?php echo esc_attr( wp_create_nonce( 'w3tc' ) ); ?>'}"
type="button"
value="<?php esc_attr_e( 'Test upload', 'w3-total-cache' ); ?>" />
<span id="cdn_test_status" class="w3tc-status w3tc-process"></span>
</th>
</tr>
<?php endif ?>

View File

@@ -0,0 +1,57 @@
<?php
/**
* File: Cdn_GoogleDrive_Popup_AuthReturn.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cdn_GoogleDrive_Popup_AuthReturn
*/
class Cdn_GoogleDrive_Popup_AuthReturn {
/**
* Renders the Google Drive authorization return view.
*
* This method retrieves the client ID, refresh token, and access token from the request,
* sets the access token for a new Google client, and queries Google Drive for folders.
* It then filters the folders to include only those that are direct children of the root.
* Finally, it includes the view to display the results.
*
* @return void
*/
public function render() {
$client_id = Util_Request::get_string( 'oa_client_id' );
$refresh_token = Util_Request::get_string( 'oa_refresh_token' );
$token_array = array(
'access_token' => Util_Request::get_string( 'oa_access_token' ),
'token_type' => Util_Request::get_string( 'oa_token_type' ),
'expires_in' => Util_Request::get_string( 'oa_expires_in' ),
'created' => Util_Request::get_string( 'oa_created' ),
);
$access_token = wp_json_encode( $token_array );
$client = new \W3TCG_Google_Client();
$client->setClientId( $client_id );
$client->setAccessToken( $access_token );
$service = new \W3TCG_Google_Service_Drive( $client );
$items = $service->files->listFiles(
array(
'q' => "mimeType = 'application/vnd.google-apps.folder'",
)
);
$folders = array();
foreach ( $items as $item ) {
if ( count( $item->parents ) > 0 && $item->parents[0]->isRoot ) {
$folders[] = $item;
}
}
include W3TC_DIR . '/Cdn_GoogleDrive_Popup_AuthReturn_View.php';
}
}

View File

@@ -0,0 +1,60 @@
<?php
/**
* File: Cdn_GoogleDrive_Popup_AuthReturn_View.php
*
* @package W3TC
*/
namespace W3TC;
if ( ! defined( 'W3TC' ) ) {
die();
}
?>
<form action="admin.php?page=w3tc_cdn" method="post" style="padding: 20px">
<?php
Util_Ui::hidden( 'w3tc-googledrive-clientid', 'client_id', $client_id );
Util_Ui::hidden( 'w3tc-googledrive-access-token', 'access_token', $access_token );
Util_Ui::hidden( 'w3tc-googledrive-refresh-token', 'refresh_token', $refresh_token );
echo wp_kses(
Util_Ui::nonce_field( 'w3tc' ),
array(
'input' => array(
'type' => array(),
'name' => array(),
'value' => array(),
),
)
);
?>
<br /><br />
<div class="metabox-holder">
<?php Util_Ui::postbox_header( esc_html__( 'Select folder', 'w3-total-cache' ) ); ?>
<table class="form-table">
<tr>
<td><?php esc_html_e( 'Folder:', 'w3-total-cache' ); ?></td>
<td>
<?php foreach ( $folders as $folder ) : ?>
<label>
<input name="folder" type="radio" class="w3tc-ignore-change"
value="<?php echo esc_attr( $folder->id ); ?>" />
<?php echo esc_html( $folder->title ); ?>
</label><br />
<?php endforeach ?>
<label>
<input name="folder" type="radio" class="w3tc-ignore-change" value="" />
<?php esc_html_e( 'Add new folder:', 'w3-total-cache' ); ?>
</label>
<input name="folder_new" type="text" class="w3tc-ignore-change" />
</td>
</tr>
</table>
<p class="submit">
<input type="submit" name="w3tc_cdn_google_drive_auth_set"
class="w3tc-button-save button-primary"
value="<?php esc_attr_e( 'Apply', 'w3-total-cache' ); ?>" />
</p>
<?php Util_Ui::postbox_footer(); ?>
</div>
</form>

View File

@@ -0,0 +1,98 @@
<?php
/**
* File: Cdn_Page.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cdn_Page
*
* phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
* phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
*/
class Cdn_Page extends Base_Page_Settings {
/**
* Current page.
*
* @var string
*/
protected $_page = 'w3tc_cdn';
/**
* Displays the CDN settings page.
*
* This method retrieves the CDN-related configuration settings and checks if the CDN is enabled and authorized.
* It also checks if the engine supports mirroring and purging, as well as whether minification and browser cache
* settings are enabled. The necessary settings are then passed to the view for rendering the CDN options page.
*
* @return void
*/
public function view() {
$config = Dispatcher::config();
$cdn_engine = $config->get_string( 'cdn.engine' );
$cdn_enabled = $config->get_boolean( 'cdn.enabled' );
$cdnfsd_engine = $config->get_string( 'cdnfsd.engine' );
$cdnfsd_enabled = $config->get_boolean( 'cdnfsd.enabled' );
$cdn_mirror = Cdn_Util::is_engine_mirror( $cdn_engine );
$cdn_mirror_purge_all = Cdn_Util::can_purge_all( $cdn_engine );
$cdn_common = Dispatcher::component( 'Cdn_Core' );
$cdn = $cdn_common->get_cdn();
$cdn_supports_header = W3TC_CDN_HEADER_MIRRORING === $cdn->headers_support();
$minify_enabled = (
$config->get_boolean( 'minify.enabled' ) &&
Util_Rule::can_check_rules() &&
$config->get_boolean( 'minify.rewrite' ) &&
( ! $config->get_boolean( 'minify.auto' ) || Cdn_Util::is_engine_mirror( $config->get_string( 'cdn.engine' ) ) )
);
$cookie_domain = $this->get_cookie_domain();
$set_cookie_domain = $this->is_cookie_domain_enabled();
// Required for Update Media Query String button.
$browsercache_enabled = $config->get_boolean( 'browsercache.enabled' );
$browsercache_update_media_qs = ( $config->get_boolean( 'browsercache.cssjs.replace' ) || $config->get_boolean( 'browsercache.other.replace' ) );
// Get CDN and CDN FSD status.
$cdn_core = new Cdn_Core();
$is_cdn_authorized = $cdn_core->is_cdn_authorized();
$is_cdnfsd_authorized = $cdn_core->is_cdnfsd_authorized();
include W3TC_INC_DIR . '/options/cdn.php';
}
/**
* Retrieves the domain for the site's cookie.
*
* This method retrieves the domain of the site where the cookie will be set, typically used to determine the
* correct domain for cookies. It first attempts to parse the site URL from the WordPress settings, and if that
* fails, it uses the HTTP_HOST from the server.
*
* @return string The domain name of the site for cookies.
*/
public function get_cookie_domain() {
$site_url = get_option( 'siteurl' );
$parse_url = @wp_parse_url( $site_url );
if ( $parse_url && ! empty( $parse_url['host'] ) ) {
return $parse_url['host'];
}
return isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '';
}
/**
* Checks if the cookie domain is enabled.
*
* This method compares the site's cookie domain to the value defined in the `COOKIE_DOMAIN` constant.
* It returns true if the `COOKIE_DOMAIN` constant is defined and matches the site's cookie domain.
*
* @return bool True if the cookie domain is enabled, false otherwise.
*/
public function is_cookie_domain_enabled() {
$cookie_domain = $this->get_cookie_domain();
return defined( 'COOKIE_DOMAIN' ) && COOKIE_DOMAIN === $cookie_domain;
}
}

View File

@@ -0,0 +1,43 @@
<?php
/**
* File: Cdn_Page_View_Fsd_HeaderActions.php
*
* @package W3TC
*/
namespace W3TC;
if ( ! defined( 'W3TC' ) ) {
die();
}
?>
<p>
<?php
echo wp_kses(
sprintf(
Util_Ui::button_link(
// translators: 1 opening HTML acronym tag, 2 closing HTML acronym tag.
__(
'Purge %1$sCDN%2$s completely',
'w3-total-cache'
),
Util_Ui::url( array( 'w3tc_cdn_flush' => 'y' ) )
),
'<acronym title="' . esc_attr__( 'Content Delivery Network', 'w3-total-cache' ) . '">',
'</acronym>'
),
array(
'input' => array(
'type' => array(),
'name' => array(),
'class' => array(),
'value' => array(),
'onclick' => array(),
),
'acronym' => array(
'title' => array(),
),
)
);
?>
</p>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,216 @@
<?php
/**
* File: Cdn_Plugin_Admin.php
*
* @since 0.9.5.4
* @package W3TC
*/
namespace W3TC;
/**
* Class Cdn_Plugin_Admin
*/
class Cdn_Plugin_Admin {
/**
* Runs the CDN plugin by setting up various hooks and filters.
*
* @return void
*/
public function run() {
$config_labels = new Cdn_ConfigLabels();
\add_filter( 'w3tc_config_labels', array( $config_labels, 'config_labels' ) );
$c = Dispatcher::config();
$cdn_engine = $c->get_string( 'cdn.engine' );
if ( $c->get_boolean( 'cdn.enabled' ) ) {
$admin_notes = new Cdn_AdminNotes();
\add_filter( 'w3tc_notes', array( $admin_notes, 'w3tc_notes' ) );
\add_filter( 'w3tc_errors', array( $admin_notes, 'w3tc_errors' ) );
if ( $c->get_boolean( 'cdn.admin.media_library' ) && $c->get_boolean( 'cdn.uploads.enable' ) ) {
\add_filter( 'wp_get_attachment_url', array( $this, 'wp_get_attachment_url' ), 0 );
\add_filter( 'attachment_link', array( $this, 'wp_get_attachment_url' ), 0 );
}
}
// Always show the Bunny CDN widget on dashboard.
\add_action( 'admin_init_w3tc_dashboard', array( '\W3TC\Cdn_BunnyCdn_Widget', 'admin_init_w3tc_dashboard' ) );
// Attach to actions without firing class loading at all without need.
switch ( $cdn_engine ) {
case 'google_drive':
\add_action( 'w3tc_settings_cdn_boxarea_configuration', array( '\W3TC\Cdn_GoogleDrive_Page', 'w3tc_settings_cdn_boxarea_configuration' ) );
break;
case 'rackspace_cdn':
\add_filter( 'w3tc_admin_actions', array( '\W3TC\Cdn_RackSpaceCdn_Page', 'w3tc_admin_actions' ) );
\add_action( 'w3tc_ajax', array( '\W3TC\Cdn_RackSpaceCdn_Popup', 'w3tc_ajax' ) );
\add_action( 'w3tc_settings_cdn_boxarea_configuration', array( '\W3TC\Cdn_RackSpaceCdn_Page', 'w3tc_settings_cdn_boxarea_configuration' ) );
break;
case 'rscf':
\add_action( 'w3tc_ajax', array( '\W3TC\Cdn_RackSpaceCloudFiles_Popup', 'w3tc_ajax' ) );
\add_action( 'w3tc_settings_cdn_boxarea_configuration', array( '\W3TC\Cdn_RackSpaceCloudFiles_Page', 'w3tc_settings_cdn_boxarea_configuration' ) );
break;
case 'bunnycdn':
\add_action( 'w3tc_ajax', array( '\W3TC\Cdn_BunnyCdn_Page', 'w3tc_ajax' ) );
\add_action( 'w3tc_ajax', array( '\W3TC\Cdn_BunnyCdn_Popup', 'w3tc_ajax' ) );
\add_action( 'w3tc_settings_cdn_boxarea_configuration', array( '\W3TC\Cdn_BunnyCdn_Page', 'w3tc_settings_cdn_boxarea_configuration' ) );
\add_action( 'w3tc_ajax_cdn_bunnycdn_widgetdata', array( '\W3TC\Cdn_BunnyCdn_Widget', 'w3tc_ajax_cdn_bunnycdn_widgetdata' ) );
\add_action( 'w3tc_purge_urls_box', array( '\W3TC\Cdn_BunnyCdn_Page', 'w3tc_purge_urls_box' ) );
break;
default:
\add_action( 'admin_init_w3tc_dashboard', array( '\W3TC\Cdn_BunnyCdn_Widget', 'admin_init_w3tc_dashboard' ) );
\add_action( 'w3tc_ajax_cdn_bunnycdn_widgetdata', array( '\W3TC\Cdn_BunnyCdn_Widget', 'w3tc_ajax_cdn_bunnycdn_widgetdata' ) );
break;
}
\add_action( 'w3tc_settings_general_boxarea_cdn', array( $this, 'w3tc_settings_general_boxarea_cdn' ) );
}
/**
* Adds configuration options for CDN settings in the general settings box area.
*
* @return void
*/
public function w3tc_settings_general_boxarea_cdn() {
$config = Dispatcher::config();
$engine_optgroups = array();
$engine_values = array();
$optgroup_pull = count( $engine_optgroups );
$engine_optgroups[] = \__( 'Origin Pull / Mirror:', 'w3-total-cache' );
$optgroup_push = count( $engine_optgroups );
$engine_optgroups[] = \__( 'Origin Push:', 'w3-total-cache' );
$engine_values[''] = array(
'label' => 'Select a provider',
);
$engine_values['akamai'] = array(
'label' => \__( 'Akamai', 'w3-total-cache' ),
'optgroup' => $optgroup_pull,
);
$engine_values['cf2'] = array(
'label' => \__( 'Amazon CloudFront', 'w3-total-cache' ),
'disabled' => ! Util_Installed::curl() ? true : null,
'optgroup' => $optgroup_pull,
);
$engine_values['att'] = array(
'label' => \__( 'AT&amp;T', 'w3-total-cache' ),
'optgroup' => $optgroup_pull,
);
$engine_values['bunnycdn'] = array(
'label' => \__( 'Bunny CDN (recommended)', 'w3-total-cache' ),
'optgroup' => $optgroup_pull,
);
$engine_values['cotendo'] = array(
'label' => \__( 'Cotendo (Akamai)', 'w3-total-cache' ),
'optgroup' => $optgroup_pull,
);
$engine_values['mirror'] = array(
'label' => \__( 'Generic Mirror', 'w3-total-cache' ),
'optgroup' => $optgroup_pull,
);
$engine_values['rackspace_cdn'] = array(
'label' => \__( 'RackSpace CDN', 'w3-total-cache' ),
'optgroup' => $optgroup_pull,
);
$engine_values['edgecast'] = array(
'label' => \__( 'Verizon Digital Media Services (EdgeCast) / Media Temple ProCDN', 'w3-total-cache' ),
'optgroup' => $optgroup_pull,
);
$engine_values['cf'] = array(
'disabled' => ! Util_Installed::curl() ? true : null,
'label' => \__( 'Amazon CloudFront Over S3', 'w3-total-cache' ),
'optgroup' => $optgroup_push,
);
$engine_values['s3'] = array(
'disabled' => ! Util_Installed::curl() ? true : null,
'label' => \__( 'Amazon Simple Storage Service (S3)', 'w3-total-cache' ),
'optgroup' => $optgroup_push,
);
$engine_values['s3_compatible'] = array(
'disabled' => ! Util_Installed::curl() ? true : null,
'label' => \__( 'Amazon Simple Storage Service (S3) Compatible', 'w3-total-cache' ),
'optgroup' => $optgroup_push,
);
$engine_values['google_drive'] = array(
'label' => \__( 'Google Drive', 'w3-total-cache' ),
'optgroup' => $optgroup_push,
);
$engine_values['azure'] = array(
'label' => \__( 'Microsoft Azure Storage', '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' ),
'optgroup' => $optgroup_push,
);
$engine_values['rscf'] = array(
'disabled' => ! Util_Installed::curl() ? true : null,
'label' => \__( 'Rackspace Cloud Files', 'w3-total-cache' ),
'optgroup' => $optgroup_push,
);
$engine_values['ftp'] = array(
'disabled' => ! Util_Installed::ftp() ? true : null,
'label' => \__( 'Self-hosted / File Transfer Protocol Upload', 'w3-total-cache' ),
'optgroup' => $optgroup_push,
);
$cdn_enabled = $config->get_boolean( 'cdn.enabled' );
$cdn_engine = $config->get_string( 'cdn.engine' );
include W3TC_DIR . '/Cdn_GeneralPage_View.php';
}
/**
* Filters the attachment URL for the WordPress admin area based on CDN settings.
*
* @param string $url The URL of the attachment.
*
* @return string The filtered URL of the attachment.
*/
public function wp_get_attachment_url( $url ) {
if ( defined( 'WP_ADMIN' ) ) {
$url = trim( $url );
if ( ! empty( $url ) ) {
$parsed = \wp_parse_url( $url );
$uri = ( isset( $parsed['path'] ) ? $parsed['path'] : '/' ) .
( isset( $parsed['query'] ) ? '?' . $parsed['query'] : '' );
$wp_upload_dir = \wp_upload_dir();
$upload_base_url = $wp_upload_dir['baseurl'];
if ( \substr( $url, 0, strlen( $upload_base_url ) ) === $upload_base_url ) {
$common = Dispatcher::component( 'Cdn_Core' );
$new_url = $common->url_to_cdn_url( $url, $uri );
if ( ! is_null( $new_url ) ) {
$url = $new_url;
}
}
}
}
return $url;
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* File: Cdn_RackSpaceCdn_AdminActions.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cdn_RackSpaceCdn_AdminActions
*/
class Cdn_RackSpaceCdn_AdminActions {
/**
* Reloads the Rackspace CDN domains (CNAMEs).
*
* This method retrieves the latest Rackspace CDN domains (CNAMEs) from the CDN service and updates the configuration.
* If the domains cannot be retrieved due to an exception, an error message is displayed and the process is halted.
* On successful retrieval, the domains are saved to the configuration, and a success message is displayed.
*
* @return void
*/
public function w3tc_cdn_rackspace_cdn_domains_reload() {
$c = Dispatcher::config();
$core = Dispatcher::component( 'Cdn_Core' );
$cdn = $core->get_cdn();
try {
// try to obtain CNAMEs.
$domains = $cdn->service_domains_get();
} catch ( \Exception $ex ) {
Util_Admin::redirect_with_custom_messages2(
array(
'errors' => array( 'Failed to obtain <acronym title="Canonical Name">CNAME</acronym>s: ' . $ex->getMessage() ),
),
true
);
return;
}
$c->set( 'cdn.rackspace_cdn.domains', $domains );
$c->save();
Util_Admin::redirect_with_custom_messages2(
array(
'notes' => array( 'CNAMEs are reloaded successfully' ),
),
true
);
}
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* File: Cdn_RackSpaceCdn_Page.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cdn_RackSpaceCdn_Page
*/
class Cdn_RackSpaceCdn_Page {
/**
* Adds Rackspace CDN-specific admin action handlers.
*
* This method integrates the Rackspace CDN admin action handlers into the W3 Total Cache admin framework,
* enabling the handling of actions specific to the Rackspace CDN configuration.
*
* @param array $handlers An array of existing admin action handlers.
*
* @return array The updated array of admin action handlers, including the Rackspace CDN handler.
*/
public static function w3tc_admin_actions( $handlers ) {
$handlers['cdn_rackspace_cdn'] = 'Cdn_RackSpaceCdn_AdminActions';
return $handlers;
}
/**
* Enqueues Rackspace CDN-specific JavaScript for the admin area.
*
* This method loads the JavaScript file necessary for the Rackspace CDN admin page in W3 Total Cache.
* It ensures the script is added to the admin area when needed.
*
* @return void
*/
public static function admin_print_scripts_w3tc_cdn() {
wp_enqueue_script( 'w3tc_cdn_rackspace', plugins_url( 'Cdn_RackSpaceCdn_Page_View.js', W3TC_FILE ), array( 'jquery' ), '1.0', false );
}
/**
* Renders the Rackspace CDN configuration box area in the settings.
*
* This method outputs the HTML for the Rackspace CDN configuration box in the W3 Total Cache settings.
* It retrieves the necessary configuration values, checks authorization, and prepares access URLs
* before including the view file.
*
* @return void
*/
public static function w3tc_settings_cdn_boxarea_configuration() {
$config = Dispatcher::config();
$api_key = $config->get_string( 'cdn.rackspace_cdn.api_key' );
$authorized = ! empty( $api_key );
$access_url_full = '';
if ( $authorized ) {
$p = $config->get_string( 'cdn.rackspace_cdn.service.protocol' );
$access_url_full = ( 'https' === $p ? 'https://' : 'http://' ) . $config->get_string( 'cdn.rackspace_cdn.service.access_url' );
}
include W3TC_DIR . '/Cdn_RackSpaceCdn_Page_View.php';
}
}

View File

@@ -0,0 +1,158 @@
jQuery(function($) {
function w3tc_rackspace_resize(o) {
o.resize();
}
function w3tc_rackspace_created(o) {
w3tc_rackspace_resize(o);
w3tc_rackspace_check_service_state();
}
function w3tc_rackspace_check_service_state() {
var service_id = jQuery('input[name="service_id"]').val();
var access_token = jQuery('input[name="access_token"]').val();
var access_region_descriptor = jQuery('input[name="access_region_descriptor"]').val();
jQuery.post(ajaxurl,
{
'action': 'w3tc_ajax',
'_wpnonce': w3tc_nonce,
'service_id': service_id,
'access_token': access_token,
'access_region_descriptor': access_region_descriptor,
'w3tc_action': 'cdn_rackspace_service_get_state'
}, function(data) {
var state = 'unknown';
if (data && data['status'])
status = data['status'];
jQuery('.w3tc_rackspace_created_status').html(status);
if (status == 'deployed')
w3tc_rackspace_service_created_done(data);
else
setTimeout(w3tc_rackspace_check_service_state, 5000);
}, 'json'
).fail(function() {
jQuery('.w3tc_rackspace_created_state').html('Failed to obtain state');
setTimeout(w3tc_rackspace_check_service_state, 5000);
});
}
function w3tc_rackspace_service_created_done(data) {
jQuery('.w3tc_rackspace_cname').html(data['cname']);
jQuery('.w3tc_rackspace_access_url').html(data['access_url']);
jQuery('.w3tc_rackspace_created_in_progress').css('display', 'none');
jQuery('.w3tc_rackspace_created_done').css('display', '');
w3tc_rackspace_resize(W3tc_Lightbox);
}
$('body')
/**
* Authorize popup
*/
.on('click', '.w3tc_cdn_rackspace_authorize', function() {
W3tc_Lightbox.open({
id:'w3tc-overlay',
close: '',
width: 800,
height: 300,
url: ajaxurl + '?action=w3tc_ajax&_wpnonce=' + w3tc_nonce +
'&w3tc_action=cdn_rackspace_intro',
callback: w3tc_rackspace_resize
});
})
.on('click', '.w3tc_popup_submit', function() {
var url = ajaxurl + '?action=w3tc_ajax&_wpnonce=' + w3tc_nonce;
W3tc_Lightbox.load_form(url, '.w3tc_cdn_rackspace_form',
w3tc_rackspace_resize);
})
.on('click', '.w3tc_cdn_rackspace_service_create_done', function() {
var url = ajaxurl + '?action=w3tc_ajax&_wpnonce=' + w3tc_nonce +
'&w3tc_action=cdn_rackspace_service_create_done';
W3tc_Lightbox.load_form(url, '.w3tc_cdn_rackspace_form',
w3tc_rackspace_created);
})
.on('click', '.w3tc_cdn_rackspace_protocol', function() {
var protocol = '';
$('body').find('.w3tc_cdn_rackspace_protocol').each(function(i) {
if (!jQuery(this).prop('checked'))
return;
protocol = $(this).val();
});
//alert('ha ' + protocol);
$('.w3tc_cdn_rackspace_cname_http').css('display',
(protocol == 'http' ? '' : 'none'));
$('.w3tc_cdn_rackspace_cname_https').css('display',
(protocol == 'https' ? '' : 'none'));
})
/**
* CNAMEs popup
*/
.on('click', '.w3tc_cdn_rackspace_configure_domains', function() {
W3tc_Lightbox.open({
id:'w3tc-overlay',
close: '',
width: 1000,
height: 400,
url: ajaxurl + '?action=w3tc_ajax&_wpnonce=' + w3tc_nonce +
'&w3tc_action=cdn_rackspace_configure_domains',
callback: function(o) {
w3tc_rackspace_resize(o);
w3tc_cdn_cnames_assign();
}
});
})
.on('click', '.w3tc_cdn_rackspace_configure_domains_done', function() {
var url = ajaxurl + '?action=w3tc_ajax&_wpnonce=' + w3tc_nonce +
'&w3tc_action=cdn_rackspace_configure_domains_done';
var v = $('.w3tc_cdn_rackspace_form').find('input').each(function(i) {
var name = $(this).attr('name');
if (name)
url += '&' + encodeURIComponent(name) + '=' +
encodeURIComponent($(this).val());
});
W3tc_Lightbox.load(url, function(o) {
w3tc_rackspace_resize(o);
w3tc_cdn_cnames_assign();
});
})
.on('size_change', '#cdn_cname_add', function() {
w3tc_rackspace_resize(W3tc_Lightbox);
})
});

View File

@@ -0,0 +1,177 @@
<?php
/**
* File: Cdn_RackSpaceCdn_Page_View.php
*
* @package W3TC
*/
namespace W3TC;
if ( ! defined( 'W3TC' ) ) {
die();
}
?>
<tr>
<th style="width: 300px;"><label><?php esc_html_e( 'Authorize:', 'w3-total-cache' ); ?></label></th>
<td>
<?php if ( $authorized ) : ?>
<input class="w3tc_cdn_rackspace_authorize button" type="button"
value="<?php esc_attr_e( 'Reauthorize', 'w3-total-cache' ); ?>" />
<?php else : ?>
<input class="w3tc_cdn_rackspace_authorize button" type="button"
value="<?php esc_attr_e( 'Authorize', 'w3-total-cache' ); ?>" />
<?php endif; ?>
</td>
</tr>
<?php if ( $authorized ) : ?>
<tr>
<th><?php esc_html_e( 'Username:', 'w3-total-cache' ); ?></th>
<td class="w3tc_config_value_text">
<?php echo esc_html( $config->get_string( 'cdn.rackspace_cdn.user_name' ) ); ?>
</td>
</tr>
<tr>
<th><?php esc_html_e( 'Region:', 'w3-total-cache' ); ?></th>
<td class="w3tc_config_value_text">
<?php echo esc_html( $config->get_string( 'cdn.rackspace_cdn.region' ) ); ?>
</td>
</tr>
<tr>
<th><?php esc_html_e( 'Service:', 'w3-total-cache' ); ?></th>
<td class="w3tc_config_value_text">
<?php echo esc_html( $config->get_string( 'cdn.rackspace_cdn.service.name' ) ); ?>
</td>
</tr>
<tr>
<th>
<label>
<?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.
__(
'%1$sCDN%2$s host (%3$sCNAME%4$s target):',
'w3-total-cache'
),
'<acronym title="' . esc_attr__( 'Content Delivery Network', 'w3-total-cache' ) . '">',
'</acronym>',
'<acronym title="' . esc_attr__( 'Canonical Name', 'w3-total-cache' ) . '">',
'</acronym>'
),
array(
'acronym' => array(
'title' => array(),
),
)
);
?>
</label>
</th>
<td class="w3tc_config_value_text">
<?php echo esc_html( $access_url_full ); ?>
</td>
</tr>
<?php if ( $config->get_string( 'cdn.rackspace_cdn.service.protocol' ) === 'http' ) : ?>
<tr>
<th><?php esc_html_e( 'Replace site\'s hostname with:', 'w3-total-cache' ); ?></th>
<td>
<?php
$cnames = $config->get_array( 'cdn.rackspace_cdn.domains' );
include W3TC_INC_DIR . '/options/cdn/common/cnames-readonly.php';
?>
<input class="w3tc_cdn_rackspace_configure_domains button" type="button"
value="<?php esc_attr_e( 'Configure CNAMEs', 'w3-total-cache' ); ?>" />
<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.
__(
'Enter hostname mapped to %1$sCDN%2$s host, this value will replace your site\'s hostname in the %3$sHTML%4$s.',
'w3-total-cache'
),
'<acronym title="' . esc_attr__( 'Content Delivery Network', 'w3-total-cache' ) . '">',
'</acronym>',
'<acronym title="' . esc_attr__( 'Hypertext Markup Language', 'w3-total-cache' ) . '">',
'</acronym>'
),
array(
'acronym' => array(
'title' => array(),
),
)
);
?>
</p>
</td>
</tr>
<?php else : ?>
<tr>
<th><?php esc_html_e( 'Replace site\'s hostname with:', 'w3-total-cache' ); ?></th>
<td>
<?php
$cnames = $config->get_array( 'cdn.rackspace_cdn.domains' );
include W3TC_INC_DIR . '/options/cdn/common/cnames-readonly.php';
?>
<input name="w3tc_cdn_rackspace_cdn_domains_reload"
class="w3tc-button-save button" type="submit"
value="
<?php
echo wp_kses(
sprintf(
// translators: 1 opening HTML acronym tag, 2 closing HTML acronym tag.
__(
'Reload %1$sCNAME%2$ss from RackSpace',
'w3-total-cache'
),
'<acronym title="' . esc_attr__( 'Canonical Name', 'w3-total-cache' ) . '">',
'</acronym>'
),
array(
'acronym' => array(
'title' => array(),
),
)
);
?>
" />
<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.
__(
'Hostname(s) mapped to %1$sCDN%2$s host, this value will replace your site\'s hostname in the %3$sHTML%4$s. You can manage them from RackSpace management console and load here afterwards.',
'w3-total-cache'
),
'<acronym title="' . esc_attr__( 'Content Delivery Network', 'w3-total-cache' ) . '">',
'</acronym>',
'<acronym title="' . esc_attr__( 'Hypertext Markup Language', 'w3-total-cache' ) . '">',
'</acronym>'
),
array(
'acronym' => array(
'title' => array(),
),
)
);
?>
</p>
</td>
</tr>
<?php endif; ?>
<tr>
<th colspan="2">
<input id="cdn_test"
class="button {type: 'rackspace_cdn', nonce: '<?php echo esc_attr( wp_create_nonce( 'w3tc' ) ); ?>'}"
type="button"
value="<?php esc_attr_e( 'Test', 'w3-total-cache' ); ?>" />
<span id="cdn_test_status" class="w3tc-status w3tc-process"></span>
</th>
</tr>
<?php endif; ?>

View File

@@ -0,0 +1,651 @@
<?php
/**
* File: Cdn_RackSpaceCdn_Popup.php
*
* @package W3TC
*/
namespace W3TC;
/**
* Class Cdn_RackSpaceCdn_Popup
*
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
*/
class Cdn_RackSpaceCdn_Popup {
/**
* Handles AJAX registration for Rackspace CDN popup actions.
*
* Registers multiple AJAX handlers for Rackspace CDN popup interactions
* using WordPress's `add_action()` for the corresponding AJAX hooks.
*
* @return void
*/
public static function w3tc_ajax() {
$o = new Cdn_RackSpaceCdn_Popup();
add_action( 'w3tc_ajax_cdn_rackspace_intro', array( $o, 'w3tc_ajax_cdn_rackspace_intro' ) );
add_action( 'w3tc_ajax_cdn_rackspace_intro_done', array( $o, 'w3tc_ajax_cdn_rackspace_intro_done' ) );
add_action( 'w3tc_ajax_cdn_rackspace_regions_done', array( $o, 'w3tc_ajax_cdn_rackspace_regions_done' ) );
add_action( 'w3tc_ajax_cdn_rackspace_services_done', array( $o, 'w3tc_ajax_cdn_rackspace_services_done' ) );
add_action( 'w3tc_ajax_cdn_rackspace_service_create_done', array( $o, 'w3tc_ajax_cdn_rackspace_service_create_done' ) );
add_action( 'w3tc_ajax_cdn_rackspace_service_get_state', array( $o, 'w3tc_ajax_cdn_rackspace_service_get_state' ) );
add_action( 'w3tc_ajax_cdn_rackspace_service_created_done', array( $o, 'w3tc_ajax_cdn_rackspace_service_created_done' ) );
add_action( 'w3tc_ajax_cdn_rackspace_service_actualize_done', array( $o, 'w3tc_ajax_cdn_rackspace_service_actualize_done' ) );
add_action( 'w3tc_ajax_cdn_rackspace_configure_domains', array( $o, 'w3tc_ajax_cdn_rackspace_configure_domains' ) );
add_action( 'w3tc_ajax_cdn_rackspace_configure_domains_done', array( $o, 'w3tc_ajax_cdn_rackspace_configure_domains_done' ) );
}
/**
* Handles the introduction popup view for Rackspace CDN.
*
* Fetches Rackspace CDN user credentials from the configuration
* and renders the introductory view.
*
* @return void
*/
public function w3tc_ajax_cdn_rackspace_intro() {
$c = Dispatcher::config();
$details = array(
'user_name' => $c->get_string( 'cdn.rackspace_cdn.user_name' ),
'api_key' => $c->get_string( 'cdn.rackspace_cdn.api_key' ),
);
include W3TC_DIR . '/Cdn_RackSpaceCdn_Popup_View_Intro.php';
exit();
}
/**
* Completes the introduction step and renders the Rackspace regions view.
*
* Processes the user credentials provided via AJAX and fetches region
* data for Rackspace CDN.
*
* @return void
*/
public function w3tc_ajax_cdn_rackspace_intro_done() {
$this->_render_cdn_rackspace_regions(
array(
'user_name' => Util_Request::get_string( 'user_name' ),
'api_key' => Util_Request::get_string( 'api_key' ),
)
);
}
/**
* Renders the list of available regions for Rackspace CDN.
*
* Authenticates the user with Rackspace API and fetches regions
* along with the associated services.
*
* @param array $details Array containing user credentials and other necessary details.
*
* @return void
*/
private function _render_cdn_rackspace_regions( $details ) {
$user_name = $details['user_name'];
$api_key = $details['api_key'];
try {
$r = Cdn_RackSpace_Api_Tokens::authenticate( $user_name, $api_key );
} catch ( \Exception $ex ) {
$details = array(
'user_name' => $user_name,
'api_key' => $api_key,
'error_message' => 'Can\'t authenticate: ' . $ex->getMessage(),
);
include W3TC_DIR . '/Cdn_RackSpaceCdn_Popup_View_Intro.php';
exit();
}
$r['regions'] = Cdn_RackSpace_Api_Tokens::cdn_services_by_region( $r['services'] );
$details['access_token'] = $r['access_token'];
$details['region_descriptors'] = $r['regions'];
// avoid fights with quotes, magic_quotes may break randomly.
$details['region_descriptors_serialized'] = strtr( wp_json_encode( $r['regions'] ), '"\\', '!^' );
include W3TC_DIR . '/Cdn_RackSpaceCdn_Popup_View_Regions.php';
exit();
}
/**
* Processes the selected region and renders available services.
*
* Validates the selected region and fetches services for the specified
* region using the Rackspace API.
*
* @return void
*/
public function w3tc_ajax_cdn_rackspace_regions_done() {
$user_name = Util_Request::get_string( 'user_name' );
$api_key = Util_Request::get_string( 'api_key' );
$access_token = Util_Request::get_string( 'access_token' );
$region = Util_Request::get_string( 'region' );
$region_descriptors = json_decode(
strtr( Util_Request::get_string( 'region_descriptors' ), '!^', '"\\' ),
true
);
if ( ! isset( $region_descriptors[ $region ] ) ) {
$this->_render_cdn_rackspace_regions(
array(
'user_name' => $user_name,
'api_key' => $api_key,
'error_message' => 'Please select region ' . $region,
)
);
}
$api = new Cdn_RackSpace_Api_Cdn(
array(
'access_token' => $access_token,
'access_region_descriptor' => $region_descriptors[ $region ],
'new_access_required' => '',
)
);
try {
$services = $api->services();
} catch ( \Exception $ex ) {
$details = array(
'user_name' => $user_name,
'api_key' => $api_key,
'error_message' => $ex->getMessage(),
);
include W3TC_DIR . '/Cdn_RackSpaceCdn_Popup_View_Intro.php';
exit();
}
$details = array(
'user_name' => $user_name,
'api_key' => $api_key,
'access_token' => $access_token,
'access_region_descriptor_serialized' => strtr( wp_json_encode( $region_descriptors[ $region ] ), '"\\', '!^' ),
'region' => $region,
// avoid fights with quotes, magic_quotes may break randomly.
'services' => $services,
);
include W3TC_DIR . '/Cdn_RackSpaceCdn_Popup_View_Services.php';
exit();
}
/**
* Handles the completion of service selection for Rackspace CDN.
*
* Processes the selected service or renders the service creation view
* if no service is selected.
*
* @return void
*/
public function w3tc_ajax_cdn_rackspace_services_done() {
$user_name = Util_Request::get_string( 'user_name' );
$api_key = Util_Request::get_string( 'api_key' );
$access_token = Util_Request::get_string( 'access_token' );
$access_region_descriptor = json_decode( strtr( Util_Request::get_string( 'access_region_descriptor' ), '!^', '"\\' ), true );
$region = Util_Request::get_string( 'region' );
$service = Util_Request::get( 'service' );
if ( ! empty( $service ) ) {
$this->_render_service_actualize(
array(
'user_name' => $user_name,
'api_key' => $api_key,
'access_token' => $access_token,
'access_region_descriptor_serialized' => strtr( wp_json_encode( $access_region_descriptor ), '"\\', '!^' ),
'region' => $region,
'service_id' => $service,
)
);
exit();
}
$home_url = get_home_url();
$parsed = wp_parse_url( $home_url );
$is_https = ( 'https' === $parsed['scheme'] );
$details = array(
'user_name' => $user_name,
'api_key' => $api_key,
'access_token' => $access_token,
'access_region_descriptor_serialized' => strtr( wp_json_encode( $access_region_descriptor ), '"\\', '!^' ),
'region' => $region,
'name' => '',
'protocol' => ( $is_https ? 'https' : 'http' ),
'cname_http' => '',
'cname_http_style' => ( $is_https ? 'display: none' : '' ),
'cname_https_prefix' => '',
'cname_https_style' => ( $is_https ? '' : 'display: none' ),
'origin' => Util_Environment::home_url_host(),
);
include W3TC_DIR . '/Cdn_RackSpaceCdn_Popup_View_Service_Create.php';
exit();
}
/**
* Creates a new service in Rackspace CDN.
*
* Processes the details for service creation including domain and origin settings,
* and sends a request to the Rackspace API to create the service.
*
* @return void
*/
public function w3tc_ajax_cdn_rackspace_service_create_done() {
$user_name = Util_Request::get_string( 'user_name' );
$api_key = Util_Request::get_string( 'api_key' );
$access_token = Util_Request::get_string( 'access_token' );
$access_region_descriptor = json_decode( strtr( Util_Request::get_string( 'access_region_descriptor' ), '!^', '"\\' ), true );
$region = Util_Request::get_string( 'region' );
$name = Util_Request::get_string( 'name' );
$protocol = Util_Request::get_string( 'protocol' );
$cname_http = Util_Request::get_string( 'cname_http' );
$cname_https_prefix = Util_Request::get_string( 'cname_https_prefix' );
$is_https = ( 'https' === $protocol );
$cname = ( $is_https ? $cname_https_prefix : $cname_http );
$api = new Cdn_RackSpace_Api_Cdn(
array(
'access_token' => $access_token,
'access_region_descriptor' => $access_region_descriptor,
'new_access_required' => '',
)
);
$service_id = null;
$access_url = null;
try {
$domain = array(
'domain' => $cname,
'protocol' => ( $is_https ? 'https' : 'http' ),
);
if ( $is_https ) {
$domain['certificate'] = 'shared';
}
$service_id = $api->service_create(
array(
'name' => $name,
'domains' => array( $domain ),
'origins' => array(
array(
'origin' => Util_Environment::home_url_host(),
'port' => ( $is_https ? 443 : 80 ),
'ssl' => $is_https,
'hostheadertype' => 'origin',
'rules' => array(),
),
),
'caching' => array(
array(
'name' => 'default',
'ttl' => 86400,
),
),
)
);
} catch ( \Exception $ex ) {
$details = array(
'user_name' => $user_name,
'api_key' => $api_key,
'access_token' => $access_token,
'access_region_descriptor_serialized' => strtr( wp_json_encode( $access_region_descriptor ), '"\\', '!^' ),
'region' => $region,
'name' => $name,
'protocol' => ( $is_https ? 'https' : 'http' ),
'cname_http' => $cname_http,
'cname_http_style' => ( $is_https ? 'display: none' : '' ),
'cname_https_prefix' => $cname_https_prefix,
'cname_https_style' => ( $is_https ? '' : 'display: none' ),
'origin' => Util_Environment::home_url_host(),
'error_message' => $ex->getMessage(),
);
include W3TC_DIR . '/Cdn_RackSpaceCdn_Popup_View_Service_Create.php';
exit();
}
$details = array(
'user_name' => $user_name,
'api_key' => $api_key,
'access_token' => $access_token,
'access_region_descriptor_serialized' => strtr( wp_json_encode( $access_region_descriptor ), '"\\', '!^' ),
'region' => $region,
'name' => $name,
'is_https' => $is_https,
'cname' => $cname,
'service_id' => $service_id,
);
include W3TC_DIR . '/Cdn_RackSpaceCdn_Popup_View_Service_Created.php';
}
/**
* Handles AJAX request to retrieve the state of a Rackspace service.
*
* @return void
*/
public function w3tc_ajax_cdn_rackspace_service_get_state() {
$access_token = Util_Request::get_string( 'access_token' );
$access_region_descriptor = json_decode( strtr( Util_Request::get_string( 'access_region_descriptor' ), '!^', '"\\' ), true );
$service_id = Util_Request::get_string( 'service_id' );
$api = new Cdn_RackSpace_Api_Cdn(
array(
'access_token' => $access_token,
'access_region_descriptor' => $access_region_descriptor,
'new_access_required' => '',
)
);
$service = $api->service_get( $service_id );
$response = array( 'status' => 'Unknown' );
if ( isset( $service['status'] ) ) {
$response['status'] = $service['status'];
}
if ( isset( $service['links_by_rel']['access_url'] ) ) {
$response['access_url'] = $service['links_by_rel']['access_url']['href'];
}
if ( isset( $service['domains'] ) ) {
$response['cname'] = $service['domains'][0]['domain'];
}
// decode to friendly name.
if ( 'create_in_progress' === $response['status'] ) {
$response['status'] = 'Creation in progress...';
}
echo esc_html( wp_json_encode( $response ) );
}
/**
* Handles the completion of Rackspace service creation.
*
* @return void
*/
public function w3tc_ajax_cdn_rackspace_service_created_done() {
$this->_save_config();
}
/**
* Renders the form for updating a Rackspace service with the provided details.
*
* @param array $details Array containing the service details.
*
* @return void
*/
private function _render_service_actualize( $details ) {
$access_region_descriptor = json_decode( strtr( $details['access_region_descriptor_serialized'], '!^', '"\\' ), true );
$api = new Cdn_RackSpace_Api_Cdn(
array(
'access_token' => $details['access_token'],
'access_region_descriptor' => $access_region_descriptor,
'new_access_required' => '',
)
);
$service = null;
try {
$service = $api->service_get( $details['service_id'] );
} catch ( \Exception $ex ) {
$details['error_message'] = $ex->getMessage();
include W3TC_DIR . '/Cdn_RackSpaceCdn_Popup_View_Intro.php';
exit();
}
$origin = '';
$protocol = 'http';
if ( isset( $service['origins'] ) && $service['origins'][0]['origin'] ) {
$protocol = $service['origins'][0]['ssl'] ? 'https' : 'http';
$origin = $service['origins'][0]['origin'];
}
$details['name'] = $service['name'];
$details['protocol'] = $protocol;
$details['origin'] = array(
'current' => $origin,
'new' => Util_Environment::home_url_host(),
);
include W3TC_DIR . '/Cdn_RackSpaceCdn_Popup_View_Service_Actualize.php';
exit();
}
/**
* Handles AJAX request to finalize Rackspace service updates.
*
* @return void
*/
public function w3tc_ajax_cdn_rackspace_service_actualize_done() {
$user_name = Util_Request::get_string( 'user_name' );
$api_key = Util_Request::get_string( 'api_key' );
$access_token = Util_Request::get_string( 'access_token' );
$access_region_descriptor = json_decode( strtr( Util_Request::get_string( 'access_region_descriptor' ), '!^', '"\\' ), true );
$region = Util_Request::get_string( 'region' );
$service_id = Util_Request::get_string( 'service_id' );
$api = new Cdn_RackSpace_Api_Cdn(
array(
'access_token' => $access_token,
'access_region_descriptor' => $access_region_descriptor,
'new_access_required' => '',
)
);
try {
$service = $api->service_get( $service_id );
$is_https = false;
$origin = '';
if ( isset( $service['origins'] ) && $service['origins'][0]['ssl'] ) {
$is_https = $service['origins'][0]['ssl'];
$origin = $service['origins'][0]['origin'];
}
$new_origin = Util_Environment::home_url_host();
if ( $origin !== $new_origin ) {
$api->service_set(
$service_id,
array(
array(
'op' => 'replace',
'path' => '/origins',
'value' => array(
array(
'origin' => $new_origin,
'port' => ( $is_https ? 443 : 80 ),
'ssl' => $is_https,
'hostheadertype' => 'origin',
'rules' => array(),
),
),
),
)
);
}
} catch ( \Exception $ex ) {
$details = array(
'user_name' => $user_name,
'api_key' => $api_key,
'error_message' => $ex->getMessage(),
);
include W3TC_DIR . '/Cdn_RackSpaceCdn_Popup_View_Intro.php';
exit();
}
$this->_save_config();
}
/**
* Saves Rackspace CDN configuration to the plugin settings.
*
* @return void
*/
private function _save_config() {
$user_name = Util_Request::get_string( 'user_name' );
$api_key = Util_Request::get_string( 'api_key' );
$access_token = Util_Request::get_string( 'access_token' );
$access_region_descriptor = json_decode( strtr( Util_Request::get_string( 'access_region_descriptor' ), '!^', '"\\' ), true );
$region = Util_Request::get_string( 'region' );
$service_id = Util_Request::get_string( 'service_id' );
$api = new Cdn_RackSpace_Api_Cdn(
array(
'access_token' => $access_token,
'access_region_descriptor' => $access_region_descriptor,
'new_access_required' => '',
)
);
$service = $api->service_get( $service_id );
$access_url = $service['links_by_rel']['access_url']['href'];
$protocol = 'http';
$domain = '';
if ( isset( $service['domains'] ) && $service['domains'][0]['protocol'] ) {
$protocol = $service['domains'][0]['protocol'];
$domain = $service['domains'][0]['domain'];
}
$c = Dispatcher::config();
$c->set( 'cdn.rackspace_cdn.user_name', $user_name );
$c->set( 'cdn.rackspace_cdn.api_key', $api_key );
$c->set( 'cdn.rackspace_cdn.region', $region );
$c->set( 'cdn.rackspace_cdn.service.name', $service['name'] );
$c->set( 'cdn.rackspace_cdn.service.id', $service_id );
$c->set( 'cdn.rackspace_cdn.service.access_url', $access_url );
$c->set( 'cdn.rackspace_cdn.service.protocol', $protocol );
if ( 'https' !== $protocol ) {
$c->set( 'cdn.rackspace_cdn.domains', array( $domain ) );
}
$c->save();
// reset calculated state.
$state = Dispatcher::config_state();
$state->set( 'cdn.rackspace_cdn.access_state', '' );
$state->save();
$postfix = Util_Admin::custom_message_id(
array(),
array( 'cdn_configuration_saved' => 'CDN credentials are saved successfully' )
);
echo esc_url( 'Location admin.php?page=w3tc_cdn&' . $postfix );
exit();
}
/**
* Handles AJAX request to render the form for configuring domains.
*
* @return void
*/
public function w3tc_ajax_cdn_rackspace_configure_domains() {
$this->render_configure_domains_form();
exit();
}
/**
* Handles AJAX request to save domain configuration changes.
*
* @return void
*/
public function w3tc_ajax_cdn_rackspace_configure_domains_done() {
$details = array(
'cnames' => Util_Request::get_array( 'cdn_cnames' ),
);
$core = Dispatcher::component( 'Cdn_Core' );
$cdn = $core->get_cdn();
try {
// try to obtain CNAMEs.
$cdn->service_domains_set( $details['cnames'] );
$c = Dispatcher::config();
$c->set( 'cdn.rackspace_cdn.domains', $details['cnames'] );
$c->save();
$postfix = Util_Admin::custom_message_id(
array(),
array( 'cdn_cnames_saved' => 'CNAMEs are saved successfully' )
);
echo esc_url( 'Location admin.php?page=w3tc_cdn&' . $postfix );
exit();
} catch ( \Exception $ex ) {
$details['error_message'] = $ex->getMessage();
}
$this->render_configure_domains_form( $details );
exit();
}
/**
* Renders the form for configuring domains.
*
* @param array $details Optional. Array of details, including domain configurations. Defaults to an empty array.
*
* @return void
*/
private function render_configure_domains_form( $details = array() ) {
if ( isset( $details['cnames'] ) ) {
$cnames = $details['cnames'];
} else {
$core = Dispatcher::component( 'Cdn_Core' );
$cdn = $core->get_cdn();
try {
// try to obtain CNAMEs.
$cnames = $cdn->service_domains_get();
} catch ( \Exception $ex ) {
$details['error_message'] = $ex->getMessage();
$cnames = array();
}
}
include W3TC_DIR . '/Cdn_RackSpaceCdn_Popup_View_ConfigureDomains.php';
}
/**
* Renders the value change summary for a specific service field.
*
* @param array $details Array containing the service details.
* @param string $field Name of the field to render value changes for.
*
* @return void
*/
private function render_service_value_change( $details, $field ) {
Util_Ui::hidden( 'w3tc-rackspace-value-' . $field, $field, $details[ $field ]['new'] );
if ( ! isset( $details[ $field ]['current'] ) || $details[ $field ]['current'] === $details[ $field ]['new'] ) {
echo esc_html( $details[ $field ]['new'] );
} else {
echo wp_kses(
sprintf(
// translators: 1 opening HTML strong tag, 2 current setting value, 3 closing HTML strong tag followed by HTML line break,
// translators: 4 opening HTML strong tag, 5 new setting value, 6 closing HTML strong tag followed by HTML line break.
__(
'currently set to %1$s%2$s%3$s will be changed to %4$s%5$s%6$s',
'w3-total-cache'
),
'<strong>',
empty( $details[ $field ]['current'] ) ? '<empty>' : $details[ $field ]['current'],
'</strong><br />',
'<strong>',
$details[ $field ]['new'],
'</strong><br />'
),
array(
'strong' => array(),
'empty' => array(),
'br' => array(),
)
);
}
}
}

View File

@@ -0,0 +1,76 @@
<?php
/**
* File: Cdn_RackSpaceCdn_Popup_View_ConfigureDomains.php
*
* @package W3TC
*/
namespace W3TC;
if ( ! defined( 'W3TC' ) ) {
die();
}
?>
<form action="admin.php?page=w3tc_cdn" method="post" style="padding: 20px" class="w3tc_cdn_rackspace_form">
<?php
if ( ! empty( $details['error_message'] ) ) {
echo '<div class="error">' . esc_html( $details['error_message'] ) . '</div>';
}
?>
<div class="metabox-holder">
<?php
Util_Ui::postbox_header(
wp_kses(
sprintf(
// translators: 1 opening HTML acronym tag, 2 closing HTML acronym tag.
__(
'%1$sCNAME%2$ss to use',
'w3-total-cache'
),
'<acronym title="' . esc_attr__( 'Canonical Name', 'w3-total-cache' ) . '">',
'</acronym>'
),
array(
'acronym' => array(
'title' => array(),
),
)
)
);
?>
<?php
$cname_class = 'w3tc-ignore-change';
require W3TC_INC_DIR . '/options/cdn/common/cnames.php';
?>
<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.
__(
'Enter hostname mapped to %1$sCDN%2$s host, this value will replace your site\'s hostname in the %3$sHTML%4$s.',
'w3-total-cache'
),
'<acronym title="' . esc_attr__( 'Content Delivery Network', 'w3-total-cache' ) . '">',
'</acronym>',
'<acronym title="' . esc_attr__( 'Hypertext Markup Language', 'w3-total-cache' ) . '">',
'</acronym>'
),
array(
'acronym' => array(
'title' => array(),
),
)
);
?>
</p>
<p class="submit">
<input type="button"
class="w3tc_cdn_rackspace_configure_domains_done w3tc-button-save button-primary"
value="<?php esc_attr_e( 'Apply', 'w3-total-cache' ); ?>" />
</p>
<?php Util_Ui::postbox_footer(); ?>
</div>
</form>

View File

@@ -0,0 +1,66 @@
<?php
/**
* File: Cdn_RackSpaceCdn_Popup_View_Intro.php
*
* @package W3TC
*/
namespace W3TC;
if ( ! defined( 'W3TC' ) ) {
die();
}
?>
<form class="w3tc_cdn_rackspace_form" method="post" style="padding: 20px">
<?php Util_Ui::hidden( 'w3tc-rackspace-action', 'w3tc_action', 'cdn_rackspace_intro_done' ); ?>
<?php
if ( isset( $details['error_message'] ) ) {
echo '<div class="error">' . esc_html( $details['error_message'] ) . '</div>';
}
?>
<div class="metabox-holder">
<?php Util_Ui::postbox_header( esc_html__( 'Your RackSpace API key', 'w3-total-cache' ) ); ?>
<table class="form-table">
<tr>
<th><?php esc_html_e( 'Username:', 'w3-total-cache' ); ?></td>
<td>
<input name="user_name" type="text" class="w3tc-ignore-change"
style="width: 100px" value="<?php echo esc_attr( $details['user_name'] ); ?>" />
</td>
</tr>
<tr>
<th>
<?php
echo wp_kses(
sprintf(
// translators: 1 opening HTML acronym tag, 2 closing HTML acronym tag.
__(
'%1$sAPI%2$s key:',
'w3-total-cache'
),
'<acronym title="' . esc_attr__( 'Application Programming Interface', 'w3-total-cache' ) . '">',
'</acronym>'
),
array(
'acronym' => array(
'title' => array(),
),
)
);
?>
</th>
<td>
<input name="api_key" type="text" class="w3tc-ignore-change"
style="width: 550px" value="<?php echo esc_attr( $details['api_key'] ); ?>" />
</td>
</tr>
</table>
<p class="submit">
<input type="button"
class="w3tc_popup_submit w3tc-button-save button-primary"
value="<?php esc_attr_e( 'Next', 'w3-total-cache' ); ?>" />
</p>
<?php Util_Ui::postbox_footer(); ?>
</div>
</form>

View File

@@ -0,0 +1,60 @@
<?php
/**
* File: Cdn_RackSpaceCdn_Popup_View_Regions.php
*
* @package W3TC
*/
namespace W3TC;
if ( ! defined( 'W3TC' ) ) {
die();
}
?>
<form action="admin.php?page=w3tc_cdn" method="post" style="padding: 20px" class="w3tc_cdn_rackspace_form">
<?php
Util_Ui::hidden( 'w3tc-rackspace-action', 'w3tc_action', 'cdn_rackspace_regions_done' );
Util_Ui::hidden( 'w3tc-rackspace-user-name', 'user_name', $details['user_name'] );
Util_Ui::hidden( 'w3tc-rackspace-api-key', 'api_key', $details['api_key'] );
Util_Ui::hidden( 'w3tc-rackspace-access-token', 'access_token', $details['access_token'] );
Util_Ui::hidden( 'w3tc-rackspace-region-descriptors', 'region_descriptors', $details['region_descriptors_serialized'] );
echo wp_kses(
Util_Ui::nonce_field( 'w3tc' ),
array(
'input' => array(
'type' => array(),
'name' => array(),
'value' => array(),
),
)
);
if ( isset( $details['error_message'] ) ) {
echo '<div class="error">' . esc_html( $details['error_message'] ) . '</div>';
}
?>
<div class="metabox-holder">
<?php Util_Ui::postbox_header( esc_html__( 'Select region', 'w3-total-cache' ) ); ?>
<table class="form-table">
<tr>
<th>Region:</td>
<td>
<?php foreach ( $details['region_descriptors'] as $region => $region_details ) : ?>
<label>
<input name="region" type="radio" class="w3tc-ignore-change"
value="<?php echo esc_attr( $region ); ?>" />
<?php echo esc_html( $region_details['name'] ); ?>
</label><br />
<?php endforeach; ?>
</td>
</tr>
</table>
<p class="submit">
<input type="button"
class="w3tc_popup_submit w3tc-button-save button-primary"
value="<?php esc_attr_e( 'Next', 'w3-total-cache' ); ?>" />
</p>
<?php Util_Ui::postbox_footer(); ?>
</div>
</form>

View File

@@ -0,0 +1,64 @@
<?php
/**
* File: Cdn_RackSpaceCdn_Popup_View_Service_Actualize.php
*
* @package W3TC
*/
namespace W3TC;
if ( ! defined( 'W3TC' ) ) {
die();
}
?>
<form action="admin.php?page=w3tc_cdn" method="post" style="padding: 20px" class="w3tc_cdn_rackspace_form">
<?php
Util_Ui::hidden( 'w3tc-rackspace-action', 'w3tc_action', 'cdn_rackspace_service_actualize_done' );
Util_Ui::hidden( 'w3tc-rackspace-user-name', 'user_name', $details['user_name'] );
Util_Ui::hidden( 'w3tc-rackspace-api-key', 'api_key', $details['api_key'] );
Util_Ui::hidden( 'w3tc-rackspace-access-token', 'access_token', $details['access_token'] );
Util_Ui::hidden( 'w3tc-rackspace-access-region-descriptor', 'access_region_descriptor', $details['access_region_descriptor_serialized'] );
Util_Ui::hidden( 'w3tc-rackspace-region', 'region', $details['region'] );
Util_Ui::hidden( 'w3tc-rackspace-service-id', 'service_id', $details['service_id'] );
echo wp_kses(
Util_Ui::nonce_field( 'w3tc' ),
array(
'input' => array(
'type' => array(),
'name' => array(),
'value' => array(),
),
)
);
if ( isset( $details['error_message'] ) ) {
echo '<div class="error">' . esc_html( $details['error_message'] ) . '</div>';
}
?>
<div class="metabox-holder">
<?php Util_Ui::postbox_header( esc_html__( 'Configure service', 'w3-total-cache' ) ); ?>
<table class="form-table">
<tr>
<th><?php esc_html_e( 'Name:', 'w3-total-cache' ); ?></th>
<td><?php echo esc_html( $details['name'] ); ?></td>
</tr>
<tr>
<th><?php esc_html_e( 'Origin host:', 'w3-total-cache' ); ?></th>
<td><?php $this->render_service_value_change( $details, 'origin' ); ?></td>
</tr>
<tr>
<th><?php esc_html_e( 'Origin protocol:', 'w3-total-cache' ); ?></th>
<td><?php echo esc_html( $details['protocol'] ); ?><br />
</td>
</tr>
</table>
<p class="submit">
<input type="button"
class="w3tc_popup_submit w3tc-button-save button-primary"
value="<?php esc_attr_e( 'Apply', 'w3-total-cache' ); ?>" />
</p>
<?php Util_Ui::postbox_footer(); ?>
</div>
</form>

View File

@@ -0,0 +1,147 @@
<?php
/**
* File: Cdn_RackSpaceCdn_Popup_View_Create.php
*
* @package W3TC
*/
namespace W3TC;
if ( ! defined( 'W3TC' ) ) {
die();
}
?>
<form action="admin.php?page=w3tc_cdn" method="post" style="padding: 20px"
class="w3tc_cdn_rackspace_form">
<?php
Util_Ui::hidden( 'w3tc-rackspace-user-name', 'user_name', $details['user_name'] );
Util_Ui::hidden( 'w3tc-rackspace-api-key', 'api_key', $details['api_key'] );
Util_Ui::hidden( 'w3tc-rackspace-access-token', 'access_token', $details['access_token'] );
Util_Ui::hidden( 'w3tc-rackspace-access-region-descriptor', 'access_region_descriptor', $details['access_region_descriptor_serialized'] );
Util_Ui::hidden( 'w3tc-rackspace-region', 'region', $details['region'] );
echo wp_kses(
Util_Ui::nonce_field( 'w3tc' ),
array(
'input' => array(
'type' => array(),
'name' => array(),
'value' => array(),
),
)
);
if ( isset( $details['error_message'] ) ) {
echo '<div class="error">' . esc_html( $details['error_message'] ) . '</div>';
}
?>
<div class="metabox-holder">
<?php Util_Ui::postbox_header( esc_html__( 'Create new service', 'w3-total-cache' ) ); ?>
<table class="form-table" style="width: 100%">
<tr>
<th style="width: 150px"><?php esc_html_e( 'Name:', 'w3-total-cache' ); ?></td>
<td>
<input name="name" type="text" class="w3tc-ignore-change"
style="width: 100px"
value="<?php echo esc_attr( $details['name'] ); ?>" />
</td>
</tr>
<tr>
<th style="white-space: nowrap"><?php esc_html_e( 'Traffic Type:', 'w3-total-cache' ); ?></td>
<td>
<label>
<input name="protocol" type="radio"
class="w3tc-ignore-change w3tc_cdn_rackspace_protocol"
value="http"
<?php checked( $details['protocol'], 'http' ); ?> />
http://
</label>
<br />
<label>
<input name="protocol" type="radio"
class="w3tc-ignore-change w3tc_cdn_rackspace_protocol"
value="https"
<?php checked( $details['protocol'], 'https' ); ?> />
https://
</label>
</td>
</tr>
<tr>
<th><?php esc_html_e( 'Origin:', 'w3-total-cache' ); ?></td>
<td>
<?php echo esc_html( $details['origin'] ); ?>
</td>
</tr>
<tr class="w3tc_cdn_rackspace_cname_http"
style="<?php echo esc_attr( $details['cname_http_style'] ); ?>">
<th style="white-space: nowrap">
<?php
echo wp_kses(
sprintf(
// translators: 1 opening HTML acronym tag, 2 closing HTML acronym tag.
__(
'Primary %1$sCNAME%2$s:',
'w3-total-cache'
),
'<acronym title="' . esc_attr__( 'Canonical Name', 'w3-total-cache' ) . '">',
'</acronym>'
),
array(
'acronym' => array(
'title' => array(),
),
)
);
?>
</th>
<td>
<input name="cname_http" type="text" class="w3tc-ignore-change"
style="width: 200px"
value="<?php echo esc_attr( $details['cname_http'] ); ?>" />
<p class="description">
<?php esc_html_e( 'The domain name through which visitors retrieve content. You will be provided with a target domain to use as an alias for this CNAME', 'w3-total-cache' ); ?>
</p>
</td>
</tr>
<tr class="w3tc_cdn_rackspace_cname_https"
style="<?php echo esc_attr( $details['cname_https_style'] ); ?>">
<th style="white-space: nowrap">
<?php
echo wp_kses(
sprintf(
// translators: 1 opening HTML acronym tag, 2 closing HTML acronym tag.
__(
'Primary %1$sCNAME%2$s:',
'w3-total-cache'
),
'<acronym title="' . esc_attr__( 'Canonical Name', 'w3-total-cache' ) . '">',
'</acronym>'
),
array(
'acronym' => array(
'title' => array(),
),
)
);
?>
</td>
<td>
<input name="cname_https_prefix" type="text" class="w3tc-ignore-change"
style="width: 100px"
value="<?php echo esc_attr( $details['cname_https_prefix'] ); ?>" />
<input name="" type="text" readonly="readonly"
value=".xxxx.secure.raxcdn.com" />
<p class="description">
<?php esc_html_e( 'The name should be a single word, and cannot contain any dots (.).', 'w3-total-cache' ); ?>
</p>
</td>
</tr>
</table>
<p class="submit">
<input type="button"
class="w3tc_cdn_rackspace_service_create_done w3tc-button-save button-primary"
value="<?php esc_attr_e( 'Next', 'w3-total-cache' ); ?>" />
</p>
<?php Util_Ui::postbox_footer(); ?>
</div>
</form>

View File

@@ -0,0 +1,70 @@
<?php
/**
* File: Cdn_RackSpaceCdn_Popup_View_Created.php
*
* @package W3TC
*/
namespace W3TC;
if ( ! defined( 'W3TC' ) ) {
die();
}
?>
<form action="admin.php?page=w3tc_cdn" method="post" style="padding: 20px" class="w3tc_cdn_rackspace_form">
<?php
Util_Ui::hidden( 'w3tc-rackspace-action', 'w3tc_action', 'cdn_rackspace_service_created_done' );
Util_Ui::hidden( 'w3tc-rackspace-user-name', 'user_name', $details['user_name'] );
Util_Ui::hidden( 'w3tc-rackspace-api-key', 'api_key', $details['api_key'] );
Util_Ui::hidden( 'w3tc-rackspace-access-token', 'access_token', $details['access_token'] );
Util_Ui::hidden( 'w3tc-rackspace-access-region-descriptor', 'access_region_descriptor', $details['access_region_descriptor_serialized'] );
Util_Ui::hidden( 'w3tc-rackspace-region', 'region', $details['region'] );
Util_Ui::hidden( 'w3tc-rackspace-service-id', 'service_id', $details['service_id'] );
echo wp_kses(
Util_Ui::nonce_field( 'w3tc' ),
array(
'input' => array(
'type' => array(),
'name' => array(),
'value' => array(),
),
)
);
?>
<div class="metabox-holder">
<?php Util_Ui::postbox_header( esc_html__( 'Succeeded', 'w3-total-cache' ) ); ?>
<div style="text-align: center" class="w3tc_rackspace_created_in_progress">
<div class="spinner" style="float: right; display: block"></div>
<div style="text-align: left">
Service <?php echo esc_html( $details['name'] ); ?> was successfully created.<br />
Waiting for RackSpace to finish the provisioning process.<br />
<br />
Actual state is:
<strong><span class="w3tc_rackspace_created_status">Initiated</span></strong>
</div>
</div>
<div style="display: none" class="w3tc_rackspace_created_done">
<div style="text-align: center">
<div style="text-align: left">
Service <?php echo esc_html( $details['name'] ); ?> was successfully configured.<br />
<?php if ( ! $is_https ) : ?>
<br />
Next, update the domain's <acronym title="Domain Name System">DNS</acronym> records
<strong><?php echo esc_html( $details['cname'] ); ?></strong> and add <acronym title="Canonical Name">CNAME</acronym> alias to<br />
<strong class="w3tc_rackspace_access_url"></strong> to enable caching.
<?php endif; ?>
</div>
</div>
<p class="submit">
<input type="button"
class="w3tc_popup_submit w3tc-button-save button-primary"
value="<?php esc_attr_e( 'Done', 'w3-total-cache' ); ?>" />
</p>
</div>
<?php Util_Ui::postbox_footer(); ?>
</div>
</form>

Some files were not shown because too many files have changed in this diff Show More