PluginProbe
BerqWP – All-In-One Optimization for Core Web Vitals, Cache, CDN, Images, CSS & JavaScript / 4.1.18
BerqWP – All-In-One Optimization for Core Web Vitals, Cache, CDN, Images, CSS & JavaScript v4.1.18
4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.14 4.1.13 4.1.12 4.1.11 4.1.10 4.0.30 4.0.29 4.0.28 4.0.27 4.0.26 4.0.24 4.0.25 4.0.23 4.0.22 4.0.21 4.0.19 4.0.18 4.0.17 4.0.16 1.9.3 All 173 releases
searchpro / inc / queue / class-berqUpload.php

class-berqUpload.php in BerqWP – All-In-One Optimization for Core Web Vitals, Cache, CDN, Images, CSS & JavaScript 4.1.18, at inc/queue/class-berqUpload.php

1,595 lines 49.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 use BerqWP\Cache;
4 use BerqWP_Deps\voku\helper\HtmlDomParser;
5
6 class berqUpload
7 {
8 static $resources = [];
9
10 public static function process_page($url, $html = '')
11 {
12 global $berq_log;
13
14 self::$resources = [];
15
16 $berq_log->info("Processing $url");
17
18 if (empty($html)) {
19 $start = microtime(true);
20 $response = wp_remote_get($url . '?berqwpnocache=' . time(), [
21 // 'sslverify' => false,
22 // 'httpversion' => '2.0',
23 // 'cookies' => [],
24 // 'headers' => [
25 // 'User-Agent' => 'Mozilla/5.0 (BerqWP)',
26 // 'Cache-Control' => 'no-cache, no-store, must-revalidate',
27 // 'Pragma' => 'no-cache',
28 // ],
29 'sslverify' => false,
30 'httpversion' => '2.0',
31 'timeout' => 30
32 ]);
33
34 $end = microtime(true);
35 $timeMs = ($end - $start) * 1000;
36 $statusCode = wp_remote_retrieve_response_code($response);
37
38 if (is_wp_error($response) || $statusCode !== 200) {
39 $berq_log->info("failed to download page html, status code: $statusCode");
40 return;
41 }
42
43 $berq_log->info("html download $timeMs ms");
44
45 $html = wp_remote_retrieve_body($response);
46
47 if (empty($html)) {
48 self::queue_remove_page($url);
49
50 return [
51 "success" => true
52 ];
53 }
54
55 $html = preg_replace('/\?berqwpnocache=\d+&/', '?', $html);
56 $html = preg_replace('/\?berqwpnocache=\d+/', '', $html);
57 $html = preg_replace('/berqwpnocache%3D\d+(%26)?/', '', $html);
58 }
59
60 // Parse HTML once and reuse the DOM across all asset extraction methods
61 $dom = HtmlDomParser::str_get_html($html);
62
63 $start = microtime(true);
64 self::process_css($dom);
65 $end = microtime(true);
66 $timeMs = ($end - $start) * 1000;
67 $berq_log->info("process css $timeMs ms");
68
69 $start = microtime(true);
70 self::process_js($dom);
71 $end = microtime(true);
72 $timeMs = ($end - $start) * 1000;
73 $berq_log->info("process js $timeMs ms");
74
75 $start = microtime(true);
76 self::process_images($dom);
77 $end = microtime(true);
78 $timeMs = ($end - $start) * 1000;
79 $berq_log->info("process images $timeMs ms");
80
81 $start = microtime(true);
82 self::proccess_inline_css($dom);
83 $end = microtime(true);
84 $timeMs = ($end - $start) * 1000;
85 $berq_log->info("process inline css $timeMs ms");
86
87 // $start = microtime(true);
88 // self::proccess_external();
89 // $end = microtime(true);
90 // $timeMs = ($end - $start) * 1000;
91 // $berq_log->info("process external 1 $timeMs ms");
92
93 $start = microtime(true);
94 self::proccess_css_ref();
95 $end = microtime(true);
96 $timeMs = ($end - $start) * 1000;
97 $berq_log->info("process css ref $timeMs ms");
98
99 $start = microtime(true);
100 self::proccess_css_import();
101 $end = microtime(true);
102 $timeMs = ($end - $start) * 1000;
103 $berq_log->info("process css import $timeMs ms");
104
105 // $start = microtime(true);
106 // self::proccess_external();
107 // $end = microtime(true);
108 // $timeMs = ($end - $start) * 1000;
109 // $berq_log->info("process external 2 $timeMs ms");
110
111 // $compressed_assets = array_map(function ($asset) {
112
113 // if (!empty($asset['content'])) {
114 // $asset['content'] = base64_encode(gzencode($asset['content']));
115 // }
116
117 // return $asset;
118 // }, self::$resources);
119
120 // free memory
121 $dom->clear();
122 unset($dom);
123
124 // add mime type in resources
125 self::$resources = array_map(function ($asset) {
126
127 if ($asset['content'] !== null) {
128 unset($asset['content']);
129 }
130
131 if (!empty($asset['path'])) {
132 // // detect mime
133 // $finfo = finfo_open(FILEINFO_MIME_TYPE);
134 // $mime = finfo_file($finfo, $asset['path']);
135 // finfo_close($finfo);
136
137 $mime = self::mime_from_ext(self::get_extension($asset['url']));
138
139 $asset['type'] = $mime;
140 // $asset['url'] = urldecode($asset['url']);
141 }
142
143 // $asset['url'] = self::clean_url($asset['url']);
144
145 return $asset;
146 }, self::$resources);
147
148 // remove assets that doesn't exist or has invalid file path
149 self::$resources = array_filter(self::$resources, function ($asset) {
150 return !empty($asset['path']) && is_file($asset['path']) && is_readable($asset['path']);
151 });
152
153 // Pre-compute SHA-256 hashes so we don't hash the same file multiple times
154 self::$resources = array_map(function ($asset) {
155 $asset['hash'] = hash_file('sha256', $asset['path']);
156 return $asset;
157 }, self::$resources);
158
159 // Filter out already-uploaded assets
160 $uploaded_assets = get_option('berqwp_uploaded_assets', []);
161
162 self::$resources = array_filter(self::$resources, function ($asset) use ($uploaded_assets) {
163
164 // return new/outdated assets for the upload
165 return !self::asset_upload_verified( $asset, $uploaded_assets );
166
167 });
168
169 // Prepare metadata
170 $meta = [
171 'page_url' => $url,
172 'assets' => array_map(fn($a) => ['url' => $a['url']], self::$resources),
173 ];
174
175 // echo print_r(json_decode(json_encode($meta), true), true);
176 // exit;
177
178 $parsed_url = wp_parse_url(get_site_url());
179 $domain = $parsed_url['host'];
180 $queue_failed = null;
181
182 if (empty(self::$resources)) {
183
184 $start = microtime(true);
185 $meta = [
186 'assets' => [],
187 ];
188
189 $body = [
190 'action' => 'prepare_assets',
191 'meta' => json_encode($meta),
192 'domain' => $domain,
193 ];
194
195 $gzip_html = gzencode($html);
196
197 if ($gzip_html === false) {
198 throw new Error("GZip encoding failed");
199 }
200
201 $body['html'] = base64_encode($gzip_html);
202 $post_data = berqwp_get_page_params($url);
203
204 if (self::is_forced($url)) {
205 $post_data['force'] = 1;
206 }
207
208 $body['params'] = json_encode($post_data);
209
210 $queue_failed = self::handle_upload($body, [], true);
211
212 $end = microtime(true);
213 $timeMs = ($end - $start) * 1000;
214 $berq_log->info("page queue 1 $timeMs ms");
215
216 } else {
217
218 $start = microtime(true);
219 $chunks = array_chunk(self::$resources, ini_get('max_file_uploads') ?? 15);
220
221 foreach ($chunks as $index => $chunk) {
222
223 $meta = [
224 'assets' => array_map(fn($a) => ['url' => $a['url'], 'hash' => $a['hash']], $chunk),
225 ];
226
227 $is_last_chunk = $index === count($chunks) - 1;
228
229 $body = [
230 'action' => 'prepare_assets',
231 'meta' => json_encode($meta),
232 'domain' => $domain,
233 ];
234
235 try {
236
237 if ($is_last_chunk) {
238
239 global $berq_log;
240 $berq_log->info("Processing last chunk");
241
242 $gzip_html = gzencode($html);
243
244 if ($gzip_html === false) {
245 throw new Error("GZip encoding failed");
246 }
247
248 $body['html'] = base64_encode($gzip_html);
249 $post_data = berqwp_get_page_params($url);
250
251 if (self::is_forced($url)) {
252 $post_data['force'] = 1;
253 }
254
255 $body['params'] = json_encode($post_data);
256 }
257
258 // Attach files
259 foreach ($chunk as $i => $asset) {
260 try {
261 $body["asset_$i"] = curl_file_create($asset['path'], $asset['type'], basename($asset['path']));
262 $berq_log->info("Successfully created curl file: " . $asset['url']);
263
264 } catch (Exception $e) {
265
266 $berq_log->info("Curl file create failed: " . $asset['url'] . $e);
267
268 } catch (Throwable $e) {
269
270 $berq_log->info("Curl file create failed: " . $asset['url'] . $e);
271
272 }
273 }
274
275 } catch (Exception $e) {
276
277 $berq_log->info("Chunk processing failed, continuing");
278
279 } catch (Throwable $e) {
280 $berq_log->info("Chunk processing failed, continuing");
281 }
282
283 $upload_result = self::handle_upload($body, $chunk, $is_last_chunk);
284
285 if ($is_last_chunk) {
286 $queue_failed = $upload_result;
287 }
288
289 unset($chunk);
290 }
291
292 self::$resources = [];
293
294 $end = microtime(true);
295 $timeMs = ($end - $start) * 1000;
296 $berq_log->info("page queue 2 $timeMs ms");
297 }
298
299 if ($queue_failed === false) {
300 global $berq_log;
301 $berq_log->info("Upload failed, adding back to queue");
302 return ["success" => false];
303 }
304
305 return [
306 "success" => true
307 ];
308 }
309
310 static function asset_upload_verified($asset, $uploaded_assets) {
311
312 $is_valid = array_filter($uploaded_assets, function ($uploaded_file) use ($asset) {
313 return $uploaded_file['url'] === $asset['url'] && $uploaded_file['hash'] === $asset['hash'] && isset($uploaded_file['verified']) && $uploaded_file['verified'] === true;
314 });
315
316 return !empty($is_valid);
317 }
318
319 public static function is_forced($page_url)
320 {
321 $queue = get_option('berqwp_optimize_queue', []);
322 $key = md5($page_url);
323
324 return !empty($queue[$key]) && !empty($queue[$key]['force']);
325 }
326
327 static function handle_upload($body, $chunk = [], $is_last = false)
328 {
329 global $berq_log;
330
331 $berq_log->info("Uploading chunk with " . count($chunk) . " assets using Guzzle");
332
333 if ($is_last) {
334 // $berq_log->info("Last chunk: " . print_r($chunk, true));
335 }
336
337 // Use scoped Guzzle client
338 $client = new \BerqWP_Deps\GuzzleHttp\Client([
339 'timeout' => 60,
340 'verify' => false, // Disable SSL verification
341 ]);
342
343
344 try {
345 // Build multipart array for Guzzle
346 $multipart = [];
347
348 // Add regular POST fields
349 foreach ($body as $key => $value) {
350
351 if (defined('BERQWP_DEV_MOCKS') && BERQWP_DEV_MOCKS) {
352 continue;
353 }
354
355 if ($value instanceof CURLFile) {
356 continue; // Skip files for now
357 }
358
359 $multipart[] = [
360 'name' => $key,
361 'contents' => $value,
362 ];
363 }
364
365 // Add file uploads
366 foreach ($body as $key => $value) {
367
368 if (defined('BERQWP_DEV_MOCKS') && BERQWP_DEV_MOCKS) {
369 continue;
370 }
371
372 if (!($value instanceof CURLFile)) {
373 continue; // Only process files
374 }
375
376 $file_path = $value->getFilename();
377 $file_name = $value->getPostFilename();
378 $mime_type = $value->getMimeType();
379
380 if (!is_file($file_path)) {
381 $berq_log->warning("File not found: {$file_path}");
382 continue;
383 }
384
385 $fp = fopen($file_path, 'r');
386 if ($fp === false) {
387 $berq_log->warning("Failed to open file: {$file_path}");
388 continue;
389 }
390
391 $multipart[] = [
392 'name' => $key,
393 'contents' => $fp,
394 'filename' => $file_name,
395 'headers' => [
396 'Content-Type' => $mime_type,
397 ],
398 ];
399 }
400
401 if (defined('BERQWP_DEV_MOCKS') && BERQWP_DEV_MOCKS && $is_last) {
402 sleep(10);
403 $gz_html = base64_decode( $body['html'] );
404 $params = json_decode($body['params'], true);
405 $page_url = $params['page_url'];
406 $mocks_dir = WP_CONTENT_DIR . '/mocks/';
407 $cache_filename = $mocks_dir . md5($page_url) . ".gz";
408 $download_queue_option = "berqwp_mock_photon_download_queue";
409 $download_queue = get_option($download_queue_option, []);
410 $file_url = WP_CONTENT_URL . "/mocks/" . md5($page_url) . ".gz";
411
412 $file_url = str_replace("localhost:9000", "host.docker.internal:9000", $file_url);
413
414 wp_mkdir_p($mocks_dir);
415
416 $download_queue[md5($page_url)] = [
417 'url' => $page_url,
418 'html' => $file_url
419 ];
420
421 update_option($download_queue_option, $download_queue);
422
423 file_put_contents($cache_filename, $gz_html);
424
425 return true;
426 }
427
428 $response = $client->request('POST', 'https://boost.berqwp.com/photon/', [
429 'multipart' => $multipart,
430 ]);
431
432 $http_code = $response->getStatusCode();
433 $response_body = $response->getBody()->getContents();
434 $json = json_decode($response_body, true);
435
436 if ($json === null) {
437 $berq_log->info("Response body JSON failed: " . $response_body);
438 }
439
440 $berq_log->info("Upload completed with HTTP code: " . $http_code);
441
442 // Update uploaded assets on success
443 if ($http_code >= 200 && $http_code < 300 && !empty($chunk) && isset($json['received'])) {
444 $uploaded_assets = get_option('berqwp_uploaded_assets', []);
445 $received_files = $json['received'];
446
447 $berq_log->info("Successfully uploaded files: " . print_r($received_files, true));
448
449 // Create associative array with URL as key for deduplication
450 $assets_by_url = [];
451 foreach ($uploaded_assets as $asset) {
452 if (!empty($asset['url'])) {
453 $assets_by_url[$asset['url']] = $asset;
454 }
455 }
456
457 // Add/update new assets (newer entries overwrite older ones)
458 foreach ($chunk as $asset) {
459 if (!empty($asset['url']) && !empty($asset['hash']) && in_array($asset['url'], $received_files)) {
460 $assets_by_url[$asset['url']] = [
461 'url' => $asset['url'],
462 'hash' => $asset['hash'],
463 'verified' => true,
464 ];
465 }
466 }
467
468 // Convert back to indexed array and save
469 update_option('berqwp_uploaded_assets', array_values($assets_by_url), false);
470 }
471
472 // Handle last chunk response
473 if ($is_last && isset($json['pending_download'])) {
474 self::handle_download_cache($json['pending_download']);
475 }
476
477 // Add to server queue if last chunk
478 if ($is_last && $http_code >= 200 && $http_code < 300) {
479 $server_queue = get_option('berqwp_server_queue', []);
480 $params = json_decode($body['params'], true);
481 $url = $params['page_url'];
482
483 if (!in_array($url, $server_queue)) {
484 $server_queue[] = $url;
485 }
486
487 update_option('berqwp_server_queue', $server_queue, false);
488 }
489
490 if ($is_last && !is_array($json)) {
491 return false;
492 }
493
494 // Throw exception on HTTP error
495 if ($http_code < 200 || $http_code >= 300) {
496 $berq_log->error('Upload failed with HTTP code: ' . $http_code);
497 throw new \Exception('Could not upload page to BerqWP server');
498 }
499
500 return true;
501 } catch (\BerqWP_Deps\GuzzleHttp\Exception\RequestException $e) {
502 $berq_log->error("Guzzle request failed: " . $e->getMessage());
503 throw new \Exception('Could not upload page to BerqWP server: ' . $e->getMessage());
504 } catch (\Exception $e) {
505 $berq_log->error("Upload failed: " . $e->getMessage());
506 throw $e;
507 } catch (\Throwable $e) {
508 $berq_log->error("Upload failed: " . $e->getMessage());
509 throw $e;
510 }
511 }
512
513 // static function handle_upload($body, $chunk = [], $is_last = false)
514 // {
515 // global $berq_log;
516
517 // $ch = curl_init('https://boost.berqwp.com/photon/');
518 // curl_setopt_array($ch, [
519 // CURLOPT_POST => true,
520 // CURLOPT_POSTFIELDS => $body,
521 // CURLOPT_RETURNTRANSFER => true,
522 // CURLOPT_TIMEOUT => 60,
523 // CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
524 // CURLOPT_TCP_KEEPALIVE => 1,
525
526 // // Disable SSL verification
527 // CURLOPT_SSL_VERIFYPEER => false,
528 // CURLOPT_SSL_VERIFYHOST => false,
529 // ]);
530
531 // $response_body = curl_exec($ch);
532 // $error = curl_error($ch);
533
534 // if (empty($error)) {
535 // $uploaded_assets = get_option('berqwp_uploaded_assets', []);
536 // $new_assets = array_map(function ($asset) {
537 // return [
538 // 'url' => $asset['url'],
539 // 'hash' => hash_file('sha256', $asset['path']),
540 // ];
541 // }, $chunk);
542
543 // update_option('berqwp_uploaded_assets', array_merge($uploaded_assets, $new_assets), false);
544 // }
545
546 // $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
547 // curl_close($ch);
548
549 // // echo $response_body;
550
551 // // if ($index === count($chunks) - 1) {
552 // if ($is_last) {
553 // $json = json_decode($response_body, true);
554 // self::handle_download_cache($json['pending_download']);
555 // }
556
557 // if ($is_last && empty($error)) {
558 // $server_queue = get_option('berqwp_server_queue', []);
559 // $url = json_decode($body['params'], true)['page_url'];
560
561 // if (!in_array($url, $server_queue)) {
562 // $server_queue[] = $url;
563 // }
564
565 // update_option('berqwp_server_queue', $server_queue, false);
566 // }
567
568 // if (!empty($error)) {
569 // $berq_log->info('Failed to upload assets ' . $error . ' code: ' . $http_code);
570 // throw new Exception('Could not upload page to BerqWP server');
571 // }
572 // }
573
574 static function request_pending_cache()
575 {
576 if (defined('BERQWP_DEV_MOCKS') && BERQWP_DEV_MOCKS) {
577 $download_queue_option = "berqwp_mock_photon_download_queue";
578 $download_queue = get_option($download_queue_option, []);
579
580
581 // $download_queue = array_map(function ($item) {
582 // return $item[0];
583 // }, $download_queue);
584
585 $download_queue = array_values($download_queue);
586
587 self::handle_download_cache($download_queue);
588 return;
589 }
590
591 $server_queue = get_option('berqwp_server_queue', []);
592
593 if (empty($server_queue)) {
594 return;
595 }
596
597 $params = berqwp_get_page_params(home_url('/'));
598 $site_id = $params['site_id'];
599 $response = wp_remote_post('https://boost.berqwp.com/photon/', [
600 'body' => [
601 'action' => 'pending_cache_downloads',
602 'site_id' => $site_id,
603 ],
604 'timeout' => 30,
605 'headers' => [
606 'Content-Type' => 'application/x-www-form-urlencoded'
607 ]
608 ]);
609
610 if (is_wp_error($response)) {
611 global $berq_log;
612 $berq_log->info("request pending cache failed");
613 return;
614 }
615
616 $json = json_decode(wp_remote_retrieve_body($response), true);
617
618 self::handle_download_cache($json['pending_download']);
619 }
620
621 static function queue_remove_page($pag_url)
622 {
623 $queue = get_option('berqwp_optimize_queue', []);
624 $key = md5($pag_url);
625 unset($queue[$key]);
626 update_option('berqwp_optimize_queue', $queue, false);
627
628 if (defined('BERQWP_DEV_MOCKS') && BERQWP_DEV_MOCKS) {
629 $download_queue_option = "berqwp_mock_photon_download_queue";
630 $download_queue = get_option($download_queue_option, []);
631 unset($download_queue[$key]);
632 update_option($download_queue_option, $download_queue, false);
633
634 $file_url = WP_CONTENT_DIR . "/mocks/" . $key . ".gz";
635 unlink($file_url);
636 }
637
638 }
639
640 static function handle_download_cache($pending_downloads)
641 {
642 global $berq_log;
643
644 if (!empty($pending_downloads)) {
645 $optimized_pages = [];
646 $params = berqwp_get_page_params(home_url('/'));
647
648 foreach ($pending_downloads as $item) {
649
650 $options = ['timeout' => 60, 'redirection' => 0];
651
652 // if (!empty($parsed['path']) && str_ends_with($parsed['path'], '.gz')) {
653 // $options['headers'] = [
654 // 'Accept-Encoding' => 'identity'
655 // ];
656 // }
657
658 if (berqwp_is_page_url_excluded($item['url'])) {
659 $server_queue = get_option('berqwp_server_queue', []);
660
661 foreach ($server_queue as $index => $url) {
662 if ($url == $item['url']) {
663 unset($server_queue[$index]);
664 }
665 }
666
667 $optimized_pages[] = $item['url'];
668 continue;
669 }
670
671 // if (defined('BERQWP_DEV_MOCKS') && BERQWP_DEV_MOCKS) {
672 // $item['html'] = str_replace("localhost:9000", "host.docker.internal:9000", $item['html']);
673 // }
674
675 // Download the HTML
676 $response = wp_remote_get($item['html'], $options);
677 $response_code = wp_remote_retrieve_response_code($response);
678
679 // Check for errors
680 if (is_wp_error($response) || $response_code !== 200) {
681 continue;
682 }
683
684 $html = wp_remote_retrieve_body($response);
685
686 $parsed = wp_parse_url($item['html']);
687
688 if (!empty($parsed['path']) && str_ends_with($parsed['path'], '.gz')) {
689
690 $html = gzdecode($html);
691 // $tmp = wp_tempnam('cache.gz');
692 // file_put_contents($tmp, $html);
693
694 // $gz = gzopen($tmp, 'rb');
695 // $html = '';
696
697 // while (!gzeof($gz)) {
698 // $html .= gzread($gz, 8192);
699 // }
700
701 // gzclose($gz);
702 // unlink($tmp);
703 }
704
705 // Allow other plugins to modify cache html
706 $html = apply_filters('berqwp_cache_buffer', $html);
707
708 $item['url'] = bwp_canonicalize_page_url($item['url']);
709 $cache = new Cache(null, bwp_get_cache_dir());
710 $cache->store_cache($item['url'], $html);
711
712 if (defined('BERQWP_DEV_MOCKS') && BERQWP_DEV_MOCKS) {
713 self::queue_remove_page($item['url']);
714 }
715
716 $server_queue = get_option('berqwp_server_queue', []);
717
718 foreach ($server_queue as $index => $url) {
719 if ($url == $item['url']) {
720
721 self::queue_remove_page($item['url']);
722
723 unset($server_queue[$index]);
724 }
725 }
726
727 $optimized_pages[] = $item['url'];
728
729 update_option('berqwp_server_queue', $server_queue, false);
730
731 $berq_log->info("Stored cache for " . $item['url']);
732
733 $page_slug = bwp_url_into_path($item['url']);
734 do_action('berqwp_stored_page_cache', $page_slug);
735 }
736
737 $site_id = $params['site_id'];
738 $response = wp_remote_post('https://boost.berqwp.com/photon/', [
739 'body' => [
740 'action' => 'clean_page_cache',
741 'site_id' => $site_id,
742 'downloaded_cache' => $optimized_pages
743 ],
744 'timeout' => 15,
745 'headers' => [
746 'Content-Type' => 'application/x-www-form-urlencoded'
747 ]
748 ]);
749 }
750 }
751
752 static function mime_from_ext($ext)
753 {
754 static $map = [
755 'css' => 'text/css; charset=utf-8',
756 'js' => 'application/javascript; charset=utf-8',
757 'mjs' => 'application/javascript; charset=utf-8',
758 'html' => 'text/html; charset=utf-8',
759 'svg' => 'image/svg+xml',
760 'png' => 'image/png',
761 'jpg' => 'image/jpeg',
762 'jpeg' => 'image/jpeg',
763 'webp' => 'image/webp',
764 'avif' => 'image/avif',
765 'gif' => 'image/gif',
766 'woff' => 'font/woff',
767 'woff2' => 'font/woff2',
768 'ttf' => 'font/ttf',
769 'otf' => 'font/otf',
770 'eot' => 'application/vnd.ms-fontobject',
771 'json' => 'application/json; charset=utf-8',
772 'xml' => 'application/xml; charset=utf-8',
773 'ico' => 'image/x-icon',
774 ];
775
776 return $map[$ext] ?? 'text/plain';
777 }
778
779 static function get_extension($url)
780 {
781 $path = wp_parse_url($url, PHP_URL_PATH);
782 if (!$path) {
783 return '';
784 }
785
786 return strtolower(pathinfo($path, PATHINFO_EXTENSION));
787 }
788
789 static function is_valid_asset($url)
790 {
791 return strpos($url, 'data:') === false;
792 }
793
794 static function get_cache_path($url)
795 {
796 if (!self::has_cache($url)) {
797 return false;
798 }
799
800 $path = bwp_get_static_cache_dir();
801
802 // $file_name = md5(self::clean_url($url)) . '.txt';
803 $file_name = md5($url) . '.txt';
804
805 return $path . $file_name;
806 }
807
808 static function is_valid_cache($url)
809 {
810 $path = bwp_get_static_cache_dir();
811
812 // $file_name = md5(self::clean_url($url)) . '.txt';
813 $file_name = md5($url) . '.txt';
814
815 return is_file($path . $file_name) && (@filemtime($path . $file_name) + 60 * 60 > time());
816 }
817
818 static function has_cache($url)
819 {
820 $path = bwp_get_static_cache_dir();
821
822 // $file_name = md5(self::clean_url($url)) . '.txt';
823 $file_name = md5($url) . '.txt';
824
825 return is_file($path . $file_name);
826 }
827
828 static function get_cache($url)
829 {
830 $path = bwp_get_static_cache_dir();
831
832 // $file_name = md5(self::clean_url($url)) . '.txt';
833 $file_name = md5($url) . '.txt';
834
835 return file_get_contents($path . $file_name);
836 }
837
838 static function cache_external_asset($url, $content)
839 {
840 if (self::is_valid_cache($url)) {
841 return $content;
842 }
843
844 $path = bwp_get_static_cache_dir();
845
846 // if (empty($content)) {
847 // global $berq_log;
848 // $berq_log->info("Empty file content, possibly file doesn't exist: $url");
849 // }
850
851 // $file_name = md5(self::clean_url($url)) . '.txt';
852 $file_name = md5($url) . '.txt';
853
854 file_put_contents($path . $file_name, $content, LOCK_EX);
855
856 return $content;
857 }
858
859 public static function proccess_css_ref()
860 {
861 $css_assets = array_filter(self::$resources, function ($asset) {
862 return $asset['type'] == 'css';
863 });
864
865 foreach ($css_assets as $content) {
866 $urls = self::extractUrlsFromCss($content['content']);
867 $asset_url = $content['url'];
868
869 foreach ($urls as $url) {
870 $url = trim($url);
871 $url = explode('#', $url)[0];
872
873 if (empty($url)) {
874 continue;
875 }
876
877 if (!self::is_valid_asset($url)) {
878 continue;
879 }
880
881 // convert to abs
882 $baseUrl = self::getBaseUrl($asset_url);
883 $url = self::rel2abs($url, $baseUrl);
884
885 // skip is if already exists
886 if (!empty(self::resource_exists($url))) {
887 continue;
888 }
889
890 if (self::is_self_hosted_url($url)) {
891 $file_path = self::url_to_path($url);
892
893 if (empty($file_path) || !is_file($file_path)) {
894
895 self::cache_external_asset($url, '');
896
897 self::$resources[] = [
898 'url' => $url,
899 'path' => self::get_cache_path($url),
900 'type' => 'css_reff',
901 'hosted' => true,
902 'content' => '',
903 'error' => '404',
904 ];
905 continue;
906 }
907
908 $file_content = file_get_contents($file_path);
909
910 self::$resources[] = [
911 'url' => $url,
912 'path' => $file_path,
913 'type' => 'css_reff',
914 'hosted' => true,
915 'content' => $file_content,
916 ];
917
918 continue;
919 }
920
921 self::$resources[] = [
922 'url' => $url,
923 'type' => 'css_reff',
924 'hosted' => false,
925 'content' => null,
926 ];
927 }
928 }
929 }
930 public static function proccess_css_import()
931 {
932 $css_assets = array_filter(self::$resources, function ($asset) {
933 return $asset['type'] == 'css';
934 });
935
936 foreach ($css_assets as $content) {
937
938 if (empty($content['content'])) {
939 continue;
940 }
941
942 preg_match_all('/@import\s*[\'"]([^\'"]+)/', $content['content'], $match);
943 $urls = $match[1];
944 $asset_url = $content['url'];
945
946 foreach ($urls as $url) {
947 $url = trim($url);
948
949 if (empty($url)) {
950 continue;
951 }
952
953 if (!self::is_valid_asset($url)) {
954 continue;
955 }
956
957 // convert to abs
958 $baseUrl = self::getBaseUrl($asset_url);
959 $url = self::rel2abs($url, $baseUrl);
960
961 // skip is if already exists
962 if (!empty(self::resource_exists($url))) {
963 continue;
964 }
965
966 if (self::is_self_hosted_url($url)) {
967 $file_path = self::url_to_path($url);
968
969 if (empty($file_path) || !is_file($file_path)) {
970
971 self::cache_external_asset($url, '');
972
973 self::$resources[] = [
974 'url' => $url,
975 'path' => self::get_cache_path($url),
976 'type' => 'css_import',
977 'hosted' => true,
978 'content' => '',
979 'error' => '404',
980 ];
981 continue;
982 }
983
984 $file_content = file_get_contents($file_path);
985
986 self::$resources[] = [
987 'url' => $url,
988 'path' => $file_path,
989 'type' => 'css_import',
990 'hosted' => true,
991 'content' => $file_content,
992 ];
993
994 continue;
995 }
996
997 self::$resources[] = [
998 'url' => $url,
999 'type' => 'css_reff',
1000 'hosted' => false,
1001 'content' => null,
1002 ];
1003 }
1004 }
1005 }
1006
1007 public static function proccess_external()
1008 {
1009 $external = array_filter(self::$resources, function ($asset) {
1010 return !$asset['hosted'] && $asset['content'] === null;
1011 });
1012
1013 // get cache
1014 $cached = array_filter($external, function ($asset) {
1015 return self::has_cache($asset['url']) && self::is_valid_cache($asset['url']);
1016 });
1017
1018 foreach ($cached as $cached_index => $cached_asset) {
1019 $url = $cached_asset['url'];
1020
1021 foreach (self::$resources as $resource_index => $resource) {
1022 if ($resource['url'] == $url) {
1023 self::$resources[$resource_index]['content'] = self::get_cache($url);
1024 self::$resources[$resource_index]['path'] = self::get_cache_path($url);
1025
1026 $external = array_filter($external, function ($asset) use ($url) {
1027 return $asset['url'] !== $url;
1028 });
1029 break;
1030 }
1031 }
1032 }
1033
1034 $responses = self::download_external_assets($external);
1035
1036 foreach ($responses as $response_index => $response) {
1037 $url = $external[$response_index]['url'];
1038
1039 foreach (self::$resources as $resource_index => $resource) {
1040 if ($resource['url'] == $url) {
1041
1042 self::cache_external_asset($resource['url'], $response);
1043 self::$resources[$resource_index]['content'] = $response;
1044 self::$resources[$resource_index]['path'] = self::get_cache_path($resource['url']);
1045 break;
1046 }
1047 }
1048 }
1049 }
1050
1051 static function process_css($dom = null)
1052 {
1053 if (empty($dom)) {
1054 return;
1055 }
1056
1057 foreach ($dom->find('link[rel=stylesheet], link[as=style]') as $link) {
1058
1059 $href = $link->getAttribute('href');
1060
1061 // convert to abs
1062 $baseUrl = self::getBaseUrl(get_site_url());
1063 $href = self::rel2abs($href, $baseUrl);
1064
1065 // skip is if already exists
1066 if (!empty(self::resource_exists($href))) {
1067 continue;
1068 }
1069
1070 if (!self::is_valid_asset($href)) {
1071 continue;
1072 }
1073
1074 if (self::is_self_hosted_url($href)) {
1075 $css_path = self::url_to_path($href);
1076
1077 if (empty($css_path) || !is_file($css_path)) {
1078
1079 self::cache_external_asset($href, '');
1080
1081 self::$resources[] = [
1082 'url' => $href,
1083 'path' => self::get_cache_path($href),
1084 'type' => 'css',
1085 'hosted' => true,
1086 'content' => '',
1087 'error' => '404',
1088 ];
1089 continue;
1090 }
1091
1092 $local_css = file_get_contents($css_path);
1093
1094 self::$resources[] = [
1095 'url' => $href,
1096 'path' => $css_path,
1097 'type' => 'css',
1098 'hosted' => true,
1099 'content' => $local_css,
1100 ];
1101
1102 continue;
1103 }
1104
1105 self::$resources[] = [
1106 'url' => $href,
1107 'type' => 'css',
1108 'hosted' => false,
1109 'content' => null,
1110 ];
1111 }
1112 }
1113
1114 static function proccess_inline_css($dom = null)
1115 {
1116 if (empty($dom)) {
1117 return;
1118 }
1119
1120 foreach ($dom->find('style') as $style) {
1121
1122 $urls = self::extractUrlsFromCss($style->innertext);
1123
1124 foreach ($urls as $url) {
1125 $url = trim($url);
1126
1127 if (empty($url)) {
1128 continue;
1129 }
1130
1131 if (!self::is_valid_asset($url)) {
1132 continue;
1133 }
1134
1135 // convert to abs
1136 $baseUrl = self::getBaseUrl(get_site_url());
1137 $url = self::rel2abs($url, $baseUrl);
1138
1139 // skip is if already exists
1140 if (!empty(self::resource_exists($url))) {
1141 continue;
1142 }
1143
1144 if (self::is_self_hosted_url($url)) {
1145 $file_path = self::url_to_path($url);
1146
1147 if (empty($file_path) || !is_file($file_path)) {
1148
1149 self::cache_external_asset($url, '');
1150
1151 self::$resources[] = [
1152 'url' => $url,
1153 'path' => self::get_cache_path($url),
1154 'type' => 'inline_css',
1155 'hosted' => true,
1156 'content' => '',
1157 'error' => '404',
1158 ];
1159 continue;
1160 }
1161
1162 $file_content = file_get_contents($file_path);
1163
1164 self::$resources[] = [
1165 'url' => $url,
1166 'path' => $file_path,
1167 'type' => 'inline_css',
1168 'hosted' => true,
1169 'content' => $file_content,
1170 ];
1171
1172 continue;
1173 }
1174
1175 self::$resources[] = [
1176 'url' => $url,
1177 'type' => 'inline_css',
1178 'hosted' => false,
1179 'content' => null,
1180 ];
1181 }
1182 }
1183 }
1184
1185 static function process_js($dom = null)
1186 {
1187 if (empty($dom)) {
1188 return;
1189 }
1190
1191 foreach ($dom->find('script') as $script) {
1192
1193 if (empty($script->getAttribute('src'))) {
1194 continue;
1195 }
1196
1197 $src = $script->getAttribute('src');
1198
1199 // convert to abs
1200 $baseUrl = self::getBaseUrl(get_site_url());
1201 $src = self::rel2abs($src, $baseUrl);
1202
1203 // skip is if already exists
1204 if (!empty(self::resource_exists($src))) {
1205 continue;
1206 }
1207
1208 if (!self::is_valid_asset($src)) {
1209 continue;
1210 }
1211
1212 if (self::is_self_hosted_url($src)) {
1213 $js_path = self::url_to_path($src);
1214
1215 if (empty($js_path) || !is_file($js_path)) {
1216
1217 self::cache_external_asset($src, '');
1218
1219 self::$resources[] = [
1220 'url' => $src,
1221 'path' => self::get_cache_path($src),
1222 'type' => 'js',
1223 'hosted' => true,
1224 'content' => '',
1225 'error' => '404',
1226 ];
1227 continue;
1228 }
1229
1230 // $js = file_get_contents($js_path);
1231
1232 self::$resources[] = [
1233 'url' => $src,
1234 'path' => $js_path,
1235 'type' => 'js',
1236 'hosted' => true,
1237 'content' => '',
1238 ];
1239
1240 continue;
1241 }
1242
1243 self::$resources[] = [
1244 'url' => $src,
1245 'type' => 'js',
1246 'hosted' => false,
1247 'content' => null,
1248 ];
1249 }
1250 }
1251
1252 static function process_images($dom = null)
1253 {
1254 if (empty($dom)) {
1255 return;
1256 }
1257
1258 foreach ($dom->find('img') as $image) {
1259
1260 if (empty($image->getAttribute('src'))) {
1261 continue;
1262 }
1263
1264 $src = $image->getAttribute('src');
1265
1266 if (!empty($image->getAttribute('srcset'))) {
1267 self::process_srcset($image->getAttribute('srcset'));
1268 }
1269
1270 // convert to abs
1271 $baseUrl = self::getBaseUrl(get_site_url());
1272 $src = self::rel2abs($src, $baseUrl);
1273
1274 // skip is if already exists
1275 if (!empty(self::resource_exists($src))) {
1276 continue;
1277 }
1278
1279 if (!self::is_valid_asset($src)) {
1280 continue;
1281 }
1282
1283 if (self::is_self_hosted_url($src)) {
1284 $img_path = self::url_to_path($src);
1285
1286 if (empty($img_path) || !is_file($img_path)) {
1287
1288 self::cache_external_asset($src, '');
1289
1290 self::$resources[] = [
1291 'url' => $src,
1292 'path' => self::get_cache_path($src),
1293 'type' => 'img',
1294 'hosted' => true,
1295 'content' => '',
1296 'error' => '404',
1297 ];
1298 continue;
1299 }
1300
1301 // $img = file_get_contents($img_path);
1302
1303 self::$resources[] = [
1304 'url' => $src,
1305 'path' => $img_path,
1306 'type' => 'img',
1307 'hosted' => true,
1308 'content' => '',
1309 ];
1310
1311 continue;
1312 }
1313
1314 self::$resources[] = [
1315 'url' => $src,
1316 'type' => 'img',
1317 'hosted' => false,
1318 'content' => null,
1319 ];
1320 }
1321 }
1322
1323 static function clean_url($url)
1324 {
1325 $parts = wp_parse_url($url);
1326
1327 if (!$parts) {
1328 return '';
1329 }
1330
1331 $clean = ($parts['scheme'] ?? 'https') . '://';
1332 $clean .= $parts['host'] ?? '';
1333
1334 if (!empty($parts['port'])) {
1335 $clean .= ':' . $parts['port'];
1336 }
1337
1338 $clean .= $parts['path'] ?? '/';
1339
1340 return $clean;
1341 }
1342
1343 static function resource_exists($url)
1344 {
1345
1346 return array_filter(self::$resources, function ($asset) use ($url) {
1347 return $asset['url'] == $url;
1348 });
1349 }
1350
1351 static function process_srcset($srcset)
1352 {
1353
1354 $sources = explode(',', $srcset);
1355
1356 foreach ($sources as $source) {
1357 // Extract URL and remove leading/trailing whitespace
1358 $srcset_img_url = trim(explode(' ', trim($source))[0]);
1359
1360 // convert to abs
1361 $baseUrl = self::getBaseUrl(get_site_url());
1362 $srcset_img_url = self::rel2abs($srcset_img_url, $baseUrl);
1363
1364 // skip is if already exists
1365 if (!empty(self::resource_exists($srcset_img_url))) {
1366 continue;
1367 }
1368
1369 if (!self::is_valid_asset($srcset_img_url)) {
1370 continue;
1371 }
1372
1373 if (self::is_self_hosted_url($srcset_img_url)) {
1374 $img_path = self::url_to_path($srcset_img_url);
1375
1376 if (empty($img_path) || !is_file($img_path)) {
1377
1378 self::cache_external_asset($srcset_img_url, '');
1379
1380 self::$resources[] = [
1381 'url' => $srcset_img_url,
1382 'path' => self::get_cache_path($srcset_img_url),
1383 'type' => 'img',
1384 'hosted' => true,
1385 'content' => '',
1386 'error' => '404',
1387 ];
1388 continue;
1389 }
1390
1391 // $img = file_get_contents($img_path);
1392
1393 self::$resources[] = [
1394 'url' => $srcset_img_url,
1395 'path' => $img_path,
1396 'type' => 'img',
1397 'hosted' => true,
1398 'content' => '',
1399 ];
1400
1401 continue;
1402 }
1403
1404 self::$resources[] = [
1405 'url' => $srcset_img_url,
1406 'type' => 'img',
1407 'hosted' => false,
1408 'content' => null,
1409 ];
1410 }
1411 }
1412
1413 static function is_self_hosted_url($url)
1414 {
1415 if (empty($url)) {
1416 return false;
1417 }
1418
1419 // Relative URLs are self-hosted
1420 if (strpos($url, '//') === false) {
1421 return true;
1422 }
1423
1424 $site_host = wp_parse_url(site_url(), PHP_URL_HOST);
1425 $url_host = wp_parse_url($url, PHP_URL_HOST);
1426
1427 return $site_host && $url_host && strtolower($site_host) === strtolower($url_host);
1428 }
1429
1430 static function url_to_path($url)
1431 {
1432
1433 if (strpos($url, '?') !== false) {
1434 $url = explode('?', $url)[0];
1435 }
1436
1437 if (strpos($url, '#') !== false) {
1438 $url = explode('#', $url)[0];
1439 }
1440
1441 // Handle relative URLs
1442 if (strpos($url, '//') === 0) {
1443 return realpath(ABSPATH . ltrim($url, '/'));
1444 }
1445
1446 $content_url = content_url();
1447 $content_dir = WP_CONTENT_DIR;
1448
1449 if (strpos($url, $content_url) === 0) {
1450 // var_dump(realpath(
1451 // str_replace($content_url, $content_dir, $url)
1452 // ));
1453 return str_replace($content_url, $content_dir, $url);
1454 }
1455
1456 // Fallback for site root files
1457 $site_url = get_site_url('/');
1458
1459 if (strpos($url, $site_url) === 0) {
1460 return realpath(
1461 str_replace($site_url, ABSPATH, $url)
1462 );
1463 }
1464
1465 return false;
1466 }
1467
1468 public static function rel2abs($rel, $base)
1469 {
1470 /* return if already absolute URL */
1471 if (parse_url($rel, PHP_URL_SCHEME) != '')
1472 return $rel;
1473
1474 // Return with base scheme if protocol-relative (starts with //)
1475 if (strpos($rel, '//') === 0) {
1476 $scheme = parse_url($base, PHP_URL_SCHEME) ?: 'https';
1477 return $scheme . ':' . $rel;
1478 }
1479
1480 /* queries and anchors */
1481 if ($rel[0] == '#' || $rel[0] == '?')
1482 return $base . $rel;
1483
1484 /* parse base URL and convert to local variables:
1485 $scheme, $host, $path */
1486 extract(parse_url($base));
1487
1488 /* remove non-directory element from path */
1489 $path = preg_replace('#/[^/]*$#', '', $path);
1490
1491 /* destroy path if relative url points to root */
1492 if ($rel[0] == '/')
1493 $path = '';
1494
1495 /* dirty absolute URL */
1496 $abs = "$host$path/$rel";
1497
1498 /* replace '//' or '/./' or '/foo/../' with '/' */
1499 $re = array('#(/\.?/)#', '#/(?!\.\.)[^/]+/\.\./#');
1500 for ($n = 1; $n > 0; $abs = preg_replace($re, '/', $abs, -1, $n)) {
1501 }
1502
1503 /* absolute URL is ready! */
1504 return $scheme . '://' . $abs;
1505 }
1506
1507 public static function getBaseUrl($fileUrl)
1508 {
1509 // Parse the URL and get its components
1510 $parsedUrl = parse_url($fileUrl);
1511 $scheme = isset($parsedUrl['scheme']) ? $parsedUrl['scheme'] . '://' : '';
1512 $host = isset($parsedUrl['host']) ? $parsedUrl['host'] : '';
1513 $path = isset($parsedUrl['path']) ? $parsedUrl['path'] : '';
1514
1515 // Remove the file name from the path to get the base URL
1516 $basePath = preg_replace('/\/[^\/]+$/', '/', $path);
1517
1518 // Construct the base URL
1519 return $scheme . $host . $basePath;
1520 }
1521
1522 public static function extractUrlsFromCss($cssContent)
1523 {
1524 $urls = [];
1525 $pattern = '/url\((.*?)\)/i';
1526
1527 if (empty($cssContent)) {
1528 return [];
1529 }
1530
1531 preg_match_all($pattern, $cssContent, $matches);
1532
1533 if (!empty($matches[1])) {
1534 foreach ($matches[1] as $match) {
1535 $urls[] = trim($match, '\'" ');
1536 }
1537 }
1538
1539 return $urls;
1540 }
1541
1542 private static function download_external_assets($assets)
1543 {
1544 $results = [];
1545 $chunks = array_chunk($assets, 10, true); // Process 10 at a time
1546
1547 foreach ($chunks as $chunk) {
1548 $multi = curl_multi_init();
1549 $handles = [];
1550
1551 // Initialize cURL handles
1552 foreach ($chunk as $index => $asset) {
1553 $ch = curl_init();
1554 curl_setopt_array($ch, [
1555 CURLOPT_URL => $asset['url'],
1556 CURLOPT_RETURNTRANSFER => true,
1557 CURLOPT_FOLLOWLOCATION => true,
1558 CURLOPT_MAXREDIRS => 3,
1559 CURLOPT_TIMEOUT => 15,
1560 CURLOPT_CONNECTTIMEOUT => 5,
1561 CURLOPT_SSL_VERIFYPEER => false,
1562 CURLOPT_SSL_VERIFYHOST => false,
1563 CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; BWP2/1.0)',
1564 CURLOPT_MAXFILESIZE => 5 * 1024 * 1024, // 5 MB limit per asset
1565 ]);
1566
1567 curl_multi_add_handle($multi, $ch);
1568 $handles[$index] = $ch;
1569 }
1570
1571 // Execute parallel downloads
1572 $running = null;
1573 do {
1574 curl_multi_exec($multi, $running);
1575 curl_multi_select($multi);
1576 } while ($running > 0);
1577
1578 // Collect results
1579 foreach ($handles as $index => $ch) {
1580 $content = curl_multi_getcontent($ch);
1581 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
1582
1583 $results[$index] = ($http_code === 200 && $content !== false) ? $content : false;
1584
1585 curl_multi_remove_handle($multi, $ch);
1586 curl_close($ch);
1587 }
1588
1589 curl_multi_close($multi);
1590 }
1591
1592 return $results;
1593 }
1594 }
1595