PluginProbe
Powered Cache – Caching and Optimization for WordPress – Easily Improve PageSpeed & Web Vitals Score / 2.2.1
Powered Cache – Caching and Optimization for WordPress – Easily Improve PageSpeed & Web Vitals Score v2.2.1
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 2.2.1, at includes/classes/CDN.php

392 lines 9.7 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 /**
52 * Filters CDN integration
53 *
54 * @hook powered_cache_cdn_disable
55 *
56 * @param {boolean} False by default.
57 *
58 * @return {boolean} New value.
59 * @since 2.1
60 */
61 $disable_cdn = apply_filters( 'powered_cache_cdn_disable', false );
62
63 if ( $disable_cdn ) {
64 return;
65 }
66
67 add_action( 'setup_theme', [ $this, 'start_buffer' ] );
68 add_filter( 'powered_cache_fo_optimized_url', array( $this, 'cdn_optimizer_url' ), 9999, 2 );
69
70 /**
71 * Fires after setup CDN hooks.
72 *
73 * @hook powered_cache_cdn_setup
74 *
75 * @since 1.0
76 */
77 do_action( 'powered_cache_cdn_setup' );
78 }
79
80 /**
81 * Start output buffering
82 *
83 * @since 2.2
84 */
85 public function start_buffer() {
86 ob_start( 'self::end_buffering' );
87 }
88
89 /**
90 * Replace origin URLs with CDN.
91 *
92 * @param string $contents Output buffer.
93 * @param int $phase Bitmask of PHP_OUTPUT_HANDLER_* constants.
94 *
95 * @return string|string[]|null
96 * @since 2.2
97 */
98 private static function end_buffering( $contents, $phase ) {
99 if ( $phase & PHP_OUTPUT_HANDLER_FINAL || $phase & PHP_OUTPUT_HANDLER_END ) {
100
101 if ( ! self::skip_cdn_integration() ) {
102 $rewritten_contents = self::rewriter( $contents );
103
104 return $rewritten_contents;
105 }
106 }
107
108 return $contents;
109 }
110
111 /**
112 * Whether integrate or not integrate CDN.
113 *
114 * @return bool
115 * @since 2.2
116 */
117 private static function skip_cdn_integration() {
118 // check request method
119 if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || 'GET' !== $_SERVER['REQUEST_METHOD'] ) {
120 return true;
121 }
122
123 // check conditional tags
124 if ( is_admin() || is_trackback() || is_robots() || is_preview() ) {
125 return true;
126 }
127
128 return false;
129 }
130
131 /**
132 * Rewrite contents
133 *
134 * @param string $contents HTML Output.
135 *
136 * @return string|string[]|null
137 * @since 2.2
138 */
139 public static function rewriter( $contents ) {
140 // check rewrite requirements
141 if ( ! is_string( $contents ) || empty( self::get_file_extensions() ) ) {
142 return $contents;
143 }
144
145 $included_file_extensions_regex = quotemeta( implode( '|', self::get_file_extensions() ) );
146 $urls_regex = '#(?:(?:[\"\'\s=>,;]|url\()\K|^)[^\"\'\s(=>,;]+(' . $included_file_extensions_regex . ')(\?[^\/?\\\"\'\s)>,]+)?(?:(?=\/?[?\\\"\'\s)>,&])|$)#i';
147 $rewritten_contents = preg_replace_callback( $urls_regex, 'self::rewrite_url', $contents );
148
149 return $rewritten_contents;
150 }
151
152 /**
153 * Rewrite the matched url.
154 *
155 * @param array $matches Matched part of content.
156 *
157 * @return mixed|string
158 * @since 2.2
159 */
160 private static function rewrite_url( $matches ) {
161 $file_url = $matches[0];
162 $site_hostname = ( ! empty( $_SERVER['HTTP_HOST'] ) ) ? $_SERVER['HTTP_HOST'] : wp_parse_url( home_url(), PHP_URL_HOST );
163
164 /**
165 * Filters site hostname(s).
166 *
167 * @hook powered_cache_cdn_site_hostnames
168 *
169 * @param {array} Site hostnames for CDN replacement.
170 *
171 * @return {array} New value.
172 *
173 * @since 2.2
174 */
175 $site_hostnames = (array) apply_filters( 'powered_cache_cdn_site_hostnames', array( $site_hostname ) );
176
177 $zone = self::get_zone_by_ext( $matches[1] );
178 $cdn_hostname = self::get_best_possible_cdn_host( $zone );
179 if ( empty( $cdn_hostname ) ) {
180 return $file_url;
181 }
182
183 // if excluded or already using CDN hostname
184 if ( self::is_excluded( $file_url ) || false !== stripos( $file_url, $cdn_hostname ) ) {
185 return $file_url;
186 }
187
188 // rewrite full URL (e.g. https://www.example.com/wp..., https:\/\/www.example.com\/wp..., or //www.example.com/wp...)
189 foreach ( $site_hostnames as $site_hostname ) {
190 if ( stripos( $file_url, '//' . $site_hostname ) !== false || stripos( $file_url, '\/\/' . $site_hostname ) !== false ) {
191 return substr_replace( $file_url, $cdn_hostname, stripos( $file_url, $site_hostname ), strlen( $site_hostname ) );
192 }
193 }
194
195 /**
196 * Filters whether relative urls needs to rewritten or not
197 *
198 * @hook powered_cache_cdn_rewrite_relative_urls
199 *
200 * @param {boolean} true to automatic update.
201 *
202 * @return {boolean} New value.
203 *
204 * @since 2.2
205 */
206 if ( apply_filters( 'powered_cache_cdn_rewrite_relative_urls', true ) ) { // rewrite relative URLs hook
207 // rewrite relative URL (e.g. /wp-content/uploads/example.jpg)
208 if ( strpos( $file_url, '//' ) !== 0 && strpos( $file_url, '/' ) === 0 ) {
209 return '//' . $cdn_hostname . $file_url;
210 }
211
212 // rewrite escaped relative URL (e.g. \/wp-content\/uploads\/example.jpg)
213 if ( strpos( $file_url, '\/\/' ) !== 0 && strpos( $file_url, '\/' ) === 0 ) {
214 return '\/\/' . $cdn_hostname . $file_url;
215 }
216 }
217
218 return $file_url;
219 }
220
221 /**
222 * Check whether given url excluded or not.
223 *
224 * @param string $file_url File URL.
225 *
226 * @return bool
227 * @since 2.2
228 */
229 private static function is_excluded( $file_url ) {
230 $settings = \PoweredCache\Utils\get_settings();
231
232 // rejected file
233 if ( ! empty( $settings['cdn_rejected_files'] ) ) {
234 $cdn_rejected_files = preg_split( '#(\r\n|\r|\n)#', $settings['cdn_rejected_files'], - 1, PREG_SPLIT_NO_EMPTY );
235 $cdn_rejected_files = implode( '|', $cdn_rejected_files );
236
237 if ( preg_match( '#(' . $cdn_rejected_files . ')#', $file_url ) ) {
238 return true;
239 }
240 }
241
242 // don't replace for base64 encoded images
243 if ( false !== strpos( $file_url, 'data:image' ) ) {
244 return true;
245 }
246
247 return false;
248 }
249
250 /**
251 * CDN hostname replacement for optimized URLs
252 *
253 * @param string $optimized_url Optimized assets URL
254 * @param string $path rel path of the files
255 *
256 * @return mixed
257 */
258 public function cdn_optimizer_url( $optimized_url, $path ) {
259 if ( '-' === $path[0] ) {
260 $path = @gzuncompress( base64_decode( substr( $path, 1 ) ) ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged,WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
261 }
262
263 $zone = 'all';
264 if ( $path && false !== strpos( $path, '.css' ) ) {
265 $zone = 'css';
266 } elseif ( $path && false !== strpos( $path, '.js' ) ) {
267 $zone = 'js';
268 }
269
270 $cdn_hostname = self::get_best_possible_cdn_host( $zone );
271
272 if ( empty( $cdn_hostname ) ) {
273 return $optimized_url;
274 }
275
276 $cdn_url = '//' . $cdn_hostname;
277
278 $optimized_url = str_replace( home_url(), $cdn_url, $optimized_url );
279
280 return $optimized_url;
281 }
282
283
284 /**
285 * try to catch best cdn address to given zone
286 *
287 * @param string $zone keys of Powered_Cache_Admin_Helper::cdn_zones
288 *
289 * @return mixed string | false
290 */
291 public static function get_best_possible_cdn_host( $zone = 'all' ) {
292 global $powered_cache_cdn_addresses;
293
294 if ( ! isset( $powered_cache_cdn_addresses ) ) {
295 $powered_cache_cdn_addresses = cdn_addresses();
296 }
297
298 if ( isset( $powered_cache_cdn_addresses[ $zone ] ) && is_array( $powered_cache_cdn_addresses[ $zone ] ) ) {
299 // if we have multiple host for the same resource get randomly
300 $random_key = array_rand( $powered_cache_cdn_addresses[ $zone ] );
301
302 return $powered_cache_cdn_addresses[ $zone ][ $random_key ];
303 }
304
305 // fallback to primary host
306 if ( isset( $powered_cache_cdn_addresses['all'] ) && is_array( $powered_cache_cdn_addresses['all'] ) ) {
307 $random_key = array_rand( $powered_cache_cdn_addresses['all'] );
308
309 return $powered_cache_cdn_addresses['all'][ $random_key ];
310 }
311
312 return false;
313 }
314
315 /**
316 * Get CDN zone by given extension.
317 *
318 * @param string $ext File extensions. Eg: .jpg, .gif, .mp3...
319 *
320 * @return string
321 * @since 2.2
322 */
323 private static function get_zone_by_ext( $ext ) {
324 $zone = 'all';
325
326 /* documented in get_file_extensions */
327 $image_extensions = apply_filters( 'powered_cache_cdn_image_extensions', array( 'jpg', 'jpeg', 'gif', 'png', 'bmp', 'ico', 'webp', 'avif', 'svg' ) );
328 $image_extensions = array_map(
329 function ( $ext ) {
330 return '.' . $ext;
331 },
332 $image_extensions
333 );
334
335 if ( in_array( $ext, $image_extensions, true ) ) {
336 $zone = 'image';
337 } elseif ( '.css' === $ext ) {
338 $zone = 'css';
339 } elseif ( '.js' === $ext ) {
340 $zone = 'js';
341 }
342
343 return $zone;
344 }
345
346 /**
347 * Get the list of supported file extensions for CDN integration.
348 *
349 * @return array|mixed|void
350 * @since 2.2
351 */
352 public static function get_file_extensions() {
353 /**
354 * Filters supported image extensions.
355 *
356 * @hook powered_cache_cdn_image_extensions
357 *
358 * @param {array} $image_extensions Supported image extensions.
359 *
360 * @return {array} New value.
361 * @deprecated since 2.2. Use powered_cache_cdn_extensions instead.
362 * @since 1.0
363 */
364 $image_extensions = apply_filters( 'powered_cache_cdn_image_extensions', array( 'jpg', 'jpeg', 'gif', 'png', 'bmp', 'ico', 'webp', 'avif', 'svg' ) );
365
366 $extensions = [ 'css', 'js', 'pdf', 'mp3', 'mp4', 'woff2', 'woff', 'ttf', 'otf' ];
367
368 $file_extensions = array_map(
369 function ( $ext ) {
370 return '.' . $ext;
371 },
372 array_merge( $image_extensions, $extensions )
373 );
374
375 /**
376 * Filters supported file extensions.
377 *
378 * @hook powered_cache_cdn_extensions
379 *
380 * @param {array} $file_extensions Supported file extensions.
381 *
382 * @return {array} New value.
383 * @since 2.2
384 */
385 $file_extensions = apply_filters( 'powered_cache_cdn_extensions', $file_extensions );
386
387 return $file_extensions;
388 }
389
390 }
391
392