PluginProbe ʕ •ᴥ•ʔ
LiteSpeed Cache / 7.6.1
LiteSpeed Cache v7.6.1
trunk 1.0.15 1.9.1.1 2.9.9.2 3.6.4 4.6 5.7.0.1 6.5.4 7.0.0.1 7.0.1 7.1 7.2 7.3 7.3.0.1 7.4 7.5 7.5.0.1 7.6 7.6.1 7.6.2 7.7 7.8 7.8.0.1 7.8.1
litespeed-cache / src / ucss.cls.php
litespeed-cache / src Last commit date
cdn 7 months ago data_structure 7 months ago activation.cls.php 7 months ago admin-display.cls.php 7 months ago admin-settings.cls.php 7 months ago admin.cls.php 7 months ago api.cls.php 7 months ago avatar.cls.php 7 months ago base.cls.php 7 months ago cdn.cls.php 7 months ago cloud.cls.php 7 months ago conf.cls.php 7 months ago control.cls.php 7 months ago core.cls.php 7 months ago crawler-map.cls.php 7 months ago crawler.cls.php 7 months ago css.cls.php 7 months ago data.cls.php 7 months ago data.upgrade.func.php 7 months ago db-optm.cls.php 7 months ago debug2.cls.php 7 months ago doc.cls.php 7 months ago error.cls.php 7 months ago esi.cls.php 7 months ago file.cls.php 7 months ago gui.cls.php 7 months ago health.cls.php 7 months ago htaccess.cls.php 7 months ago img-optm.cls.php 7 months ago import.cls.php 7 months ago import.preset.cls.php 7 months ago lang.cls.php 7 months ago localization.cls.php 7 months ago media.cls.php 7 months ago metabox.cls.php 7 months ago object-cache-wp.cls.php 7 months ago object-cache.cls.php 7 months ago object.lib.php 7 months ago optimize.cls.php 7 months ago optimizer.cls.php 7 months ago placeholder.cls.php 7 months ago purge.cls.php 7 months ago report.cls.php 7 months ago rest.cls.php 7 months ago root.cls.php 7 months ago router.cls.php 7 months ago str.cls.php 7 months ago tag.cls.php 7 months ago task.cls.php 7 months ago tool.cls.php 7 months ago ucss.cls.php 7 months ago utility.cls.php 7 months ago vary.cls.php 7 months ago vpi.cls.php 7 months ago
ucss.cls.php
573 lines
1 <?php
2 // phpcs:ignoreFile
3
4 /**
5 * The ucss class.
6 *
7 * @since 5.1
8 */
9
10 namespace LiteSpeed;
11
12 defined('WPINC') || exit();
13
14 class UCSS extends Base {
15
16 const LOG_TAG = '[UCSS]';
17
18 const TYPE_GEN = 'gen';
19 const TYPE_CLEAR_Q = 'clear_q';
20
21 protected $_summary;
22 private $_ucss_whitelist;
23 private $_queue;
24
25 /**
26 * Init
27 *
28 * @since 3.0
29 */
30 public function __construct() {
31 $this->_summary = self::get_summary();
32
33 add_filter('litespeed_ucss_whitelist', array( $this->cls('Data'), 'load_ucss_whitelist' ));
34 }
35
36 /**
37 * Uniform url tag for ucss usage
38 *
39 * @since 4.7
40 */
41 public static function get_url_tag( $request_url = false ) {
42 $url_tag = $request_url;
43 if (is_404()) {
44 $url_tag = '404';
45 } elseif (apply_filters('litespeed_ucss_per_pagetype', false)) {
46 $url_tag = Utility::page_type();
47 self::debug('litespeed_ucss_per_pagetype filter altered url to ' . $url_tag);
48 }
49
50 return $url_tag;
51 }
52
53 /**
54 * Get UCSS path
55 *
56 * @since 4.0
57 */
58 public function load( $request_url, $dry_run = false ) {
59 // Check UCSS URI excludes
60 $ucss_exc = apply_filters('litespeed_ucss_exc', $this->conf(self::O_OPTM_UCSS_EXC));
61 if ($ucss_exc && ($hit = Utility::str_hit_array($request_url, $ucss_exc))) {
62 self::debug('UCSS bypassed due to UCSS URI Exclude setting: ' . $hit);
63 Core::comment('QUIC.cloud UCSS bypassed by setting');
64 return false;
65 }
66
67 $filepath_prefix = $this->_build_filepath_prefix('ucss');
68
69 $url_tag = self::get_url_tag($request_url);
70
71 $vary = $this->cls('Vary')->finalize_full_varies();
72 $filename = $this->cls('Data')->load_url_file($url_tag, $vary, 'ucss');
73 if ($filename) {
74 $static_file = LITESPEED_STATIC_DIR . $filepath_prefix . $filename . '.css';
75
76 if (file_exists($static_file)) {
77 self::debug2('existing ucss ' . $static_file);
78 // Check if is error comment inside only
79 $tmp = File::read($static_file);
80 if (substr($tmp, 0, 2) == '/*' && substr(trim($tmp), -2) == '*/') {
81 self::debug2('existing ucss is error only: ' . $tmp);
82 Core::comment('QUIC.cloud UCSS bypassed due to generation error ❌ ' . $filepath_prefix . $filename . '.css');
83 return false;
84 }
85
86 Core::comment('QUIC.cloud UCSS loaded �
87 ' . $filepath_prefix . $filename . '.css' );
88
89 return $filename . '.css';
90 }
91 }
92
93 if ($dry_run) {
94 return false;
95 }
96
97 Core::comment('QUIC.cloud UCSS in queue');
98
99 $uid = get_current_user_id();
100
101 $ua = $this->_get_ua();
102
103 // Store it for cron
104 $this->_queue = $this->load_queue('ucss');
105
106 if (count($this->_queue) > 500) {
107 self::debug('UCSS Queue is full - 500');
108 return false;
109 }
110
111 $queue_k = (strlen($vary) > 32 ? md5($vary) : $vary) . ' ' . $url_tag;
112 $this->_queue[$queue_k] = array(
113 'url' => apply_filters('litespeed_ucss_url', $request_url),
114 'user_agent' => substr($ua, 0, 200),
115 'is_mobile' => $this->_separate_mobile(),
116 'is_webp' => $this->cls('Media')->webp_support() ? 1 : 0,
117 'uid' => $uid,
118 'vary' => $vary,
119 'url_tag' => $url_tag,
120 ); // Current UA will be used to request
121 $this->save_queue('ucss', $this->_queue);
122 self::debug('Added queue_ucss [url_tag] ' . $url_tag . ' [UA] ' . $ua . ' [vary] ' . $vary . ' [uid] ' . $uid);
123
124 // Prepare cache tag for later purge
125 Tag::add('UCSS.' . md5($queue_k));
126
127 return false;
128 }
129
130 /**
131 * Get User Agent
132 *
133 * @since 5.3
134 */
135 private function _get_ua() {
136 return !empty($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
137 }
138
139 /**
140 * Add rows to q
141 *
142 * @since 5.3
143 */
144 public function add_to_q( $url_files ) {
145 // Store it for cron
146 $this->_queue = $this->load_queue('ucss');
147
148 if (count($this->_queue) > 500) {
149 self::debug('UCSS Queue is full - 500');
150 return false;
151 }
152
153 $ua = $this->_get_ua();
154 foreach ($url_files as $url_file) {
155 $vary = $url_file['vary'];
156 $request_url = $url_file['url'];
157 $is_mobile = $url_file['mobile'];
158 $is_webp = $url_file['webp'];
159 $url_tag = self::get_url_tag($request_url);
160
161 $queue_k = (strlen($vary) > 32 ? md5($vary) : $vary) . ' ' . $url_tag;
162 $q = array(
163 'url' => apply_filters('litespeed_ucss_url', $request_url),
164 'user_agent' => substr($ua, 0, 200),
165 'is_mobile' => $is_mobile,
166 'is_webp' => $is_webp,
167 'uid' => false,
168 'vary' => $vary,
169 'url_tag' => $url_tag,
170 ); // Current UA will be used to request
171
172 self::debug('Added queue_ucss [url_tag] ' . $url_tag . ' [UA] ' . $ua . ' [vary] ' . $vary . ' [uid] false');
173 $this->_queue[$queue_k] = $q;
174 }
175 $this->save_queue('ucss', $this->_queue);
176 }
177
178 /**
179 * Generate UCSS
180 *
181 * @since 4.0
182 */
183 public static function cron( $continue = false ) {
184 $_instance = self::cls();
185 return $_instance->_cron_handler($continue);
186 }
187
188 /**
189 * Handle UCSS cron
190 *
191 * @since 4.2
192 */
193 private function _cron_handler( $continue ) {
194 $this->_queue = $this->load_queue('ucss');
195
196 if (empty($this->_queue)) {
197 return;
198 }
199
200 // For cron, need to check request interval too
201 if (!$continue) {
202 if (!empty($this->_summary['curr_request']) && time() - $this->_summary['curr_request'] < 300 && !$this->conf(self::O_DEBUG)) {
203 self::debug('Last request not done');
204 return;
205 }
206 }
207
208 $i = 0;
209 foreach ($this->_queue as $k => $v) {
210 if (!empty($v['_status'])) {
211 continue;
212 }
213
214 self::debug('cron job [tag] ' . $k . ' [url] ' . $v['url'] . ($v['is_mobile'] ? ' 📱 ' : '') . ' [UA] ' . $v['user_agent']);
215
216 if (!isset($v['is_webp'])) {
217 $v['is_webp'] = false;
218 }
219
220 ++$i;
221 $res = $this->_send_req($v['url'], $k, $v['uid'], $v['user_agent'], $v['vary'], $v['url_tag'], $v['is_mobile'], $v['is_webp']);
222 if (!$res) {
223 // Status is wrong, drop this this->_queue
224 $this->_queue = $this->load_queue('ucss');
225 unset($this->_queue[$k]);
226 $this->save_queue('ucss', $this->_queue);
227
228 if (!$continue) {
229 return;
230 }
231
232 if ($i > 3) {
233 GUI::print_loading(count($this->_queue), 'UCSS');
234 return Router::self_redirect(Router::ACTION_UCSS, self::TYPE_GEN);
235 }
236
237 continue;
238 }
239
240 // Exit queue if out of quota or service is hot
241 if ($res === 'out_of_quota' || $res === 'svc_hot') {
242 return;
243 }
244
245 $this->_queue = $this->load_queue('ucss');
246 $this->_queue[$k]['_status'] = 'requested';
247 $this->save_queue('ucss', $this->_queue);
248 self::debug('Saved to queue [k] ' . $k);
249
250 // only request first one
251 if (!$continue) {
252 return;
253 }
254
255 if ($i > 3) {
256 GUI::print_loading(count($this->_queue), 'UCSS');
257 return Router::self_redirect(Router::ACTION_UCSS, self::TYPE_GEN);
258 }
259 }
260 }
261
262 /**
263 * Send to QC API to generate UCSS
264 *
265 * @since 2.3
266 * @access private
267 */
268 private function _send_req( $request_url, $queue_k, $uid, $user_agent, $vary, $url_tag, $is_mobile, $is_webp ) {
269 // Check if has credit to push or not
270 $err = false;
271 $allowance = $this->cls('Cloud')->allowance(Cloud::SVC_UCSS, $err);
272 if (!$allowance) {
273 self::debug(' No credit: ' . $err);
274 $err && Admin_Display::error(Error::msg($err));
275 return 'out_of_quota';
276 }
277
278 set_time_limit(120);
279
280 // Update css request status
281 $this->_summary['curr_request'] = time();
282 self::save_summary();
283
284 // Gather guest HTML to send
285 $html = $this->cls('CSS')->prepare_html($request_url, $user_agent, $uid);
286
287 if (!$html) {
288 return false;
289 }
290
291 // Parse HTML to gather all CSS content before requesting
292 $css = false;
293 list(, $html) = $this->prepare_css($html, $is_webp, true); // Use this to drop CSS from HTML as we don't need those CSS to generate UCSS
294 $filename = $this->cls('Data')->load_url_file($url_tag, $vary, 'css');
295 $filepath_prefix = $this->_build_filepath_prefix('css');
296 $static_file = LITESPEED_STATIC_DIR . $filepath_prefix . $filename . '.css';
297 self::debug('Checking combined file ' . $static_file);
298 if (file_exists($static_file)) {
299 $css = File::read($static_file);
300 }
301
302 if (!$css) {
303 self::debug('❌ No combined css');
304 return false;
305 }
306
307 $data = array(
308 'url' => $request_url,
309 'queue_k' => $queue_k,
310 'user_agent' => $user_agent,
311 'is_mobile' => $is_mobile ? 1 : 0, // todo:compatible w/ tablet
312 'is_webp' => $is_webp ? 1 : 0,
313 'html' => $html,
314 'css' => $css,
315 );
316 if (!isset($this->_ucss_whitelist)) {
317 $this->_ucss_whitelist = $this->_filter_whitelist();
318 }
319 $data['whitelist'] = $this->_ucss_whitelist;
320
321 self::debug('Generating: ', $data);
322
323 $json = Cloud::post(Cloud::SVC_UCSS, $data, 30);
324 if (!is_array($json)) {
325 return $json;
326 }
327
328 // Old version compatibility
329 if (empty($json['status'])) {
330 if (!empty($json['ucss'])) {
331 $this->_save_con('ucss', $json['ucss'], $queue_k, $is_mobile, $is_webp);
332 }
333
334 // Delete the row
335 return false;
336 }
337
338 // Unknown status, remove this line
339 if ($json['status'] != 'queued') {
340 return false;
341 }
342
343 // Save summary data
344 $this->_summary['last_spent'] = time() - $this->_summary['curr_request'];
345 $this->_summary['last_request'] = $this->_summary['curr_request'];
346 $this->_summary['curr_request'] = 0;
347 self::save_summary();
348
349 return true;
350 }
351
352 /**
353 * Save UCSS content
354 *
355 * @since 4.2
356 */
357 private function _save_con( $type, $css, $queue_k, $is_mobile, $is_webp ) {
358 // Add filters
359 $css = apply_filters('litespeed_' . $type, $css, $queue_k);
360 self::debug2('con: ', $css);
361
362 if (substr($css, 0, 2) == '/*' && substr($css, -2) == '*/') {
363 self::debug('❌ empty ' . $type . ' [content] ' . $css);
364 // continue; // Save the error info too
365 }
366
367 // Write to file
368 $filecon_md5 = md5($css);
369
370 $filepath_prefix = $this->_build_filepath_prefix($type);
371 $static_file = LITESPEED_STATIC_DIR . $filepath_prefix . $filecon_md5 . '.css';
372
373 File::save($static_file, $css, true);
374
375 $url_tag = $this->_queue[$queue_k]['url_tag'];
376 $vary = $this->_queue[$queue_k]['vary'];
377 self::debug2("Save URL to file [file] $static_file [vary] $vary");
378
379 $this->cls('Data')->save_url($url_tag, $vary, $type, $filecon_md5, dirname($static_file), $is_mobile, $is_webp);
380
381 Purge::add(strtoupper($type) . '.' . md5($queue_k));
382 }
383
384 /**
385 * Prepare CSS from HTML for CCSS generation only. UCSS will used combined CSS directly.
386 * Prepare refined HTML for both CCSS and UCSS.
387 *
388 * @since 3.4.3
389 */
390 public function prepare_css( $html, $is_webp = false, $dryrun = false ) {
391 $css = '';
392 preg_match_all('#<link ([^>]+)/?>|<style([^>]*)>([^<]+)</style>#isU', $html, $matches, PREG_SET_ORDER);
393 foreach ($matches as $match) {
394 $debug_info = '';
395 if (strpos($match[0], '<link') === 0) {
396 $attrs = Utility::parse_attr($match[1]);
397
398 if (empty($attrs['rel'])) {
399 continue;
400 }
401
402 if ($attrs['rel'] != 'stylesheet') {
403 if ($attrs['rel'] != 'preload' || empty($attrs['as']) || $attrs['as'] != 'style') {
404 continue;
405 }
406 }
407
408 if (!empty($attrs['media']) && strpos($attrs['media'], 'print') !== false) {
409 continue;
410 }
411
412 if (empty($attrs['href'])) {
413 continue;
414 }
415
416 // Check Google fonts hit
417 if (strpos($attrs['href'], 'fonts.googleapis.com') !== false) {
418 $html = str_replace($match[0], '', $html);
419 continue;
420 }
421
422 $debug_info = $attrs['href'];
423
424 // Load CSS content
425 if (!$dryrun) {
426 // Dryrun will not load CSS but just drop them
427 $con = $this->cls('Optimizer')->load_file($attrs['href']);
428 if (!$con) {
429 continue;
430 }
431 } else {
432 $con = '';
433 }
434 } else {
435 // Inline style
436 $attrs = Utility::parse_attr($match[2]);
437
438 if (!empty($attrs['media']) && strpos($attrs['media'], 'print') !== false) {
439 continue;
440 }
441
442 Debug2::debug2('[CSS] Load inline CSS ' . substr($match[3], 0, 100) . '...', $attrs);
443 $con = $match[3];
444
445 $debug_info = '__INLINE__';
446 }
447
448 $con = Optimizer::minify_css($con);
449 if ($is_webp && $this->cls('Media')->webp_support()) {
450 $con = $this->cls('Media')->replace_background_webp($con);
451 }
452
453 if (!empty($attrs['media']) && $attrs['media'] !== 'all') {
454 $con = '@media ' . $attrs['media'] . '{' . $con . "}\n";
455 } else {
456 $con = $con . "\n";
457 }
458
459 $con = '/* ' . $debug_info . ' */' . $con;
460 $css .= $con;
461
462 $html = str_replace($match[0], '', $html);
463 }
464
465 return array( $css, $html );
466 }
467
468 /**
469 * Filter the comment content, add quotes to selector from whitelist. Return the json
470 *
471 * @since 3.3
472 */
473 private function _filter_whitelist() {
474 $whitelist = array();
475 $list = apply_filters('litespeed_ucss_whitelist', $this->conf(self::O_OPTM_UCSS_SELECTOR_WHITELIST));
476 foreach ($list as $k => $v) {
477 if (substr($v, 0, 2) === '//') {
478 continue;
479 }
480 // Wrap in quotes for selectors
481 if (substr($v, 0, 1) !== '/' && strpos($v, '"') === false && strpos($v, "'") === false) {
482 // $v = "'$v'";
483 }
484 $whitelist[] = $v;
485 }
486
487 return $whitelist;
488 }
489
490 /**
491 * Notify finished from server
492 *
493 * @since 5.1
494 */
495 public function notify() {
496 $post_data = \json_decode(file_get_contents('php://input'), true);
497 if (is_null($post_data)) {
498 $post_data = $_POST;
499 }
500 self::debug('notify() data', $post_data);
501
502 $this->_queue = $this->load_queue('ucss');
503
504 list($post_data) = $this->cls('Cloud')->extract_msg($post_data, 'ucss');
505
506 $notified_data = $post_data['data'];
507 if (empty($notified_data) || !is_array($notified_data)) {
508 self::debug('❌ notify exit: no notified data');
509 return Cloud::err('no notified data');
510 }
511
512 // Check if its in queue or not
513 $valid_i = 0;
514 foreach ($notified_data as $v) {
515 if (empty($v['request_url'])) {
516 self::debug('❌ notify bypass: no request_url', $v);
517 continue;
518 }
519 if (empty($v['queue_k'])) {
520 self::debug('❌ notify bypass: no queue_k', $v);
521 continue;
522 }
523
524 if (empty($this->_queue[$v['queue_k']])) {
525 self::debug('❌ notify bypass: no this queue [q_k]' . $v['queue_k']);
526 continue;
527 }
528
529 // Save data
530 if (!empty($v['data_ucss'])) {
531 $is_mobile = $this->_queue[$v['queue_k']]['is_mobile'];
532 $is_webp = $this->_queue[$v['queue_k']]['is_webp'];
533 $this->_save_con('ucss', $v['data_ucss'], $v['queue_k'], $is_mobile, $is_webp);
534
535 ++$valid_i;
536 }
537
538 unset($this->_queue[$v['queue_k']]);
539 self::debug('notify data handled, unset queue [q_k] ' . $v['queue_k']);
540 }
541 $this->save_queue('ucss', $this->_queue);
542
543 self::debug('notified');
544
545 return Cloud::ok(array( 'count' => $valid_i ));
546 }
547
548 /**
549 * Handle all request actions from main cls
550 *
551 * @since 2.3
552 * @access public
553 */
554 public function handler() {
555 $type = Router::verify_type();
556
557 switch ($type) {
558 case self::TYPE_GEN:
559 self::cron(true);
560 break;
561
562 case self::TYPE_CLEAR_Q:
563 $this->clear_q('ucss');
564 break;
565
566 default:
567 break;
568 }
569
570 Admin::redirect();
571 }
572 }
573