PluginProbe
Powered Cache – Caching and Optimization for WordPress – Easily Improve PageSpeed & Web Vitals Score / trunk
Powered Cache – Caching and Optimization for WordPress – Easily Improve PageSpeed & Web Vitals Score vtrunk
trunk 1.0 1.0.1 1.1 1.1.1 1.1.2 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 2.0 2.0.1 2.0.2 2.0.3 2.0.4 2.1 2.1.1 2.1.2 2.2 2.2.1 All 69 releases
← All changes | includes/classes/CDN.php +220 -150 2.1trunk View file →
@@ -47,8 +47,16 @@
47 47 if ( ! $settings['enable_cdn'] ) {
48 48 return;
49 49 }
50 50
51 + add_action( 'plugins_loaded', [ $this, 'cdn_setup' ] );
52 +
53 + }
54 +
55 + /**
56 + * Setup CDN
57 + */
58 + public function cdn_setup() {
51 59 /**
52 60 * Filters CDN integration
53 61 *
54 62 * @hook powered_cache_cdn_disable
@@ -54,9 +62,9 @@
54 62 * @hook powered_cache_cdn_disable
55 63 *
56 64 * @param {boolean} False by default.
57 65 *
58 - * @return {array} New value.
66 + * @return {boolean} New value.
59 67 * @since 2.1
60 68 */
61 69 $disable_cdn = apply_filters( 'powered_cache_cdn_disable', false );
62 70
@@ -63,24 +71,9 @@
63 71 if ( $disable_cdn ) {
64 72 return;
65 73 }
66 74
67 - add_filter( 'wp_get_attachment_url', array( $this, 'cdn_url' ), 9999 );
68 - add_filter( 'stylesheet_uri', array( $this, 'cdn_url' ), 9999 );
69 - add_filter( 'smilies_src', array( $this, 'cdn_url' ), 9999 );
70 - add_filter( 'bp_core_fetch_avatar_url', array( $this, 'cdn_url' ), 9999 );
71 - add_filter( 'style_loader_src', array( $this, 'cdn_url' ), 9999 );
72 - add_filter( 'script_loader_src', array( $this, 'cdn_url' ), 9999 );
73 - add_filter( 'powered_cache_cdn_assets', array( $this, 'cdn_url' ), 9999 );
74 - add_filter( 'wp_calculate_image_srcset', array( $this, 'srcset_url' ), 9999 );
75 - add_filter( 'wp_get_attachment_image_src', array( $this, 'attachment_image_src' ), 9999 );
76 - add_filter( 'the_content', array( $this, 'cdn_images' ), 9999 );
77 - add_filter( 'post_thumbnail_html', array( $this, 'cdn_images' ), 9999 );
78 - add_filter( 'get_avatar', array( $this, 'cdn_images' ), 9999 );
79 - add_filter( 'bp_core_fetch_avatar', array( $this, 'cdn_images' ), 9999 );
80 - add_filter( 'widget_text', array( $this, 'cdn_images' ), 9999 );
81 - add_filter( 'media_image', array( $this, 'cdn_images' ), 9999 );
82 - add_filter( 'powered_cache_page_caching_buffer', array( $this, 'cdn_images' ), 9999 );
75 + add_action( 'setup_theme', [ $this, 'start_buffer' ] );
83 76 add_filter( 'powered_cache_fo_optimized_url', array( $this, 'cdn_optimizer_url' ), 9999, 2 );
84 77
85 78 /**
86 79 * Fires after setup CDN hooks.
@@ -92,155 +85,162 @@
92 85 do_action( 'powered_cache_cdn_setup' );
93 86 }
94 87
95 88 /**
96 - * CDN hostname replacement for optimized URLs
89 + * Start output buffering
97 90 *
98 - * @param string $optimized_url Optimized assets URL
99 - * @param string $path rel path of the files
100 - *
101 - * @return mixed
91 + * @since 2.2
102 92 */
103 - public function cdn_optimizer_url( $optimized_url, $path ) {
104 - if ( '-' === $path[0] ) {
105 - $path = @gzuncompress( base64_decode( substr( $path, 1 ) ) ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged,WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
106 - }
107 -
108 - $zone = 'all';
109 - if ( $path && false !== strpos( $path, '.css' ) ) {
110 - $zone = 'css';
111 - } elseif ( $path && false !== strpos( $path, '.js' ) ) {
112 - $zone = 'js';
113 - }
114 -
115 - $cdn_url = self::get_best_possible_cdn_host( $zone );
116 -
117 - $optimized_url = str_replace( home_url(), $cdn_url, $optimized_url );
118 -
119 - return $optimized_url;
93 + public function start_buffer() {
94 + ob_start( [ '\PoweredCache\CDN', 'end_buffering' ] );
120 95 }
121 96
122 97 /**
123 - * Replace CDN url for srcset
98 + * Replace origin URLs with CDN.
124 99 *
125 - * @param array $sources Source files
100 + * @param string $contents Output buffer.
101 + * @param int $phase Bitmask of PHP_OUTPUT_HANDLER_* constants.
126 102 *
127 - * @return mixed
128 - * @since 1.2
103 + * @return string|string[]|null
104 + * @since 2.2
129 105 */
130 - public function srcset_url( $sources ) {
131 - foreach ( (array) $sources as $key => $source ) {
132 - $sources[ $key ]['url'] = $this->cdn_url( $source['url'] );
106 + private static function end_buffering( $contents, $phase ) {
107 + if ( $phase & PHP_OUTPUT_HANDLER_FINAL || $phase & PHP_OUTPUT_HANDLER_END ) {
108 +
109 + if ( ! self::skip_cdn_integration() ) {
110 + $rewritten_contents = self::rewriter( $contents );
111 +
112 + return $rewritten_contents;
113 + }
133 114 }
134 115
135 - return $sources;
116 + return $contents;
136 117 }
137 118
138 119 /**
139 - * Replace given url with the CDN host
120 + * Whether integrate or not integrate CDN.
140 121 *
141 - * @param string $url URL
142 - *
143 - * @return string
122 + * @return bool
123 + * @since 2.2
144 124 */
145 - public function cdn_url( $url ) {
146 - if ( is_admin() || is_preview() ) {
147 - return $url;
125 + private static function skip_cdn_integration() {
126 + // check request method
127 + if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || 'GET' !== $_SERVER['REQUEST_METHOD'] ) {
128 + return true;
148 129 }
149 130
150 - return $this->maybe_cdn_replace( $url );
131 + // Skip CDN integration for Block Editor requests, particularly when using the media endpoint for in-editor image resizing/manipulation.
132 + if ( isset( $_SERVER['HTTP_X_WP_NONCE'] ) && wp_is_json_request() ) {
133 + return true;
134 + }
135 +
136 + // check conditional tags
137 + if ( is_admin() || is_trackback() || is_robots() || is_preview() ) {
138 + return true;
139 + }
140 +
141 + return false;
151 142 }
152 143
153 -
154 144 /**
155 - * Replace URL for wp_get_attachment_image_src function
145 + * Rewrite contents
156 146 *
157 - * @param array|false $image Either array with src, width & height, icon src, or false
147 + * @param string $contents HTML Output.
158 148 *
159 - * @return array $image
160 - * @since 1.2.4
149 + * @return string|string[]|null
150 + * @since 2.2
161 151 */
162 - public function attachment_image_src( $image ) {
163 - if ( is_admin() || is_preview() ) {
164 - return $image;
152 + public static function rewriter( $contents ) {
153 + // check rewrite requirements
154 + if ( ! is_string( $contents ) || empty( self::get_file_extensions() ) ) {
155 + return $contents;
165 156 }
166 157
167 - if ( ! (bool) $image ) {
168 - return $image;
169 - }
158 + $included_file_extensions_regex = implode( '|', array_map( 'preg_quote', self::get_file_extensions() ) );
159 + $urls_regex = '#(?:(?:[\"\'\s=>,;]|url\()\K|^)[^\"\'\s(=>,;]+(' . $included_file_extensions_regex . ')(\?[^\/?\\\"\'\s)>,]+)?(?:(?=\/?[?\\\"\'\s)>,&])|$)#i';
160 + $rewritten_contents = preg_replace_callback( $urls_regex, [ '\PoweredCache\CDN', 'rewrite_url' ], $contents );
170 161
171 - $cdn_url = self::get_best_possible_cdn_host( 'image' );
172 - // no host found
173 - if ( false === $cdn_url ) {
174 - return $image;
175 - }
176 -
177 - $image[0] = str_replace( home_url(), $cdn_url, $image[0] );
178 -
179 - return $image;
162 + return $rewritten_contents;
180 163 }
181 164
182 165 /**
183 - * Replace images with CDN host
166 + * Rewrite the matched url.
184 167 *
185 - * @param string $content Content
168 + * @param array $matches Matched part of content.
186 169 *
187 - * @return mixed
170 + * @return mixed|string
171 + * @since 2.2
188 172 */
189 - public function cdn_images( $content ) {
190 - if ( is_admin() || is_preview() || empty( $content ) ) {
191 - return $content;
173 + private static function rewrite_url( $matches ) {
174 + $file_url = $matches[0];
175 + $site_hostname = ( ! empty( $_SERVER['HTTP_HOST'] ) ) ? $_SERVER['HTTP_HOST'] : wp_parse_url( home_url(), PHP_URL_HOST ); // phpcs:ignore
176 +
177 + /**
178 + * Filters site hostname(s).
179 + *
180 + * @hook powered_cache_cdn_site_hostnames
181 + *
182 + * @param {array} Site hostnames for CDN replacement.
183 + *
184 + * @return {array} New value.
185 + *
186 + * @since 2.2
187 + */
188 + $site_hostnames = (array) apply_filters( 'powered_cache_cdn_site_hostnames', array( $site_hostname ) );
189 +
190 + $zone = self::get_zone_by_ext( $matches[1] );
191 + $cdn_hostname = self::get_best_possible_cdn_host( $zone );
192 + if ( empty( $cdn_hostname ) ) {
193 + return $file_url;
192 194 }
193 195
194 - if ( ! class_exists( 'DOMDocument' ) ) {
195 - return $content;
196 + // if excluded or already using CDN hostname
197 + if ( self::is_excluded( $file_url ) || false !== stripos( $file_url, $cdn_hostname ) ) {
198 + return $file_url;
196 199 }
197 200
198 - if ( function_exists( 'libxml_use_internal_errors' ) ) {
199 - libxml_use_internal_errors( true );
201 + // rewrite full URL (e.g. https://www.example.com/wp..., https:\/\/www.example.com\/wp..., or //www.example.com/wp...)
202 + foreach ( $site_hostnames as $site_hostname ) {
203 + if ( stripos( $file_url, '//' . $site_hostname ) !== false || stripos( $file_url, '\/\/' . $site_hostname ) !== false ) {
204 + return substr_replace( $file_url, $cdn_hostname, stripos( $file_url, $site_hostname ), strlen( $site_hostname ) );
205 + }
200 206 }
201 207
202 - $document = new DOMDocument();
203 - @$document->loadHTML( $content ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
204 - $images = $document->getElementsByTagName( 'img' );
205 -
206 - $img_url = array();
207 - foreach ( $images as $img ) {
208 - if ( $img->hasAttribute( 'src' ) ) {
209 - $img_url[] = $img->getAttribute( 'src' );
208 + /**
209 + * Filters whether relative urls needs to rewritten or not
210 + *
211 + * @hook powered_cache_cdn_rewrite_relative_urls
212 + *
213 + * @param {boolean} true to automatic update.
214 + *
215 + * @return {boolean} New value.
216 + *
217 + * @since 2.2
218 + */
219 + if ( apply_filters( 'powered_cache_cdn_rewrite_relative_urls', true ) ) { // rewrite relative URLs hook
220 + // rewrite relative URL (e.g. /wp-content/uploads/example.jpg)
221 + if ( strpos( $file_url, '//' ) !== 0 && strpos( $file_url, '/' ) === 0 ) {
222 + return '//' . $cdn_hostname . $file_url;
210 223 }
211 224
212 - if ( $img->hasAttribute( 'srcset' ) ) {
213 - $imgset = explode( ',', $img->getAttribute( 'srcset' ) );
214 - foreach ( $imgset as $src_item ) {
215 - $imgsrc = explode( ' ', trim( $src_item ) );
216 - // first item is url, second width
217 - $img_url[] = $imgsrc[0];
218 - }
225 + // rewrite escaped relative URL (e.g. \/wp-content\/uploads\/example.jpg)
226 + if ( strpos( $file_url, '\/\/' ) !== 0 && strpos( $file_url, '\/' ) === 0 ) {
227 + return '\/\/' . $cdn_hostname . $file_url;
219 228 }
220 229 }
221 230
222 - $img_url = array_unique( $img_url );
223 - $cdn_url = array();
224 -
225 - foreach ( $img_url as $replace_url ) {
226 - $cdn_url[] = $this->maybe_cdn_replace( $replace_url );
227 - }
228 -
229 - return str_replace( $img_url, $cdn_url, $content );
231 + return $file_url;
230 232 }
231 233
232 -
233 234 /**
234 - * this method decide to url replacement.
235 - * rejected files & external resources will be ignored
235 + * Check whether given url excluded or not.
236 236 *
237 - * @param string $url URL
237 + * @param string $file_url File URL.
238 238 *
239 - * @return string cdn url
240 - * @since 1.0
239 + * @return bool
240 + * @since 2.2
241 241 */
242 - public static function maybe_cdn_replace( $url ) {
242 + private static function is_excluded( $file_url ) {
243 243 $settings = \PoweredCache\Utils\get_settings();
244 244
245 245 // rejected file
246 246 if ( ! empty( $settings['cdn_rejected_files'] ) ) {
@@ -246,59 +246,55 @@
246 246 if ( ! empty( $settings['cdn_rejected_files'] ) ) {
247 247 $cdn_rejected_files = preg_split( '#(\r\n|\r|\n)#', $settings['cdn_rejected_files'], - 1, PREG_SPLIT_NO_EMPTY );
248 248 $cdn_rejected_files = implode( '|', $cdn_rejected_files );
249 249
250 - if ( preg_match( '#(' . $cdn_rejected_files . ')#', $url ) ) {
251 - return $url;
250 + if ( preg_match( '#(' . $cdn_rejected_files . ')#', $file_url ) ) {
251 + return true;
252 252 }
253 253 }
254 254
255 - // external resource
256 - if ( false === strpos( $url, home_url() ) ) {
257 - return $url;
255 + // don't replace for base64 encoded images
256 + if ( false !== strpos( $file_url, 'data:image' ) ) {
257 + return true;
258 258 }
259 259
260 - // don't replace for base64 encoded images
261 - if ( false !== strpos( $url, 'data:image' ) ) {
262 - return $url;
260 + return false;
261 + }
262 +
263 + /**
264 + * CDN hostname replacement for optimized URLs
265 + *
266 + * @param string $optimized_url Optimized assets URL
267 + * @param string $path rel path of the files
268 + *
269 + * @return mixed
270 + */
271 + public function cdn_optimizer_url( $optimized_url, $path ) {
272 + if ( '-' === $path[0] ) {
273 + $path = @gzuncompress( base64_decode( substr( $path, 1 ) ) ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged,WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
263 274 }
264 275
265 - $url_path = wp_parse_url( $url, PHP_URL_PATH );
266 - $ext = explode( '.', $url_path );
267 - $ext = strtolower( end( $ext ) );
268 -
269 276 $zone = 'all';
270 -
271 - /**
272 - * Filters supported image extensions.
273 - *
274 - * @hook powered_cache_cdn_image_extensions
275 - *
276 - * @param {array} $image_extensions Supported image extensions.
277 - *
278 - * @return {array} New value.
279 - * @since 1.0
280 - */
281 - $image_extensions = apply_filters( 'powered_cache_cdn_image_extensions', array( 'jpg', 'jpeg', 'gif', 'png', 'bmp', 'ico', 'webp' ) );
282 -
283 - if ( in_array( $ext, $image_extensions, true ) ) {
284 - $zone = 'image';
285 - } elseif ( 'css' === $ext ) {
277 + if ( $path && false !== strpos( $path, '.css' ) ) {
286 278 $zone = 'css';
287 - } elseif ( 'js' === $ext ) {
279 + } elseif ( $path && false !== strpos( $path, '.js' ) ) {
288 280 $zone = 'js';
289 281 }
290 282
291 - $cdn_url = self::get_best_possible_cdn_host( $zone );
283 + $cdn_hostname = self::get_best_possible_cdn_host( $zone );
292 284
293 - // no host found
294 - if ( false === $cdn_url ) {
295 - return $url;
285 + if ( empty( $cdn_hostname ) ) {
286 + return $optimized_url;
296 287 }
297 288
298 - return str_replace( home_url(), $cdn_url, $url );
289 + $cdn_url = '//' . $cdn_hostname;
290 +
291 + $optimized_url = str_replace( home_url(), $cdn_url, $optimized_url );
292 +
293 + return $optimized_url;
299 294 }
300 295
296 +
301 297 /**
302 298 * try to catch best cdn address to given zone
303 299 *
304 300 * @param string $zone keys of Powered_Cache_Admin_Helper::cdn_zones
@@ -328,6 +324,80 @@
328 324
329 325 return false;
330 326 }
331 327
328 + /**
329 + * Get CDN zone by given extension.
330 + *
331 + * @param string $ext File extensions. Eg: .jpg, .gif, .mp3...
332 + *
333 + * @return string
334 + * @since 2.2
335 + */
336 + private static function get_zone_by_ext( $ext ) {
337 + $zone = 'all';
338 +
339 + /* documented in get_file_extensions */
340 + $image_extensions = apply_filters( 'powered_cache_cdn_image_extensions', array( 'jpg', 'jpeg', 'gif', 'png', 'bmp', 'ico', 'webp', 'avif', 'svg' ) );
341 + $image_extensions = array_map(
342 + function ( $ext ) {
343 + return '.' . $ext;
344 + },
345 + $image_extensions
346 + );
347 +
348 + if ( in_array( $ext, $image_extensions, true ) ) {
349 + $zone = 'image';
350 + } elseif ( '.css' === $ext ) {
351 + $zone = 'css';
352 + } elseif ( '.js' === $ext ) {
353 + $zone = 'js';
354 + }
355 +
356 + return $zone;
357 + }
358 +
359 + /**
360 + * Get the list of supported file extensions for CDN integration.
361 + *
362 + * @return array|mixed|void
363 + * @since 2.2
364 + */
365 + public static function get_file_extensions() {
366 + /**
367 + * Filters supported image extensions.
368 + *
369 + * @hook powered_cache_cdn_image_extensions
370 + *
371 + * @param {array} $image_extensions Supported image extensions.
372 + *
373 + * @return {array} New value.
374 + * @deprecated since 2.2. Use powered_cache_cdn_extensions instead.
375 + * @since 1.0
376 + */
377 + $image_extensions = apply_filters( 'powered_cache_cdn_image_extensions', array( 'jpg', 'jpeg', 'gif', 'png', 'bmp', 'ico', 'webp', 'avif', 'svg' ) );
378 +
379 + $extensions = [ 'css', 'js', 'pdf', 'mp3', 'mp4', 'woff2', 'woff', 'ttf', 'otf' ];
380 +
381 + $file_extensions = array_map(
382 + function ( $ext ) {
383 + return '.' . $ext;
384 + },
385 + array_merge( $image_extensions, $extensions )
386 + );
387 +
388 + /**
389 + * Filters supported file extensions.
390 + *
391 + * @hook powered_cache_cdn_extensions
392 + *
393 + * @param {array} $file_extensions Supported file extensions.
394 + *
395 + * @return {array} New value.
396 + * @since 2.2
397 + */
398 + $file_extensions = apply_filters( 'powered_cache_cdn_extensions', $file_extensions );
399 +
400 + return $file_extensions;
401 + }
402 +
332 403 }
333 -