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

387 lines 9.6 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 $cdn_url = '//' . $cdn_hostname;
272
273 $optimized_url = str_replace( home_url(), $cdn_url, $optimized_url );
274
275 return $optimized_url;
276 }
277
278
279 /**
280 * try to catch best cdn address to given zone
281 *
282 * @param string $zone keys of Powered_Cache_Admin_Helper::cdn_zones
283 *
284 * @return mixed string | false
285 */
286 public static function get_best_possible_cdn_host( $zone = 'all' ) {
287 global $powered_cache_cdn_addresses;
288
289 if ( ! isset( $powered_cache_cdn_addresses ) ) {
290 $powered_cache_cdn_addresses = cdn_addresses();
291 }
292
293 if ( isset( $powered_cache_cdn_addresses[ $zone ] ) && is_array( $powered_cache_cdn_addresses[ $zone ] ) ) {
294 // if we have multiple host for the same resource get randomly
295 $random_key = array_rand( $powered_cache_cdn_addresses[ $zone ] );
296
297 return $powered_cache_cdn_addresses[ $zone ][ $random_key ];
298 }
299
300 // fallback to primary host
301 if ( isset( $powered_cache_cdn_addresses['all'] ) && is_array( $powered_cache_cdn_addresses['all'] ) ) {
302 $random_key = array_rand( $powered_cache_cdn_addresses['all'] );
303
304 return $powered_cache_cdn_addresses['all'][ $random_key ];
305 }
306
307 return false;
308 }
309
310 /**
311 * Get CDN zone by given extension.
312 *
313 * @param string $ext File extensions. Eg: .jpg, .gif, .mp3...
314 *
315 * @return string
316 * @since 2.2
317 */
318 private static function get_zone_by_ext( $ext ) {
319 $zone = 'all';
320
321 /* documented in get_file_extensions */
322 $image_extensions = apply_filters( 'powered_cache_cdn_image_extensions', array( 'jpg', 'jpeg', 'gif', 'png', 'bmp', 'ico', 'webp', 'avif', 'svg' ) );
323 $image_extensions = array_map(
324 function ( $ext ) {
325 return '.' . $ext;
326 },
327 $image_extensions
328 );
329
330 if ( in_array( $ext, $image_extensions, true ) ) {
331 $zone = 'image';
332 } elseif ( '.css' === $ext ) {
333 $zone = 'css';
334 } elseif ( '.js' === $ext ) {
335 $zone = 'js';
336 }
337
338 return $zone;
339 }
340
341 /**
342 * Get the list of supported file extensions for CDN integration.
343 *
344 * @return array|mixed|void
345 * @since 2.2
346 */
347 public static function get_file_extensions() {
348 /**
349 * Filters supported image extensions.
350 *
351 * @hook powered_cache_cdn_image_extensions
352 *
353 * @param {array} $image_extensions Supported image extensions.
354 *
355 * @return {array} New value.
356 * @deprecated since 2.2. Use powered_cache_cdn_extensions instead.
357 * @since 1.0
358 */
359 $image_extensions = apply_filters( 'powered_cache_cdn_image_extensions', array( 'jpg', 'jpeg', 'gif', 'png', 'bmp', 'ico', 'webp', 'avif', 'svg' ) );
360
361 $extensions = [ 'css', 'js', 'pdf', 'mp3', 'mp4', 'woff2', 'woff', 'ttf', 'otf' ];
362
363 $file_extensions = array_map(
364 function ( $ext ) {
365 return '.' . $ext;
366 },
367 array_merge( $image_extensions, $extensions )
368 );
369
370 /**
371 * Filters supported file extensions.
372 *
373 * @hook powered_cache_cdn_extensions
374 *
375 * @param {array} $file_extensions Supported file extensions.
376 *
377 * @return {array} New value.
378 * @since 2.2
379 */
380 $file_extensions = apply_filters( 'powered_cache_cdn_extensions', $file_extensions );
381
382 return $file_extensions;
383 }
384
385 }
386
387