PluginProbe ʕ •ᴥ•ʔ
Kubio AI Page Builder / trunk
Kubio AI Page Builder vtrunk
2.8.1 trunk 1.0.0 1.0.1 1.1.0 1.2.0 1.2.1 1.2.2 1.2.3 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.4.3 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.7.0 1.7.1 1.7.2 1.7.3 1.8.0 1.8.1 1.8.2 1.9.0 2.0.0 2.1.1 2.1.2 2.1.3 2.2.0 2.2.3 2.2.4 2.2.5 2.3.0 2.3.1 2.3.3 2.3.4 2.4.0 2.4.1 2.4.2 2.4.3 2.4.5 2.5.0 2.5.1 2.5.2 2.5.3 2.6.0 2.6.1 2.6.2 2.6.3 2.6.5 2.6.6 2.6.7 2.7.0 2.7.1 2.7.2 2.7.3 2.8.0
kubio / lib / src / GoogleFontsLocalLoader.php
kubio / lib / src Last commit date
CLI 1 year ago Core 1 month ago DemoSites 1 month ago AssetsDependencyInjector.php 1 year ago Config.php 1 year ago FileLog.php 1 year ago Flags.php 1 year ago GoogleFontsLocalLoader.php 1 year ago GutenbergControls.php 1 year ago Migrations.php 1 year ago NotificationsManager.php 1 month ago PluginsManager.php 2 years ago
GoogleFontsLocalLoader.php
513 lines
1 <?php
2
3 namespace Kubio;
4
5 use IlluminateAgnostic\Arr\Support\Arr;
6 use Kubio\Core\StyleManager\Utils as StyleManagerUtils;
7 use Kubio\Core\Registry;
8 use Kubio\Core\Utils;
9
10 class GoogleFontsLocalLoader {
11
12 private static $instance = null;
13
14 private $queries_transient = 'kubio_local_google_queries_transient';
15 private $font_file_action = 'kubio_get_google_font_file';
16 private $fonts_css_action = 'kubio_get_google_font_css';
17
18 private $uploads_dir = 'kubio-google-fonts-cache';
19
20 private $google_css_url = 'https://fonts.googleapis.com/css';
21 private $google_font_url = 'https://fonts.gstatic.com/s';
22 private $user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:97.0) Gecko/20100101 Firefox/97.0';
23
24 private $local_fonts_dir;
25 private $local_fonts_url;
26
27
28 /**
29 *
30 * @return GoogleFontsLocalLoader
31 */
32 public static function getInstance() {
33 if ( ! static::$instance ) {
34 static::$instance = new static();
35 }
36
37 return static::$instance;
38 }
39
40 public function __construct() {
41
42 $upload_dir = wp_upload_dir();
43 $upload_path = untrailingslashit( $upload_dir['basedir'] );
44
45 $this->local_fonts_dir = "{$upload_path}/{$this->uploads_dir}";
46 $this->local_fonts_url = "{$upload_dir['baseurl']}/{$this->uploads_dir}";
47
48 if ( ! file_exists( $this->local_fonts_dir ) ) {
49 wp_mkdir_p( $this->local_fonts_dir );
50 }
51 }
52
53
54 public function resolveFontsCSS() {
55 // phpcs:ignore WordPress.Security.NonceVerification
56 $action = Arr::get( $_REQUEST, 'action' );
57
58 if ( $action !== $this->fonts_css_action ) {
59 return;
60 }
61
62 header( 'Content-type: text/css' );
63 header( 'Cache-control: public' );
64
65 // phpcs:ignore WordPress.Security.NonceVerification
66 $key = sanitize_key( Arr::get( $_REQUEST, 'key', '' ) );
67 $cached = $this->getCachedDataByKey( $key );
68
69 if ( ! $cached ) {
70 die( '' );
71 }
72
73 $query = Arr::get( $cached, 'query' );
74 $css = Arr::get( $cached, 'css' );
75
76 if ( ! $css ) {
77 $css = $this->getCSS( $query );
78 $this->cacheQueryCSS( $query, $css );
79 }
80
81 $css = $this->replacePlaceholdersWithLocalCSS( $css );
82
83 if ( Utils::isDebug() ) {
84 $css = "/* {$this->google_css_url}?family={$query} */\n\n{$css}";
85 }
86
87 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
88 die( $css );
89 }
90
91 private function getCSS( $query, $replace_urls = true ) {
92 $fonts_url = add_query_arg(
93 array(
94 'family' => urlencode( $query ),
95 'display' => 'swap',
96 ),
97 $this->google_css_url
98 );
99
100 $response = wp_remote_get(
101 $fonts_url,
102 array(
103 'user-agent' => $this->user_agent,
104 )
105 );
106
107 if ( is_wp_error( $response ) ) {
108 return null;
109 }
110
111 if ( wp_remote_retrieve_response_code( $response ) !== 200 ) {
112 return null;
113 }
114
115 $css = wp_remote_retrieve_body( $response );
116
117 if ( $replace_urls ) {
118 $css = $this->replaceGoogleURLS( $css );
119 }
120
121 return $css;
122 }
123
124 private function replaceGoogleURLS( $css ) {
125
126 $google_font_url = $this->google_font_url;
127 $css = preg_replace_callback(
128 '#url\((.*?)\)#',
129 function ( $matches ) use ( $google_font_url ) {
130 $url = str_replace( $google_font_url, '', Arr::get( $matches, 1, '' ) );
131
132 return "url({{{{$url}}}})";
133 },
134 $css
135 );
136
137 return $css;
138 }
139
140 public function getLocalFontFilePath( $font_file ) {
141 $file_key = md5( $font_file );
142 return "{$this->local_fonts_dir}/{$file_key}.woff2";
143 }
144
145 public function getLocalFontFileURL( $font_file ) {
146 $file_key = md5( $font_file );
147 return "{$this->local_fonts_url}/{$file_key}.woff2";
148 }
149
150 public function saveFontContentToLocalFile( $font_file, $content ) {
151 $path = $this->getLocalFontFilePath( $font_file );
152 return file_put_contents( $path, $content );
153 }
154
155 public function localFontFileExists( $font_file ) {
156 return file_exists( $this->getLocalFontFilePath( $font_file ) );
157 }
158
159 private function replacePlaceholdersWithLocalCSS( $css ) {
160 $self = $this;
161 $action = $this->font_file_action;
162
163 $css = preg_replace_callback(
164 '#url\(\{\{\{(.*?)\}\}\}\)#',
165 function ( $matches ) use ( $self, $action ) {
166 $font_file = Arr::get( $matches, 1, '' );
167
168 if ( $self->localFontFileExists( $font_file ) ) {
169
170 $url = $self->getLocalFontFileURL( $font_file );
171 } else {
172 $url = add_query_arg(
173 array(
174 'font' => urlencode( $font_file ),
175 'action' => $action,
176 'security' => $self->createSecurityKey( "{$action}_{$font_file}" ),
177 ),
178 admin_url( 'admin-ajax.php' )
179 );
180 }
181
182 return "url({$url})";
183 },
184 $css
185 );
186
187 return $css;
188 }
189
190 public function getCachedDataByKey( $key ) {
191 $transient = get_transient( $this->queries_transient );
192 if ( ! is_array( $transient ) ) {
193 return null;
194 }
195 return Arr::get( $transient, $key );
196 }
197
198 public function getCachedQueryData( $query ) {
199 return $this->getCachedDataByKey( md5( $query ) );
200 }
201
202 public function cacheQueryCSS( $query, $css ) {
203
204 $transient = get_transient( $this->queries_transient );
205 if ( ! is_array( $transient ) ) {
206 $transient = array();
207 }
208
209 Arr::set(
210 $transient,
211 md5( $query ),
212 array(
213 'query' => $query,
214 'css' => $css,
215 )
216 );
217
218 set_transient( $this->queries_transient, $transient );
219 }
220
221 public function addQueryToCache( $query ) {
222 $transient = get_transient( $this->queries_transient );
223 if ( ! is_array( $transient ) ) {
224 $transient = array();
225 }
226
227 $key = md5( $query );
228
229 if ( isset( $transient[ $key ] ) ) {
230 return;
231 }
232
233 Arr::set(
234 $transient,
235 $key,
236 array(
237 'query' => $query,
238 )
239 );
240 set_transient( $this->queries_transient, $transient );
241 }
242
243
244 public function enqueueFonts( $query ) {
245 $cached = $this->getCachedQueryData( $query );
246 if ( ! $cached ) {
247 $this->addQueryToCache(
248 $query,
249 array(
250 'query' => $query,
251 )
252 );
253 }
254
255 wp_enqueue_style(
256 'kubio-local-google-fonts',
257 add_query_arg(
258 array(
259 'action' => $this->fonts_css_action,
260 'key' => md5( $query ),
261 ),
262 site_url()
263 ),
264 array(),
265 md5( $query )
266 );
267 }
268
269 public function getFontsQuery( $withGeneralSettings = true ) {
270
271 $fonts = array();
272
273 if ( $withGeneralSettings ) {
274 // get global google fonts
275 $fonts = \kubio_get_global_data( 'fonts.google', array() );
276 }
277
278 // add current rendered fonts variants
279 $rendered_fonts = Registry::getInstance()->getRenderedFonts();
280 foreach ( $rendered_fonts as $family => $variants ) {
281 $fonts[] = array(
282 'family' => $family,
283 'variants' => $variants,
284 );
285 }
286
287 $fonts = apply_filters( 'kubio/google_fonts', $fonts );
288
289 if ( ! count( $fonts ) ) {
290 return null;
291 }
292
293 // merge fonts by family
294 $mapped_fonts = array();
295 foreach ( $fonts as $font_data ) {
296 $family = $font_data['family'];
297 $mapped_fonts[ $family ] = isset( $mapped_fonts[ $family ] ) ? $mapped_fonts[ $family ] : array();
298 $mapped_fonts[ $family ] = array_merge( $mapped_fonts[ $family ], $font_data['variants'] );
299
300 }
301
302 // build fonts query
303 $groups = array();
304 foreach ( $mapped_fonts as $family => $weights ) {
305
306 // add the default if necessary 400 and normailize weights array - ensure proper caching by sorting the weights and removing duplicates
307 $groups[] = $family . ':' . implode( ',', StyleManagerUtils::normalizeFontWeights( $weights ) );
308 }
309 $fonts_query = implode( '|', $groups );
310
311 return $fonts_query;
312 }
313
314 public function getFontsMap( $query, $subset = 'latin' ) {
315
316 $css = $this->getCSS( $query, false );
317
318 if ( $subset === 'all' ) {
319 $subset_regex = '/\/\*([^*\/]*)\*\//i';
320 preg_match_all( $subset_regex, $css, $matches, PREG_SET_ORDER );
321 $subsets_list = array();
322 foreach ( $matches as $match ) {
323 $current_subset = trim( $match[1] );
324 if ( ! in_array( $current_subset, $subsets_list ) ) {
325 $subsets_list[] = $current_subset;
326 }
327 }
328 $all_fonts = array();
329 foreach ( $subsets_list as $subset_item ) {
330 $fonts = $this->getFontsMap( $query, $subset_item );
331 $all_fonts = array_merge( $all_fonts, $fonts );
332 }
333
334 //sorts fonts faces
335 usort(
336 $all_fonts,
337 function ( $a, $b ) {
338 return array( $a['font-family'], $a['font-style'], $a['font-weight'], $a['subset'] )
339 <=>
340 array( $b['font-family'], $b['font-style'], $b['font-weight'], $b['subset'] );
341 }
342 );
343 return $all_fonts;
344 }
345
346 // prepare subset
347 $css = preg_replace( '#/\*\s+?(' . $subset . ")\s+?\*/(.*\n?)@font-face#", 'is_subset_match', $css );
348
349 // remove comments
350 $css = preg_replace( '#/\*(.*?)\*/#', '', $css );
351 $css = preg_replace( '#format\((.*?)\)#', '', $css );
352 $css = preg_replace( '#url\(https://(.*?)\)#', '$1', $css );
353
354 $re = '/is_subset_match.*{\K[^}]*(?=})/';
355 preg_match_all( $re, $css, $matches, PREG_SET_ORDER );
356
357 $parsed = array();
358 $keys = array( 'font-family', 'src', 'font-style', 'font-weight', 'unicode-range' );
359 if ( $matches ) {
360
361 foreach ( $matches as $k => $ff ) {
362
363 $css = $ff[0];
364 $attrs = explode( ';', $css );
365
366 $props = array();
367 foreach ( $attrs as $attr ) {
368 if ( strlen( trim( $attr ) ) > 0 ) {
369 $pair = explode( ':', trim( $attr ) );
370 if ( in_array( $pair[0], $keys ) ) {
371 $value = trim( $pair[1] );
372
373 if ( $pair[0] === 'font-family' ) {
374 $value = str_replace( "'", '', $value );
375 }
376
377 if ( $pair[0] === 'font-weight' ) {
378 $value = intval( $value );
379 }
380
381 if ( $pair[0] === 'src' ) {
382 $value = "https://{$value}";
383 }
384
385 $props[ trim( $pair[0] ) ] = $value;
386 }
387 }
388 }
389 $props['subset'] = $subset;
390 $parsed[ $k ] = $props;
391 }
392 }
393
394 return $parsed;
395 }
396
397 public function resolveFont() {
398
399 // phpcs:ignore WordPress.Security.NonceVerification
400 $font_file = sanitize_text_field( Arr::get( $_REQUEST, 'font', '' ) );
401 // phpcs:ignore WordPress.Security.NonceVerification
402 $security_key = sanitize_text_field( Arr::get( $_REQUEST, 'security', '' ) );
403
404 $valid_nonce = $this->verifySecurityKey( $security_key, "{$this->font_file_action}_{$font_file}" );
405
406 if ( ! $valid_nonce ) {
407 wp_die( esc_html( 'Forbidden', 'kubio' ), 403 );
408 }
409
410 $content = $this->resolveFontFileContent( $font_file );
411
412 if ( is_wp_error( $content ) ) {
413 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
414 wp_die( $content, 404 );
415 }
416
417 $this_year = strtotime( gmdate( 'Y' ) . '-01-01' );
418 header( 'Content-type: font/woff2' );
419 header( 'Cache-control: public' );
420 header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', $this_year ) . ' GMT' );
421 header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', $this_year + YEAR_IN_SECONDS ) . ' GMT' );
422 header( 'Etag: ' . md5( base64_encode( $content ) ) );
423
424 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
425 die( $content );
426 }
427
428 private function resolveFontFileContent( $font_file ) {
429 if ( $this->localFontFileExists( $font_file ) ) {
430 return file_get_contents( $this->getLocalFontFilePath( $font_file ) );
431 }
432
433 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable
434 if ( ! is_writable( $this->local_fonts_dir ) ) {
435 return new \WP_Error( 'folder_not_writable' );
436 }
437
438 $google_font_url = "{$this->google_font_url}/{$font_file}";
439
440 $reponse = wp_remote_get( $google_font_url );
441 if ( is_wp_error( $reponse ) ) {
442 return new \WP_Error( 'could_not_retrieve_url' );
443 }
444
445 $content = wp_remote_retrieve_body( $reponse );
446
447 $this->saveFontContentToLocalFile( $font_file, $content );
448
449 return $content;
450 }
451
452 private function getSecuritySalt() {
453 if ( defined( 'NONCE_KEY' ) ) {
454 return NONCE_KEY;
455 }
456
457 if ( define( 'SECURE_AUTH_KEY' ) ) {
458 return SECURE_AUTH_KEY;
459 }
460
461 if ( define( 'AUTH_KEY' ) ) {
462 return AUTH_KEY;
463 }
464
465 if ( define( 'SECURE_AUTH_SALT' ) ) {
466 return SECURE_AUTH_SALT;
467 }
468
469 if ( define( 'AUTH_SALT' ) ) {
470 return AUTH_SALT;
471 }
472
473 $pro_activation_time = Flags::get( 'kubio_pro_activation_time' );
474
475 if ( $pro_activation_time ) {
476 return $pro_activation_time;
477 }
478
479 $activation_time = Flags::get( 'kubio_activation_time' );
480
481 if ( $activation_time ) {
482 return $activation_time;
483 }
484
485 return uniqid( time() );
486 }
487
488 public function createSecurityKey( $action ) {
489 return wp_hash( $this->getSecuritySalt() . '|' . $action );
490 }
491
492 public function verifySecurityKey( $nonce, $action ) {
493 return $nonce === $this->createSecurityKey( $action );
494 }
495
496
497 public function addAdminAjaxActions() {
498 add_action( "wp_ajax_{$this->font_file_action}", array( $this, 'resolveFont' ) );
499 add_action( "wp_ajax_nopriv_{$this->font_file_action}", array( $this, 'resolveFont' ) );
500
501 add_action( 'plugins_loaded', array( $this, 'resolveFontsCSS' ) );
502 }
503
504 public static function enqueuLocalGoogleFonts( $fonts_query ) {
505 return GoogleFontsLocalLoader::getInstance()->enqueueFonts( $fonts_query );
506 }
507
508
509 public static function registerFontResolver() {
510 return GoogleFontsLocalLoader::getInstance()->addAdminAjaxActions();
511 }
512 }
513