PluginProbe
Powered Cache – Caching and Optimization for WordPress – Easily Improve PageSpeed & Web Vitals Score / 3.7
Powered Cache – Caching and Optimization for WordPress – Easily Improve PageSpeed & Web Vitals Score v3.7
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
powered-cache / includes / classes / CDN.php

CDN.php in Powered Cache – Caching and Optimization for WordPress – Easily Improve PageSpeed & Web Vitals Score 3.7, at includes/classes/CDN.php

405 lines 10.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * CDN functionalities
4 *
5 * @package PoweredCache
6 */
7
8 namespace PoweredCache;
9
10 use \DOMDocument as DOMDocument;
11 use function PoweredCache\Utils\cdn_addresses;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 /**
18 * Class CDN
19 */
20 class CDN {
21
22 /**
23 * Return an instance of the current class
24 *
25 * @return CDN
26 * @since 1.0
27 */
28 public static function factory() {
29 static $instance = false;
30
31 if ( ! $instance ) {
32 $instance = new self();
33 $instance->setup();
34 }
35
36 return $instance;
37 }
38
39 /**
40 * Setup hooks
41 *
42 * @since 1.0
43 */
44 public function setup() {
45 $settings = \PoweredCache\Utils\get_settings();
46
47 if ( ! $settings['enable_cdn'] ) {
48 return;
49 }
50
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' ] );
76 add_filter( 'powered_cache_fo_optimized_url', array( $this, 'cdn_optimizer_url' ), 9999, 2 );
77
78 /**
79 * Fires after setup CDN hooks.
80 *
81 * @hook powered_cache_cdn_setup
82 *
83 * @since 1.0
84 */
85 do_action( 'powered_cache_cdn_setup' );
86 }
87
88 /**
89 * Start output buffering
90 *
91 * @since 2.2
92 */
93 public function start_buffer() {
94 ob_start( [ '\PoweredCache\CDN', 'end_buffering' ] );
95 }
96
97 /**
98 * Replace origin URLs with CDN.
99 *
100 * @param string $contents Output buffer.
101 * @param int $phase Bitmask of PHP_OUTPUT_HANDLER_* constants.
102 *
103 * @return string|string[]|null
104 * @since 2.2
105 */
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 }
114 }
115
116 return $contents;
117 }
118
119 /**
120 * Whether integrate or not integrate CDN.
121 *
122 * @return bool
123 * @since 2.2
124 */
125 private static function skip_cdn_integration() {
126 // check request method
127 if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || 'GET' !== $_SERVER['REQUEST_METHOD'] ) {
128 return true;
129 }
130
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;
142 }
143
144 /**
145 * Rewrite contents
146 *
147 * @param string $contents HTML Output.
148 *
149 * @return string|string[]|null
150 * @since 2.2
151 */
152 public static function rewriter( $contents ) {
153 // check rewrite requirements
154 if ( ! is_string( $contents ) || empty( self::get_file_extensions() ) ) {
155 return $contents;
156 }
157
158 $included_file_extensions_regex = quotemeta( implode( '|', 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 );
161
162 return $rewritten_contents;
163 }
164
165 /**
166 * Rewrite the matched url.
167 *
168 * @param array $matches Matched part of content.
169 *
170 * @return mixed|string
171 * @since 2.2
172 */
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;
194 }
195
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;
199 }
200
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 }
207
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;
223 }
224
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;
228 }
229 }
230
231 return $file_url;
232 }
233
234 /**
235 * Check whether given url excluded or not.
236 *
237 * @param string $file_url File URL.
238 *
239 * @return bool
240 * @since 2.2
241 */
242 private static function is_excluded( $file_url ) {
243 $settings = \PoweredCache\Utils\get_settings();
244
245 // rejected file
246 if ( ! empty( $settings['cdn_rejected_files'] ) ) {
247 $cdn_rejected_files = preg_split( '#(\r\n|\r|\n)#', $settings['cdn_rejected_files'], - 1, PREG_SPLIT_NO_EMPTY );
248 $cdn_rejected_files = implode( '|', $cdn_rejected_files );
249
250 if ( preg_match( '#(' . $cdn_rejected_files . ')#', $file_url ) ) {
251 return true;
252 }
253 }
254
255 // don't replace for base64 encoded images
256 if ( false !== strpos( $file_url, 'data:image' ) ) {
257 return true;
258 }
259
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
274 }
275
276 $zone = 'all';
277 if ( $path && false !== strpos( $path, '.css' ) ) {
278 $zone = 'css';
279 } elseif ( $path && false !== strpos( $path, '.js' ) ) {
280 $zone = 'js';
281 }
282
283 $cdn_hostname = self::get_best_possible_cdn_host( $zone );
284
285 if ( empty( $cdn_hostname ) ) {
286 return $optimized_url;
287 }
288
289 $cdn_url = '//' . $cdn_hostname;
290
291 $optimized_url = str_replace( home_url(), $cdn_url, $optimized_url );
292
293 return $optimized_url;
294 }
295
296
297 /**
298 * try to catch best cdn address to given zone
299 *
300 * @param string $zone keys of Powered_Cache_Admin_Helper::cdn_zones
301 *
302 * @return mixed string | false
303 */
304 public static function get_best_possible_cdn_host( $zone = 'all' ) {
305 global $powered_cache_cdn_addresses;
306
307 if ( ! isset( $powered_cache_cdn_addresses ) ) {
308 $powered_cache_cdn_addresses = cdn_addresses();
309 }
310
311 if ( isset( $powered_cache_cdn_addresses[ $zone ] ) && is_array( $powered_cache_cdn_addresses[ $zone ] ) ) {
312 // if we have multiple host for the same resource get randomly
313 $random_key = array_rand( $powered_cache_cdn_addresses[ $zone ] );
314
315 return $powered_cache_cdn_addresses[ $zone ][ $random_key ];
316 }
317
318 // fallback to primary host
319 if ( isset( $powered_cache_cdn_addresses['all'] ) && is_array( $powered_cache_cdn_addresses['all'] ) ) {
320 $random_key = array_rand( $powered_cache_cdn_addresses['all'] );
321
322 return $powered_cache_cdn_addresses['all'][ $random_key ];
323 }
324
325 return false;
326 }
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
403 }
404
405