Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/w3-total-cache/Generic_AdminActions_Flush.php
+++ b/w3-total-cache/Generic_AdminActions_Flush.php
@@ -259,6 +259,10 @@
*/
public function w3tc_flush_post() {
$post_id = Util_Request::get_integer( 'post_id' );
+ if ( $post_id <= 0 ) {
+ $post_id = (int) Util_Environment::detect_post_id();
+ }
+
w3tc_flush_post( $post_id, true, array( 'ui_action' => 'flush_button' ) );
$this->_redirect_after_flush( 'pgcache_purge_post', __( 'purge Page cache for post', 'w3-total-cache' ) );
--- a/w3-total-cache/Generic_Plugin.php
+++ b/w3-total-cache/Generic_Plugin.php
@@ -560,46 +560,52 @@
public function admin_bar_menu() {
global $wp_admin_bar;
+ $is_full_admin = current_user_can( 'manage_options' );
+
/**
- * Hard floor at `manage_options` so the `w3tc_capability_admin_bar`
- * filter cannot downgrade the admin bar to lower-capability users
- * (consistent with the floor applied at every other
- * `w3tc_capability_*` filter site).
+ * Full Performance menu for manage_options; otherwise a purge-only
+ * subset when the user has a filterable purge capability.
+ *
+ * @since 2.10.4
*/
- if ( ! current_user_can( 'manage_options' ) ) {
+ if ( ! $is_full_admin && ! Util_Capability::user_can_purge_anything() ) {
return;
}
- $base_capability = apply_filters( 'w3tc_capability_admin_bar', 'manage_options' );
-
- if ( current_user_can( $base_capability ) ) {
- $w3tc_modules = Dispatcher::component( 'ModuleStatus' );
+ $base_capability = Util_Capability::sanitize_capability(
+ apply_filters( 'w3tc_capability_admin_bar', 'manage_options' )
+ );
- $menu_items = array();
+ if ( ! $is_full_admin && ! current_user_can( $base_capability ) && ! Util_Capability::user_can_purge_anything() ) {
+ return;
+ }
- $menu_items['00010.generic'] = array(
- 'id' => 'w3tc',
- 'title' => sprintf(
- '<span class="w3tc-icon ab-icon"></span><span class="ab-label">%s</span>',
- __( 'Performance', 'w3-total-cache' )
- ),
- 'href' => network_admin_url( 'admin.php?page=w3tc_dashboard' ),
- );
+ $w3tc_modules = Dispatcher::component( 'ModuleStatus' );
+ $menu_items = array();
- $current_page = Util_Request::get_string( 'page', 'w3tc_dashboard' );
+ $menu_items['00010.generic'] = array(
+ 'id' => 'w3tc',
+ 'title' => sprintf(
+ '<span class="w3tc-icon ab-icon"></span><span class="ab-label">%s</span>',
+ __( 'Performance', 'w3-total-cache' )
+ ),
+ 'href' => $is_full_admin
+ ? network_admin_url( 'admin.php?page=w3tc_dashboard' )
+ : false,
+ );
- if ( $w3tc_modules->plugin_is_enabled() ) {
+ if ( $w3tc_modules->plugin_is_enabled() ) {
+ if ( $is_full_admin || Util_Capability::can_flush_all() ) {
$menu_items['10010.generic'] = array(
'id' => 'w3tc_flush_all',
'parent' => 'w3tc',
'title' => __( 'Purge All Caches', 'w3-total-cache' ),
- 'href' => Util_Nonce::admin_nonce_url(
- network_admin_url( 'admin.php?page=' . $current_page . '&w3tc_flush_all' ),
- 'w3tc_flush_all'
- ),
+ 'href' => Util_Capability::purge_action_url( 'w3tc_flush_all' ),
);
+ }
- // Add menu item to flush all cached except Cloudflare.
+ // Cloudflare except-cf and Purge Modules: full admin only.
+ if ( $is_full_admin ) {
if (
$this->_config->get_boolean( 'cdnfsd.enabled' ) &&
'cloudflare' === $this->_config->get_string( 'cdnfsd.engine' ) &&
@@ -626,26 +632,37 @@
),
);
}
+ }
+
+ if ( ! is_admin() ) {
+ $post_id = (int) Util_Environment::detect_post_id();
- if ( ! is_admin() ) {
+ if ( $post_id > 0 && ( $is_full_admin || Util_Capability::can_flush_post_id( $post_id ) ) ) {
$menu_items['10020.generic'] = array(
'id' => 'w3tc_flush_current_page',
'parent' => 'w3tc',
'title' => __( 'Purge Current Page', 'w3-total-cache' ),
- 'href' => Util_Nonce::admin_nonce_url(
- network_admin_url( 'admin.php?page=w3tc_dashboard&w3tc_flush_post&post_id=' . Util_Environment::detect_post_id() . '&force=true' ),
- 'w3tc_flush_post'
+ 'href' => Util_Capability::purge_action_url(
+ 'w3tc_flush_post',
+ array(
+ 'post_id' => $post_id,
+ 'force' => 'true',
+ )
),
);
}
+ }
+ if ( $is_full_admin ) {
$menu_items['20010.generic'] = array(
'id' => 'w3tc_flush',
'parent' => 'w3tc',
'title' => __( 'Purge Modules', 'w3-total-cache' ),
);
}
+ }
+ if ( $is_full_admin ) {
$menu_items['30000.generic'] = array(
'id' => 'w3tc_feature_showcase',
'parent' => 'w3tc',
@@ -697,41 +714,70 @@
'title' => __( 'Debug: Overlays', 'w3-total-cache' ),
);
}
+ }
+
+ $menu_items = apply_filters( 'w3tc_admin_bar_menu', $menu_items );
+
+ /**
+ * Non-admins: keep allowlisted purge items whose href targets a
+ * filterable purge action (extensions may reuse an allowlisted id
+ * with a dashboard-only link).
+ *
+ * @since 2.10.4
+ */
+ if ( ! $is_full_admin ) {
+ $menu_items = array_filter(
+ $menu_items,
+ static function ( $item ) {
+ return is_array( $item ) && Util_Capability::is_allowed_purge_admin_bar_item( $item );
+ }
+ );
+ }
- $menu_items = apply_filters( 'w3tc_admin_bar_menu', $menu_items );
+ $w3tc_keys = array_keys( $menu_items );
+ asort( $w3tc_keys );
- $w3tc_keys = array_keys( $menu_items );
- asort( $w3tc_keys );
+ foreach ( $w3tc_keys as $w3tc_key ) {
+ $item_id = $menu_items[ $w3tc_key ]['id'];
- foreach ( $w3tc_keys as $w3tc_key ) {
- $capability = apply_filters(
- 'w3tc_capability_admin_bar_' . $menu_items[ $w3tc_key ]['id'],
- $base_capability
- );
+ if ( 'w3tc' === $item_id ) {
+ $default_cap = $is_full_admin
+ ? $base_capability
+ : Util_Capability::admin_bar_parent_capability();
+ } elseif ( 'w3tc_flush_all' === $item_id ) {
+ $default_cap = Util_Capability::flush_all_capability();
+ } elseif ( 'w3tc_flush_current_page' === $item_id ) {
+ $default_cap = Util_Capability::flush_post_capability();
+ } else {
+ $default_cap = $base_capability;
+ }
- if ( current_user_can( $capability ) ) {
- $wp_admin_bar->add_menu( $menu_items[ $w3tc_key ] );
- }
+ $capability = Util_Capability::sanitize_capability(
+ apply_filters( 'w3tc_capability_admin_bar_' . $item_id, $default_cap )
+ );
+
+ if ( current_user_can( $capability ) ) {
+ $wp_admin_bar->add_menu( $menu_items[ $w3tc_key ] );
}
+ }
- if ( ! is_admin() && ! is_null( $this->frontend_notice ) && ! empty( $this->frontend_notice['messages'] ) ) {
- $sanitized_messages = array_map( 'wp_strip_all_tags', $this->frontend_notice['messages'] );
- $w3tc_label = esc_html( wp_html_excerpt( implode( ' ', $sanitized_messages ), 120, '…' ) );
-
- if ( '' !== $w3tc_label ) {
- $wp_admin_bar->add_menu(
- array(
- 'id' => 'w3tc_frontend_notice',
- 'parent' => 'top-secondary',
- 'title' => $w3tc_label . '<span class="w3tc-frontend-notice-dismiss" role="button" aria-label="' . esc_attr__( 'Dismiss notice', 'w3-total-cache' ) . '">×</span>',
- 'href' => false,
- 'meta' => array(
- 'class' => 'w3tc-frontend-notice w3tc-frontend-notice-' . $this->frontend_notice['type'],
- 'title' => $w3tc_label,
- ),
- )
- );
- }
+ if ( $is_full_admin && ! is_admin() && ! is_null( $this->frontend_notice ) && ! empty( $this->frontend_notice['messages'] ) ) {
+ $sanitized_messages = array_map( 'wp_strip_all_tags', $this->frontend_notice['messages'] );
+ $w3tc_label = esc_html( wp_html_excerpt( implode( ' ', $sanitized_messages ), 120, '…' ) );
+
+ if ( '' !== $w3tc_label ) {
+ $wp_admin_bar->add_menu(
+ array(
+ 'id' => 'w3tc_frontend_notice',
+ 'parent' => 'top-secondary',
+ 'title' => $w3tc_label . '<span class="w3tc-frontend-notice-dismiss" role="button" aria-label="' . esc_attr__( 'Dismiss notice', 'w3-total-cache' ) . '">×</span>',
+ 'href' => false,
+ 'meta' => array(
+ 'class' => 'w3tc-frontend-notice w3tc-frontend-notice-' . $this->frontend_notice['type'],
+ 'title' => $w3tc_label,
+ ),
+ )
+ );
}
}
}
--- a/w3-total-cache/Generic_Plugin_Admin.php
+++ b/w3-total-cache/Generic_Plugin_Admin.php
@@ -62,6 +62,7 @@
add_filter( 'w3tc_save_options', array( $this, 'w3tc_save_options' ) );
+ add_action( 'admin_init', array( $this, 'maybe_execute_purge_action' ), 1 );
add_action( 'admin_init', array( $this, 'admin_init' ) );
add_action( 'admin_init_w3tc_dashboard', array( 'W3TCGeneric_WidgetAccount', 'admin_init_w3tc_dashboard' ) );
add_action( 'admin_init_w3tc_dashboard', array( 'W3TCGeneric_WidgetSettings', 'admin_init_w3tc_dashboard' ) );
@@ -126,16 +127,15 @@
}
/**
- * Load action
+ * Detect a dispatchable admin-action key from the current request.
*
- * @return void
+ * @since 2.10.4
+ *
+ * @param Root_AdminActions $executor Action executor.
+ *
+ * @return string|false Handler key or false.
*/
- public function load() {
- $this->add_help_tabs();
- $this->_page = Util_Admin::get_current_page();
-
- // Run plugin action.
- $action = false;
+ private function detect_admin_action( Root_AdminActions $executor ) {
$display_only_keys = array(
'w3tc_note',
'w3tc_error',
@@ -143,8 +143,6 @@
'w3tc_message_action',
);
- $executor = new Root_AdminActions();
-
/**
* Resolve the handler key from POST on form submissions so a stale
* `w3tc_*` query arg left on the URL cannot shadow the clicked submit
@@ -171,12 +169,63 @@
}
if ( $executor->is_dispatchable( $w3tc_key ) ) {
- $action = $w3tc_key;
- break 2;
+ return $w3tc_key;
}
}
}
+ return false;
+ }
+
+ /**
+ * Execute allowlisted purge actions on admin_init (no W3TC menu required).
+ *
+ * @since 2.10.4
+ *
+ * @return void
+ */
+ public function maybe_execute_purge_action() {
+ $executor = new Root_AdminActions();
+ $action = $this->detect_admin_action( $executor );
+
+ if ( ! $action || ! Util_Capability::is_purge_action( $action ) ) {
+ return;
+ }
+
+ if ( ! Util_Nonce::verify_admin( Util_Nonce::admin_action( $action ) ) ) {
+ wp_nonce_ays( Util_Nonce::admin_action( $action ) );
+ }
+
+ if ( ! Util_Capability::can_execute_purge( $action ) ) {
+ wp_die(
+ esc_html__( 'You do not have sufficient permissions to perform this action.', 'w3-total-cache' ),
+ '',
+ array( 'response' => 403 )
+ );
+ }
+
+ try {
+ $executor->execute( $action );
+ } catch ( Exception $e ) {
+ $w3tc_key = 'admin_action_failed_' . $action;
+ Util_Admin::redirect_with_custom_messages( array(), array( $w3tc_key => $e->getMessage() ) );
+ }
+
+ exit();
+ }
+
+ /**
+ * Load action
+ *
+ * @return void
+ */
+ public function load() {
+ $this->add_help_tabs();
+ $this->_page = Util_Admin::get_current_page();
+
+ $executor = new Root_AdminActions();
+ $action = $this->detect_admin_action( $executor );
+
if ( $action ) {
/**
* Per-action nonce key for the admin-action dispatcher.
@@ -188,13 +237,21 @@
}
/**
- * Defence-in-depth: enforce manage_options at the dispatcher
- * regardless of how the menu was registered or how
- * w3tc_capability_menu was filtered.
+ * Purge allowlist uses filterable caps; all other admin actions
+ * keep the manage_options floor.
*
* @since 2.10.0
+ * @since 2.10.4 Purge allowlist exception.
*/
- if ( ! current_user_can( 'manage_options' ) ) {
+ if ( Util_Capability::is_purge_action( $action ) ) {
+ if ( ! Util_Capability::can_execute_purge( $action ) ) {
+ wp_die(
+ esc_html__( 'You do not have sufficient permissions to perform this action.', 'w3-total-cache' ),
+ '',
+ array( 'response' => 403 )
+ );
+ }
+ } elseif ( ! current_user_can( 'manage_options' ) ) {
wp_die(
esc_html__( 'You do not have sufficient permissions to perform this action.', 'w3-total-cache' ),
'',
@@ -1155,26 +1212,17 @@
*/
public function favorite_actions( $actions ) {
/**
- * Floor the filterable cap at manage_options so a downstream
- * filter cannot expose the "Empty Caches" favorite action to
- * non-admins.
- *
- * Early-return when the current user is not an admin: there is no
- * reason to compute or filter a capability for an action we would
- * never expose to them anyway.
+ * Filterable purge-all capability (default manage_options).
*
- * @since 2.10.0
+ * @since 2.10.4
*/
- if ( ! current_user_can( 'manage_options' ) ) {
+ if ( ! Util_Capability::can_flush_all() ) {
return $actions;
}
- $capability = apply_filters( 'w3tc_capability_favorite_action_flush_all', 'manage_options' );
- if ( empty( $capability ) ) {
- $capability = 'manage_options';
- }
+ $capability = Util_Capability::flush_all_capability();
- $actions[ Util_Nonce::admin_nonce_url( network_admin_url( 'admin.php?page=w3tc_dashboard&w3tc_flush_all' ), 'w3tc_flush_all' ) ] = array(
+ $actions[ Util_Capability::purge_action_url( 'w3tc_flush_all' ) ] = array(
__( 'Empty Caches', 'w3-total-cache' ),
$capability,
);
--- a/w3-total-cache/Generic_Plugin_AdminRowActions.php
+++ b/w3-total-cache/Generic_Plugin_AdminRowActions.php
@@ -34,38 +34,7 @@
* @return array
*/
public function post_row_actions( $actions, $post ) {
- $capability = apply_filters( 'w3tc_capability_row_action_w3tc_flush_post', 'manage_options' );
-
- /**
- * Floor the filterable cap at manage_options to prevent a
- * downstream filter from exposing the row action to non-admins
- *.
- *
- * @since 2.10.0
- */
- if ( empty( $capability ) || ! current_user_can( 'manage_options' ) ) {
- return $actions;
- }
-
- if ( current_user_can( $capability ) ) {
- $actions = array_merge(
- $actions,
- array(
- 'w3tc_flush_post' => sprintf(
- '<a href="%s">' . __( 'Purge from cache', 'w3-total-cache' ) . '</a>',
- Util_Nonce::admin_nonce_url(
- sprintf(
- 'admin.php?page=w3tc_dashboard&w3tc_flush_post&post_id=%d&force=true',
- $post->ID
- ),
- 'w3tc_flush_post'
- )
- ),
- )
- );
- }
-
- return $actions;
+ return $this->add_flush_post_row_action( $actions, $post );
}
/**
@@ -77,36 +46,42 @@
* @return array
*/
public function page_row_actions( $actions, $post ) {
- $capability = apply_filters( 'w3tc_capability_row_action_w3tc_flush_post', 'manage_options' );
+ return $this->add_flush_post_row_action( $actions, $post );
+ }
- /**
- * Floor the filterable cap at manage_options to prevent a
- * downstream filter from exposing the row action to non-admins
- *.
- *
- * @since 2.10.0
- */
- if ( empty( $capability ) || ! current_user_can( 'manage_options' ) ) {
+ /**
+ * Append Purge from cache when the user may flush this post.
+ *
+ * @since 2.10.4
+ *
+ * @param array $actions Actions.
+ * @param object $post Post.
+ *
+ * @return array
+ */
+ private function add_flush_post_row_action( $actions, $post ) {
+ if ( ! isset( $post->ID ) || ! Util_Capability::can_flush_post_id( (int) $post->ID ) ) {
return $actions;
}
- if ( current_user_can( $capability ) ) {
- $actions = array_merge(
- $actions,
- array(
- 'w3tc_flush_post' => sprintf(
- '<a href="%s">' . __( 'Purge from cache', 'w3-total-cache' ) . '</a>',
- Util_Nonce::admin_nonce_url(
- sprintf(
- 'admin.php?page=w3tc_dashboard&w3tc_flush_post&post_id=%d&force=true',
- $post->ID
- ),
- 'w3tc_flush_post'
+ $actions = array_merge(
+ $actions,
+ array(
+ 'w3tc_flush_post' => sprintf(
+ '<a href="%s">%s</a>',
+ esc_url(
+ Util_Capability::purge_action_url(
+ 'w3tc_flush_post',
+ array(
+ 'post_id' => (int) $post->ID,
+ 'force' => 'true',
+ )
)
),
- )
- );
- }
+ esc_html__( 'Purge from cache', 'w3-total-cache' )
+ ),
+ )
+ );
return $actions;
}
@@ -117,24 +92,24 @@
* @return void
*/
public function post_submitbox_start() {
- if ( current_user_can( 'manage_options' ) ) {
- global $post;
- if ( ! is_null( $post ) ) {
- $w3tc_url = Util_Ui::url(
- array(
- 'page' => 'w3tc_dashboard',
- 'w3tc_flush_post' => 'y',
- 'post_id' => $post->ID,
- 'force' => true,
- )
- );
+ global $post;
- printf(
- '<div><a href="%s">%s</a></div>',
- esc_url( $w3tc_url ),
- esc_html__( 'Purge from cache', 'w3-total-cache' )
- );
- }
+ if ( is_null( $post ) || ! Util_Capability::can_flush_post_id( (int) $post->ID ) ) {
+ return;
}
+
+ printf(
+ '<div><a href="%s">%s</a></div>',
+ esc_url(
+ Util_Capability::purge_action_url(
+ 'w3tc_flush_post',
+ array(
+ 'post_id' => (int) $post->ID,
+ 'force' => 'true',
+ )
+ )
+ ),
+ esc_html__( 'Purge from cache', 'w3-total-cache' )
+ );
}
}
--- a/w3-total-cache/UserExperience_LazyLoad_Mutator.php
+++ b/w3-total-cache/UserExperience_LazyLoad_Mutator.php
@@ -157,21 +157,25 @@
* @return string The modified <img> tag.
*/
public function tag_img_content_replace( $content, $dim ) {
- // do replace.
- $w3tc_count = 0;
- $content = preg_replace(
- '~(s)src=~is',
- '$1src="' . $this->placeholder( $dim['w'], $dim['h'] ) . '" data-src=',
+ $placeholder = $this->placeholder( $dim['w'], $dim['h'] );
+ $w3tc_count = 0;
+ $content = $this->replace_top_level_quoted_attributes(
$content,
- -1,
+ 'src',
+ static function ( $whitespace, $name, $quoted_value ) use ( $placeholder ) {
+ return $whitespace . 'src="' . $placeholder . '" data-src=' . $quoted_value;
+ },
+ 1,
$w3tc_count
);
if ( $w3tc_count > 0 ) {
- $content = preg_replace(
- '~(s)(srcset|sizes)=~is',
- '$1data-$2=',
- $content
+ $content = $this->replace_top_level_quoted_attributes(
+ $content,
+ 'srcset|sizes',
+ static function ( $whitespace, $name, $quoted_value ) {
+ return $whitespace . 'data-' . $name . '=' . $quoted_value;
+ }
);
$content = $this->add_class_lazy( $content );
@@ -206,18 +210,11 @@
}
// if not in attributes - try to find via url.
- if (
- ! preg_match(
- '~ssrc=('([^']*)'|"([^"]*)"|([^'"][^\s]*))~is',
- $content,
- $m
- )
- ) {
+ $w3tc_url = $this->get_top_level_quoted_attribute_value( $content, 'src' );
+ if ( null === $w3tc_url ) {
return $dim;
}
- $w3tc_url = ( ! empty( $m[4] ) ? $m[4] : ( ( ! empty( $m[3] ) ? $m[3] : $m[2] ) ) );
-
// full url found.
if ( isset( $this->posts_by_url[ $w3tc_url ] ) ) {
$post_id = $this->posts_by_url[ $w3tc_url ];
@@ -401,7 +398,128 @@
* @return string The SVG placeholder.
*/
public function placeholder( $w, $h ) {
- return 'data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20' .
- $w . '%20' . $h . ''%3E%3C/svg%3E';
+ $svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' .
+ (int) $w . ' ' . (int) $h . '"></svg>';
+
+ return 'data:image/svg+xml,' . rawurlencode( $svg );
+ }
+
+ /**
+ * Replace top-level quoted attributes, skipping matches inside other attribute values.
+ *
+ * @since 2.10.4
+ *
+ * @param string $content Tag markup to rewrite.
+ * @param string $attr_pattern Attribute name or alternation (e.g. `src` or `srcset|sizes`).
+ * @param callable $replacer Callback `( $whitespace, $name, $quoted_value ) => string`.
+ * @param int $limit Max replacements; -1 for all.
+ * @param int $count Set to the number of replacements performed.
+ *
+ * @return string
+ */
+ public function replace_top_level_quoted_attributes( $content, $attr_pattern, $replacer, $limit = -1, &$count = 0 ) {
+ $count = 0;
+
+ if ( ! preg_match_all(
+ '~(s)(' . $attr_pattern . ')=('([^']*)'|"([^"]*)")~is',
+ $content,
+ $matches,
+ PREG_OFFSET_CAPTURE
+ ) ) {
+ return $content;
+ }
+
+ $replacements = array();
+ foreach ( $matches[0] as $i => $full ) {
+ if ( $limit >= 0 && count( $replacements ) >= $limit ) {
+ break;
+ }
+
+ $offset = $full[1];
+ if ( ! $this->is_outside_attribute_value( $content, $offset ) ) {
+ continue;
+ }
+
+ $replacements[] = array(
+ 'offset' => $offset,
+ 'length' => strlen( $full[0] ),
+ 'replacement' => call_user_func(
+ $replacer,
+ $matches[1][ $i ][0],
+ $matches[2][ $i ][0],
+ $matches[3][ $i ][0]
+ ),
+ );
+ }
+
+ for ( $i = count( $replacements ) - 1; $i >= 0; $i-- ) {
+ $content = substr_replace(
+ $content,
+ $replacements[ $i ]['replacement'],
+ $replacements[ $i ]['offset'],
+ $replacements[ $i ]['length']
+ );
+ ++$count;
+ }
+
+ return $content;
+ }
+
+ /**
+ * Read the first top-level quoted attribute value, or null if none.
+ *
+ * @since 2.10.4
+ *
+ * @param string $content Tag markup.
+ * @param string $attr Attribute name.
+ *
+ * @return string|null
+ */
+ public function get_top_level_quoted_attribute_value( $content, $attr ) {
+ if ( ! preg_match_all(
+ '~(s)' . preg_quote( $attr, '~' ) . '=('([^']*)'|"([^"]*)")~is',
+ $content,
+ $matches,
+ PREG_OFFSET_CAPTURE
+ ) ) {
+ return null;
+ }
+
+ foreach ( $matches[0] as $i => $full ) {
+ if ( ! $this->is_outside_attribute_value( $content, $full[1] ) ) {
+ continue;
+ }
+
+ $quoted = $matches[2][ $i ][0];
+ return substr( $quoted, 1, -1 );
+ }
+
+ return null;
+ }
+
+ /**
+ * Whether the byte offset sits outside a quoted HTML attribute value.
+ *
+ * @since 2.10.4
+ *
+ * @param string $content Tag markup.
+ * @param int $offset Byte offset into $content.
+ *
+ * @return bool
+ */
+ private function is_outside_attribute_value( $content, $offset ) {
+ $in = null;
+ for ( $i = 0; $i < $offset; $i++ ) {
+ $ch = $content[ $i ];
+ if ( null === $in ) {
+ if ( '"' === $ch || "'" === $ch ) {
+ $in = $ch;
+ }
+ } elseif ( $ch === $in ) {
+ $in = null;
+ }
+ }
+
+ return null === $in;
}
}
--- a/w3-total-cache/UserExperience_LazyLoad_Mutator_Picture.php
+++ b/w3-total-cache/UserExperience_LazyLoad_Mutator_Picture.php
@@ -87,12 +87,12 @@
private function tag_source( $matches ) {
$content = $matches[0];
- $content = preg_replace(
- '~(s)(srcset|sizes)=~i',
- '$1data-$2=',
- $content
+ return $this->common->replace_top_level_quoted_attributes(
+ $content,
+ 'srcset|sizes',
+ static function ( $whitespace, $name, $quoted_value ) {
+ return $whitespace . 'data-' . $name . '=' . $quoted_value;
+ }
);
-
- return $content;
}
}
--- a/w3-total-cache/Util_Capability.php
+++ b/w3-total-cache/Util_Capability.php
@@ -0,0 +1,352 @@
+<?php
+/**
+ * File: Util_Capability.php
+ *
+ * Filterable cache-purge capability helpers. Default remains manage_options;
+ * settings, config save, extensions, and AJAX stay floored elsewhere.
+ *
+ * @package W3TC
+ * @since 2.10.4
+ */
+
+namespace W3TC;
+
+/**
+ * Class Util_Capability
+ *
+ * @since 2.10.4
+ */
+class Util_Capability {
+
+ /**
+ * Admin-action keys that may use filterable purge caps.
+ *
+ * @since 2.10.4
+ *
+ * @var string[]
+ */
+ const PURGE_ACTIONS = array(
+ 'w3tc_flush_all',
+ 'w3tc_flush_post',
+ 'w3tc_flush_current_page',
+ );
+
+ /**
+ * Admin-bar menu item ids allowed for non-manage_options purge users.
+ *
+ * @since 2.10.4
+ *
+ * @var string[]
+ */
+ const PURGE_ADMIN_BAR_IDS = array(
+ 'w3tc',
+ 'w3tc_flush_all',
+ 'w3tc_flush_current_page',
+ );
+
+ /**
+ * Sanitize a filtered capability value.
+ *
+ * Non-string or empty values fall back to manage_options.
+ *
+ * @since 2.10.4
+ *
+ * @param mixed $capability Filtered capability.
+ *
+ * @return string
+ */
+ public static function sanitize_capability( $capability ) {
+ if ( ! is_string( $capability ) || '' === $capability ) {
+ return 'manage_options';
+ }
+
+ return $capability;
+ }
+
+ /**
+ * Capability required to purge all caches.
+ *
+ * Applies `w3tc_capability_admin_bar`, then `w3tc_capability_flush_all`,
+ * then `w3tc_capability_favorite_action_flush_all`.
+ *
+ * @since 2.10.4
+ *
+ * @return string
+ */
+ public static function flush_all_capability() {
+ /**
+ * Legacy admin-bar base capability.
+ *
+ * @param string $capability Capability slug.
+ */
+ $capability = apply_filters( 'w3tc_capability_admin_bar', 'manage_options' );
+
+ /**
+ * Filters the capability required to purge all caches.
+ *
+ * @since 2.10.4
+ *
+ * @param string $capability Capability slug.
+ */
+ $capability = apply_filters( 'w3tc_capability_flush_all', $capability );
+
+ /**
+ * Legacy favorite-action capability filter.
+ *
+ * @param string $capability Capability slug.
+ */
+ $capability = apply_filters( 'w3tc_capability_favorite_action_flush_all', $capability );
+
+ return self::sanitize_capability( $capability );
+ }
+
+ /**
+ * Capability required to purge a post/page (or current page).
+ *
+ * Applies `w3tc_capability_admin_bar`, then `w3tc_capability_flush_post`,
+ * then `w3tc_capability_row_action_w3tc_flush_post`.
+ *
+ * @since 2.10.4
+ *
+ * @return string
+ */
+ public static function flush_post_capability() {
+ /**
+ * Legacy admin-bar base capability.
+ *
+ * @param string $capability Capability slug.
+ */
+ $capability = apply_filters( 'w3tc_capability_admin_bar', 'manage_options' );
+
+ /**
+ * Filters the capability required to purge a specific post/page.
+ *
+ * @since 2.10.4
+ *
+ * @param string $capability Capability slug.
+ */
+ $capability = apply_filters( 'w3tc_capability_flush_post', $capability );
+
+ /**
+ * Legacy row-action capability filter.
+ *
+ * @param string $capability Capability slug.
+ */
+ $capability = apply_filters( 'w3tc_capability_row_action_w3tc_flush_post', $capability );
+
+ return self::sanitize_capability( $capability );
+ }
+
+ /**
+ * Whether the current user may purge all caches.
+ *
+ * @since 2.10.4
+ *
+ * @return bool
+ */
+ public static function can_flush_all() {
+ return current_user_can( self::flush_all_capability() );
+ }
+
+ /**
+ * Whether the current user may purge posts/pages (role-level gate).
+ *
+ * @since 2.10.4
+ *
+ * @return bool
+ */
+ public static function can_flush_post() {
+ return current_user_can( self::flush_post_capability() );
+ }
+
+ /**
+ * Whether the current user may purge a specific post.
+ *
+ * Requires the filtered flush-post capability and edit_post for the id.
+ *
+ * @since 2.10.4
+ *
+ * @param int $post_id Post ID.
+ *
+ * @return bool
+ */
+ public static function can_flush_post_id( $post_id ) {
+ $post_id = (int) $post_id;
+
+ if ( ! self::can_flush_post() ) {
+ return false;
+ }
+
+ if ( $post_id <= 0 ) {
+ return false;
+ }
+
+ return current_user_can( 'edit_post', $post_id );
+ }
+
+ /**
+ * Whether the current user has any purge privilege.
+ *
+ * @since 2.10.4
+ *
+ * @return bool
+ */
+ public static function user_can_purge_anything() {
+ return self::can_flush_all() || self::can_flush_post();
+ }
+
+ /**
+ * Capability for the Performance admin-bar parent for purge-only users.
+ *
+ * Uses a purge cap the current user already satisfies so flush-all-only
+ * grants still show the parent item.
+ *
+ * @since 2.10.4
+ *
+ * @return string
+ */
+ public static function admin_bar_parent_capability() {
+ if ( self::can_flush_all() ) {
+ return self::flush_all_capability();
+ }
+
+ if ( self::can_flush_post() ) {
+ return self::flush_post_capability();
+ }
+
+ return 'manage_options';
+ }
+
+ /**
+ * Whether an admin-action key is a filterable purge action.
+ *
+ * @since 2.10.4
+ *
+ * @param string $action Dispatcher handler key.
+ *
+ * @return bool
+ */
+ public static function is_purge_action( $action ) {
+ return in_array( $action, self::PURGE_ACTIONS, true );
+ }
+
+ /**
+ * Whether a filtered admin-bar item is safe for purge-only non-admins.
+ *
+ * Requires an allowlisted id and (except the parent) an href that targets
+ * a filterable purge action — extensions may reuse an allowlisted id with
+ * a dashboard-only link.
+ *
+ * @since 2.10.4
+ *
+ * @param array $item Admin-bar menu item.
+ *
+ * @return bool
+ */
+ public static function is_allowed_purge_admin_bar_item( array $item ) {
+ if ( ! isset( $item['id'] ) || ! in_array( $item['id'], self::PURGE_ADMIN_BAR_IDS, true ) ) {
+ return false;
+ }
+
+ if ( 'w3tc' === $item['id'] ) {
+ return true;
+ }
+
+ if ( empty( $item['href'] ) || ! is_string( $item['href'] ) ) {
+ return false;
+ }
+
+ foreach ( self::PURGE_ACTIONS as $action ) {
+ if ( false !== strpos( $item['href'], $action ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Whether the current user may execute a purge admin-action.
+ *
+ * @since 2.10.4
+ *
+ * @param string $action Dispatcher handler key.
+ *
+ * @return bool
+ */
+ public static function can_execute_purge( $action ) {
+ if ( ! self::is_purge_action( $action ) ) {
+ return false;
+ }
+
+ if ( 'w3tc_flush_all' === $action ) {
+ return self::can_flush_all();
+ }
+
+ if ( 'w3tc_flush_post' === $action ) {
+ $post_id = Util_Request::get_integer( 'post_id' );
+ if ( $post_id <= 0 ) {
+ $post_id = (int) Util_Environment::detect_post_id();
+ }
+
+ return self::can_flush_post_id( $post_id );
+ }
+
+ return self::can_flush_current_page_request();
+ }
+
+ /**
+ * Authorize w3tc_flush_current_page for a same-host URL target.
+ *
+ * Flush-all may clear any same-host URL. Flush-post requires edit_post on
+ * the post resolved from the URL.
+ *
+ * @since 2.10.4
+ *
+ * @return bool
+ */
+ public static function can_flush_current_page_request() {
+ $url = Util_Request::get_string( 'url' );
+ if ( '' === $url && isset( $_SERVER['HTTP_REFERER'] ) ) {
+ $url = sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized immediately; referer fallback for URL flush.
+ }
+
+ if ( '' === $url ) {
+ return false;
+ }
+
+ $validated = wp_validate_redirect( $url, false );
+ if ( ! $validated ) {
+ return false;
+ }
+
+ if ( self::can_flush_all() ) {
+ return true;
+ }
+
+ $post_id = (int) url_to_postid( $validated );
+ if ( $post_id <= 0 ) {
+ return false;
+ }
+
+ return self::can_flush_post_id( $post_id );
+ }
+
+ /**
+ * Build a nonce-protected admin URL for a purge action (no W3TC page).
+ *
+ * @since 2.10.4
+ *
+ * @param string $action Dispatcher handler key.
+ * @param array<string,mixed> $args Extra query args (e.g. post_id).
+ *
+ * @return string
+ */
+ public static function purge_action_url( $action, array $args = array() ) {
+ $query = array_merge( array( $action => '1' ), $args );
+ $query_string = http_build_query( $query, '', '&' );
+ $url = Util_Ui::admin_url( 'admin.php?' . $query_string );
+
+ return Util_Nonce::admin_nonce_url( $url, $action );
+ }
+}
--- a/w3-total-cache/vendor/composer/installed.php
+++ b/w3-total-cache/vendor/composer/installed.php
@@ -1,9 +1,9 @@
<?php return array(
'root' => array(
'name' => 'boldgrid/w3-total-cache',
- 'pretty_version' => '2.10.3',
- 'version' => '2.10.3.0',
- 'reference' => 'd6bace7be8cd9ed91d6363454f59e3cb3c5d9ee6',
+ 'pretty_version' => '2.10.4',
+ 'version' => '2.10.4.0',
+ 'reference' => '3e50364ce76d53ce6fb5b04120da40c8ca53eb4f',
'type' => 'wordpress-plugin',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -38,9 +38,9 @@
'dev_requirement' => false,
),
'boldgrid/w3-total-cache' => array(
- 'pretty_version' => '2.10.3',
- 'version' => '2.10.3.0',
- 'reference' => 'd6bace7be8cd9ed91d6363454f59e3cb3c5d9ee6',
+ 'pretty_version' => '2.10.4',
+ 'version' => '2.10.4.0',
+ 'reference' => '3e50364ce76d53ce6fb5b04120da40c8ca53eb4f',
'type' => 'wordpress-plugin',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
--- a/w3-total-cache/w3-total-cache-api.php
+++ b/w3-total-cache/w3-total-cache-api.php
@@ -10,7 +10,7 @@
defined( 'ABSPATH' ) || die;
define( 'W3TC', true );
-define( 'W3TC_VERSION', '2.10.3' );
+define( 'W3TC_VERSION', '2.10.4' );
define( 'W3TC_POWERED_BY', 'W3 Total Cache' );
define( 'W3TC_EMAIL', 'w3tc@w3-edge.com' );
define( 'W3TC_TEXT_DOMAIN', 'w3-total-cache' );
--- a/w3-total-cache/w3-total-cache.php
+++ b/w3-total-cache/w3-total-cache.php
@@ -3,7 +3,7 @@
* Plugin Name: W3 Total Cache
* Plugin URI: https://www.boldgrid.com/totalcache/
* Description: The highest rated and most complete WordPress performance plugin. Dramatically improve the speed and user experience of your site. Add browser, page, object and database caching as well as minify and content delivery network (CDN) to WordPress.
- * Version: 2.10.3
+ * Version: 2.10.4
* Requires at least: 6.0
* Requires PHP: 7.4
* Author: BoldGrid