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 +236 -146 2.0.2trunk View file →
@@ -47,24 +47,33 @@
47 47 if ( ! $settings['enable_cdn'] ) {
48 48 return;
49 49 }
50 50
51 - add_filter( 'wp_get_attachment_url', array( $this, 'cdn_url' ), 9999 );
52 - add_filter( 'stylesheet_uri', array( $this, 'cdn_url' ), 9999 );
53 - add_filter( 'smilies_src', array( $this, 'cdn_url' ), 9999 );
54 - add_filter( 'bp_core_fetch_avatar_url', array( $this, 'cdn_url' ), 9999 );
55 - add_filter( 'style_loader_src', array( $this, 'cdn_url' ), 9999 );
56 - add_filter( 'script_loader_src', array( $this, 'cdn_url' ), 9999 );
57 - add_filter( 'powered_cache_cdn_assets', array( $this, 'cdn_url' ), 9999 );
58 - add_filter( 'wp_calculate_image_srcset', array( $this, 'srcset_url' ), 9999 );
59 - add_filter( 'wp_get_attachment_image_src', array( $this, 'attachment_image_src' ), 9999 );
60 - add_filter( 'the_content', array( $this, 'cdn_images' ), 9999 );
61 - add_filter( 'post_thumbnail_html', array( $this, 'cdn_images' ), 9999 );
62 - add_filter( 'get_avatar', array( $this, 'cdn_images' ), 9999 );
63 - add_filter( 'bp_core_fetch_avatar', array( $this, 'cdn_images' ), 9999 );
64 - add_filter( 'widget_text', array( $this, 'cdn_images' ), 9999 );
65 - add_filter( 'media_image', array( $this, 'cdn_images' ), 9999 );
66 - add_filter( 'powered_cache_page_caching_buffer', array( $this, 'cdn_images' ), 9999 );
51 + add_action( 'plugins_loaded', [ $this, 'cdn_setup' ] );
52 +
53 + }
54 +
55 + /**
56 + * Setup CDN
57 + */
58 + public function cdn_setup() {
59 + /**
60 + * Filters CDN integration
61 + *
62 + * @hook powered_cache_cdn_disable
63 + *
64 + * @param {boolean} False by default.
65 + *
66 + * @return {boolean} New value.
67 + * @since 2.1
68 + */
69 + $disable_cdn = apply_filters( 'powered_cache_cdn_disable', false );
70 +
71 + if ( $disable_cdn ) {
72 + return;
73 + }
74 +
75 + add_action( 'setup_theme', [ $this, 'start_buffer' ] );
67 76 add_filter( 'powered_cache_fo_optimized_url', array( $this, 'cdn_optimizer_url' ), 9999, 2 );
68 77
69 78 /**
70 79 * Fires after setup CDN hooks.
@@ -76,151 +85,162 @@
76 85 do_action( 'powered_cache_cdn_setup' );
77 86 }
78 87
79 88 /**
80 - * CDN hostname replacement for optimized URLs
89 + * Start output buffering
81 90 *
82 - * @param string $optimized_url Optimized assets URL
83 - * @param string $path rel path of the files
84 - *
85 - * @return mixed
91 + * @since 2.2
86 92 */
87 - public function cdn_optimizer_url( $optimized_url, $path ) {
88 - if ( '-' === $path[0] ) {
89 - $path = @gzuncompress( base64_decode( substr( $path, 1 ) ) ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged,WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
90 - }
91 -
92 - $zone = 'all';
93 - if ( $path && false !== strpos( $path, '.css' ) ) {
94 - $zone = 'css';
95 - } elseif ( $path && false !== strpos( $path, '.js' ) ) {
96 - $zone = 'js';
97 - }
98 -
99 - $cdn_url = self::get_best_possible_cdn_host( $zone );
100 -
101 - $optimized_url = str_replace( home_url(), $cdn_url, $optimized_url );
102 -
103 - return $optimized_url;
93 + public function start_buffer() {
94 + ob_start( [ '\PoweredCache\CDN', 'end_buffering' ] );
104 95 }
105 96
106 97 /**
107 - * Replace CDN url for srcset
98 + * Replace origin URLs with CDN.
108 99 *
109 - * @param array $sources Source files
100 + * @param string $contents Output buffer.
101 + * @param int $phase Bitmask of PHP_OUTPUT_HANDLER_* constants.
110 102 *
111 - * @return mixed
112 - * @since 1.2
103 + * @return string|string[]|null
104 + * @since 2.2
113 105 */
114 - public function srcset_url( $sources ) {
115 - foreach ( $sources as $key => $source ) {
116 - $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 + }
117 114 }
118 115
119 - return $sources;
116 + return $contents;
120 117 }
121 118
122 119 /**
123 - * Replace given url with the CDN host
120 + * Whether integrate or not integrate CDN.
124 121 *
125 - * @param string $url URL
126 - *
127 - * @return string
122 + * @return bool
123 + * @since 2.2
128 124 */
129 - public function cdn_url( $url ) {
130 - if ( is_admin() || is_preview() ) {
131 - 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;
132 129 }
133 130
134 - 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;
135 142 }
136 143
137 -
138 144 /**
139 - * Replace URL for wp_get_attachment_image_src function
145 + * Rewrite contents
140 146 *
141 - * @param array|false $image Either array with src, width & height, icon src, or false
147 + * @param string $contents HTML Output.
142 148 *
143 - * @return array $image
144 - * @since 1.2.4
149 + * @return string|string[]|null
150 + * @since 2.2
145 151 */
146 - public function attachment_image_src( $image ) {
147 - if ( is_admin() || is_preview() ) {
148 - 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;
149 156 }
150 157
151 - if ( ! (bool) $image ) {
152 - return $image;
153 - }
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 );
154 161
155 - $cdn_url = self::get_best_possible_cdn_host( 'image' );
156 - // no host found
157 - if ( false === $cdn_url ) {
158 - return $image;
159 - }
160 -
161 - $image[0] = str_replace( home_url(), $cdn_url, $image[0] );
162 -
163 - return $image;
162 + return $rewritten_contents;
164 163 }
165 164
166 165 /**
167 - * Replace images with CDN host
166 + * Rewrite the matched url.
168 167 *
169 - * @param string $content Content
168 + * @param array $matches Matched part of content.
170 169 *
171 - * @return mixed
170 + * @return mixed|string
171 + * @since 2.2
172 172 */
173 - public function cdn_images( $content ) {
174 - if ( is_admin() || is_preview() || empty( $content ) ) {
175 - 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;
176 194 }
177 195
178 - if ( ! class_exists( 'DOMDocument' ) ) {
179 - 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;
180 199 }
181 200
182 - $document = new DOMDocument();
183 - @$document->loadHTML( $content ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
184 - $images = $document->getElementsByTagName( 'img' );
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 + }
206 + }
185 207
186 - $img_url = array();
187 - foreach ( $images as $img ) {
188 - if ( $img->hasAttribute( 'src' ) ) {
189 - $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;
190 223 }
191 224
192 - if ( $img->hasAttribute( 'srcset' ) ) {
193 - $imgset = explode( ',', $img->getAttribute( 'srcset' ) );
194 - foreach ( $imgset as $src_item ) {
195 - $imgsrc = explode( ' ', trim( $src_item ) );
196 - // first item is url, second width
197 - $img_url[] = $imgsrc[0];
198 - }
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;
199 228 }
200 229 }
201 230
202 - $img_url = array_unique( $img_url );
203 - $cdn_url = array();
204 -
205 - foreach ( $img_url as $replace_url ) {
206 - $cdn_url[] = $this->maybe_cdn_replace( $replace_url );
207 - }
208 -
209 - return str_replace( $img_url, $cdn_url, $content );
231 + return $file_url;
210 232 }
211 233
212 -
213 234 /**
214 - * this method decide to url replacement.
215 - * rejected files & external resources will be ignored
235 + * Check whether given url excluded or not.
216 236 *
217 - * @param string $url URL
237 + * @param string $file_url File URL.
218 238 *
219 - * @return string cdn url
220 - * @since 1.0
239 + * @return bool
240 + * @since 2.2
221 241 */
222 - public static function maybe_cdn_replace( $url ) {
242 + private static function is_excluded( $file_url ) {
223 243 $settings = \PoweredCache\Utils\get_settings();
224 244
225 245 // rejected file
226 246 if ( ! empty( $settings['cdn_rejected_files'] ) ) {
@@ -226,59 +246,55 @@
226 246 if ( ! empty( $settings['cdn_rejected_files'] ) ) {
227 247 $cdn_rejected_files = preg_split( '#(\r\n|\r|\n)#', $settings['cdn_rejected_files'], - 1, PREG_SPLIT_NO_EMPTY );
228 248 $cdn_rejected_files = implode( '|', $cdn_rejected_files );
229 249
230 - if ( preg_match( '#(' . $cdn_rejected_files . ')#', $url ) ) {
231 - return $url;
250 + if ( preg_match( '#(' . $cdn_rejected_files . ')#', $file_url ) ) {
251 + return true;
232 252 }
233 253 }
234 254
235 - // external resource
236 - if ( false === strpos( $url, home_url() ) ) {
237 - return $url;
255 + // don't replace for base64 encoded images
256 + if ( false !== strpos( $file_url, 'data:image' ) ) {
257 + return true;
238 258 }
239 259
240 - // don't replace for base64 encoded images
241 - if ( false !== strpos( $url, 'data:image' ) ) {
242 - 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
243 274 }
244 275
245 - $url_path = wp_parse_url( $url, PHP_URL_PATH );
246 - $ext = explode( '.', $url_path );
247 - $ext = strtolower( end( $ext ) );
248 -
249 276 $zone = 'all';
250 -
251 - /**
252 - * Filters supported image extensions.
253 - *
254 - * @hook powered_cache_cdn_image_extensions
255 - *
256 - * @param {array} $image_extensions Supported image extensions.
257 - *
258 - * @return {array} New value.
259 - * @since 1.0
260 - */
261 - $image_extensions = apply_filters( 'powered_cache_cdn_image_extensions', array( 'jpg', 'jpeg', 'gif', 'png', 'bmp', 'ico', 'webp' ) );
262 -
263 - if ( in_array( $ext, $image_extensions, true ) ) {
264 - $zone = 'image';
265 - } elseif ( 'css' === $ext ) {
277 + if ( $path && false !== strpos( $path, '.css' ) ) {
266 278 $zone = 'css';
267 - } elseif ( 'js' === $ext ) {
279 + } elseif ( $path && false !== strpos( $path, '.js' ) ) {
268 280 $zone = 'js';
269 281 }
270 282
271 - $cdn_url = self::get_best_possible_cdn_host( $zone );
283 + $cdn_hostname = self::get_best_possible_cdn_host( $zone );
272 284
273 - // no host found
274 - if ( false === $cdn_url ) {
275 - return $url;
285 + if ( empty( $cdn_hostname ) ) {
286 + return $optimized_url;
276 287 }
277 288
278 - 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;
279 294 }
280 295
296 +
281 297 /**
282 298 * try to catch best cdn address to given zone
283 299 *
284 300 * @param string $zone keys of Powered_Cache_Admin_Helper::cdn_zones
@@ -308,6 +324,80 @@
308 324
309 325 return false;
310 326 }
311 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 +
312 403 }
313 -