PluginProbe
BerqWP – All-In-One Optimization for Core Web Vitals, Cache, CDN, Images, CSS & JavaScript / 4.0.23
BerqWP – All-In-One Optimization for Core Web Vitals, Cache, CDN, Images, CSS & JavaScript v4.0.23
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 1.9.4 1.9.5 1.9.6 All 170 releases
searchpro / inc / queue / class-berqUpload-bak.php

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

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