PluginProbe
Autoptimize / 3.0.3
Autoptimize v3.0.3
2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.5.0 2.5.1 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 All 107 releases
autoptimize / classes / autoptimizeCriticalCSSCron.php

autoptimizeCriticalCSSCron.php in Autoptimize 3.0.3, at classes/autoptimizeCriticalCSSCron.php

849 lines 43.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Critical CSS Cron logic:
4 * processes the queue, submitting jobs to criticalcss.com and retrieving generated CSS from criticalcss.com and saving rules.
5 */
6
7 if ( ! defined( 'ABSPATH' ) ) {
8 exit;
9 }
10
11 class autoptimizeCriticalCSSCron {
12 public function __construct() {
13 $this->criticalcss = autoptimize()->criticalcss();
14
15 // Add queue control to a registered event.
16 add_action( 'ao_ccss_queue', array( $this, 'ao_ccss_queue_control' ) );
17 // Add cleaning job to a registered event.
18 add_action( 'ao_ccss_maintenance', array( $this, 'ao_ccss_cleaning' ) );
19 }
20
21 public function ao_ccss_queue_control() {
22 // The queue execution backend.
23 $key = $this->criticalcss->get_option( 'key' );
24
25 if ( empty( $key ) ) {
26 // no key set, not processing the queue!
27 $this->criticalcss->log( 'No key set, so not processing queue.', 3 );
28 return;
29 }
30
31 /**
32 * Provide a debug facility for the queue
33 * This debug facility provides a way to easily force some queue behaviors useful for development and testing.
34 * To enable this feature, create the file AO_CCSS_DIR . 'queue.json' with a JSON object like the one bellow:
35 *
36 * {"enable":bool,"htcode":int,"status":0|"str","resultStatus ":0|"str","validationStatus":0|"str"}
37 *
38 * Where values are:
39 * - enable : 0 or 1, enable or disable this debug facility straight from the file
40 * - htcode : 0 or any HTTP reponse code (e.g. 2xx, 4xx, 5xx) to force API responses
41 * - status : 0 or a valid value for 'status' (see 'Generating critical css - Job Status Types' in spec docs)
42 * - resultStatus : 0 or a valid value for 'resultStatus' (see 'Appendix - Result status types' in the spec docs)
43 * - validationStatus: 0 or a valid value for 'validationStatus' (see 'Appendix - Validation status types' in the spec docs)
44 *
45 * When properly set, queue will always finish a job with the declared settings above regardless of the real API responses.
46 */
47 $queue_debug = false;
48 if ( file_exists( AO_CCSS_DEBUG ) ) {
49 $qdobj_raw = file_get_contents( AO_CCSS_DEBUG );
50 $qdobj = json_decode( $qdobj_raw, true );
51 if ( $qdobj ) {
52 if ( 1 === $qdobj['enable'] ) {
53 $queue_debug = true;
54 $this->criticalcss->log( 'Queue operating in debug mode with the following settings: <' . $qdobj_raw . '>', 3 );
55 }
56 }
57 }
58
59 // Set some default values for $qdobj to avoid function call warnings.
60 if ( ! $queue_debug ) {
61 $qdobj['htcode'] = false;
62 }
63
64 // Check if queue is already running.
65 $queue_lock = false;
66 if ( file_exists( AO_CCSS_LOCK ) ) {
67 $queue_lock = true;
68 }
69
70 // Proceed with the queue if it's not already running.
71 if ( ! $queue_lock ) {
72
73 // Log queue start and create the lock file.
74 $this->criticalcss->log( 'Queue control started', 3 );
75 if ( touch( AO_CCSS_LOCK ) ) {
76 $this->criticalcss->log( 'Queue control locked', 3 );
77 }
78
79 // Attach required variables.
80 $queue = $this->criticalcss->get_option( 'queue' );
81 $rtimelimit = $this->criticalcss->get_option( 'rtimelimit' );
82
83 // Initialize counters.
84 if ( $rtimelimit == 0 ) {
85 // no time limit set, let's go with 1000 seconds.
86 $rtimelimit = 1000;
87 }
88 $mt = time() + $rtimelimit; // maxtime queue processing can run.
89 $jc = 1; // job count number.
90 $jr = 1; // jobs requests number.
91 $jt = count( $queue ); // number of jobs in queue.
92
93 // Sort queue by ascending job status (e.g. ERROR, JOB_ONGOING, JOB_QUEUED, NEW...).
94 array_multisort( array_column( $queue, 'jqstat' ), $queue ); // @codingStandardsIgnoreLine
95
96 // Iterates over the entire queue.
97 foreach ( $queue as $path => $jprops ) {
98 // Prepare flags and target rule.
99 $update = false;
100 $deljob = false;
101 $rule_update = false;
102 $oldccssfile = false;
103 $trule = explode( '|', $jprops['rtarget'] );
104
105 // Log job count.
106 $this->criticalcss->log( 'Processing job ' . $jc . ' of ' . $jt . ' with id <' . $jprops['ljid'] . '> and status <' . $jprops['jqstat'] . '>', 3 );
107
108 // Process NEW jobs.
109 if ( 'NEW' == $jprops['jqstat'] ) {
110
111 // Log the new job.
112 $this->criticalcss->log( 'Found NEW job with local ID <' . $jprops['ljid'] . '>, starting its queue processing', 3 );
113
114 // Compare job and rule hashes (if any).
115 $hash = $this->ao_ccss_diff_hashes( $jprops['ljid'], $jprops['hash'], $jprops['hashes'], $jprops['rtarget'] );
116
117 // If job hash is new or different of a previous one.
118 if ( $hash ) {
119 if ( $jr > 2 ) {
120 // we already posted 2 jobs to criticalcss.com, don't post more this run
121 // but we can keep on processing the queue to keep it tidy.
122 $this->criticalcss->log( 'Holding off on generating request for job with local ID <' . $jprops['ljid'] . '>, maximum number of POSTS reached.', 3 );
123 continue;
124 }
125
126 // Set job hash.
127 $jprops['hash'] = $hash;
128
129 // Dispatch the job generate request and increment request count.
130 $apireq = $this->ao_ccss_api_generate( $path, $queue_debug, $qdobj['htcode'] );
131 $jr++;
132
133 // NOTE: All the following conditions maps to the ones in admin_settings_queue.js.php.
134 if ( empty( $apireq ) ) {
135 // ERROR: no response
136 // Update job properties.
137 $jprops['jqstat'] = 'NO_RESPONSE';
138 $jprops['jrstat'] = 'NONE';
139 $jprops['jvstat'] = 'NONE';
140 $jprops['jftime'] = microtime( true );
141 $this->criticalcss->log( 'Job id <' . $jprops['ljid'] . '> request has no response, status now is <' . $jprops['jqstat'] . '>', 3 );
142 } elseif ( array_key_exists( 'errorCode', $apireq ) && 'INVALID_JWT_TOKEN' == $apireq['errorCode'] ) {
143 // ERROR: key validation
144 // Update job properties.
145 $jprops['jqstat'] = $apireq['errorCode'];
146 $jprops['jrstat'] = $apireq['error'];
147 $jprops['jvstat'] = 'NONE';
148 $jprops['jftime'] = microtime( true );
149 $this->criticalcss->log( 'API key validation error when processing job id <' . $jprops['ljid'] . '>, job status is now <' . $jprops['jqstat'] . '>', 3 );
150 } elseif ( array_key_exists( 'job', $apireq ) && 'JOB_QUEUED' == $apireq['job']['status'] || 'JOB_ONGOING' == $apireq['job']['status'] ) {
151 // SUCCESS: request has a valid result.
152 // Update job properties.
153 $jprops['jid'] = $apireq['job']['id'];
154 $jprops['jqstat'] = $apireq['job']['status'];
155 $this->criticalcss->log( 'Job id <' . $jprops['ljid'] . '> generate request successful, remote id <' . $jprops['jid'] . '>, status now is <' . $jprops['jqstat'] . '>', 3 );
156 } elseif ( 'STATUS_JOB_BAD' == $apireq['job']['status'] ) {
157 // ERROR: concurrent requests
158 // Update job properties.
159 $jprops['jid'] = $apireq['job']['id'];
160 $jprops['jqstat'] = $apireq['job']['status'];
161 if ( $apireq['job']['error'] ) {
162 $jprops['jrstat'] = $apireq['job']['error'];
163 } else {
164 $jprops['jrstat'] = 'Baby did a bad bad thing';
165 }
166 $jprops['jvstat'] = 'NONE';
167 $jprops['jftime'] = microtime( true );
168 $this->criticalcss->log( 'Concurrent requests when processing job id <' . $jprops['ljid'] . '>, job status is now <' . $jprops['jqstat'] . '>', 3 );
169 } else {
170 // UNKNOWN: unhandled generate exception
171 // Update job properties.
172 $jprops['jqstat'] = 'JOB_UNKNOWN';
173 $jprops['jrstat'] = 'NONE';
174 $jprops['jvstat'] = 'NONE';
175 $jprops['jftime'] = microtime( true );
176 $this->criticalcss->log( 'Job id <' . $jprops['ljid'] . '> generate request has an UNKNOWN condition, status now is <' . $jprops['jqstat'] . '>, check log messages above for more information', 2 );
177 $this->criticalcss->log( 'Job response was: ' . json_encode( $apireq ), 3 );
178 }
179 } else {
180 // SUCCESS: Job hash is equal to a previous one, so it's done
181 // Update job status and finish time.
182 $jprops['jqstat'] = 'JOB_DONE';
183 $jprops['jftime'] = microtime( true );
184 $this->criticalcss->log( 'Job id <' . $jprops['ljid'] . '> requires no further processing, status now is <' . $jprops['jqstat'] . '>', 3 );
185 }
186
187 // Set queue update flag.
188 $update = true;
189
190 } elseif ( 'JOB_QUEUED' == $jprops['jqstat'] || 'JOB_ONGOING' == $jprops['jqstat'] ) {
191 // Process QUEUED and ONGOING jobs
192 // Log the pending job.
193 $this->criticalcss->log( 'Found PENDING job with local ID <' . $jprops['ljid'] . '>, continuing its queue processing', 3 );
194
195 // Dispatch the job result request and increment request count.
196 $apireq = $this->ao_ccss_api_results( $jprops['jid'], $queue_debug, $qdobj['htcode'] );
197
198 // NOTE: All the following condigitons maps to the ones in admin_settings_queue.js.php
199 // Replace API response values if queue debugging is enabled and some value is set.
200 if ( $queue_debug ) {
201 if ( $qdobj['status'] ) {
202 $apireq['status'] = $qdobj['status'];
203 }
204 if ( $qdobj['resultStatus'] ) {
205 $apireq['resultStatus'] = $qdobj['resultStatus'];
206 }
207 if ( $qdobj['validationStatus'] ) {
208 $apireq['validationStatus'] = $qdobj['validationStatus'];
209 }
210 }
211
212 if ( 'JOB_QUEUED' == $apireq['status'] || 'JOB_ONGOING' == $apireq['status'] ) {
213 // SUCCESS: request has a valid result
214 // Process a PENDING job
215 // Update job properties.
216 $jprops['jqstat'] = $apireq['status'];
217 $this->criticalcss->log( 'Job id <' . $jprops['ljid'] . '> result request successful, remote id <' . $jprops['jid'] . '>, status <' . $jprops['jqstat'] . '> unchanged', 3 );
218 } elseif ( 'JOB_DONE' == $apireq['status'] ) {
219 // Process a DONE job
220 // New resultStatus from ccss.com "HTML_404", consider as "GOOD" for now.
221 if ( 'HTML_404' == $apireq['resultStatus'] ) {
222 $apireq['resultStatus'] = 'GOOD';
223 }
224
225 if ( 'GOOD' == $apireq['resultStatus'] && ( 'GOOD' == $apireq['validationStatus'] || 'WARN' == $apireq['validationStatus'] ) ) {
226 // SUCCESS: GOOD job with GOOD or WARN validation
227 // Update job properties.
228 $jprops['file'] = $this->ao_ccss_save_file( $apireq['css'], $trule, false );
229 $jprops['jqstat'] = $apireq['status'];
230 $jprops['jrstat'] = $apireq['resultStatus'];
231 $jprops['jvstat'] = $apireq['validationStatus'];
232 $jprops['jftime'] = microtime( true );
233 $rule_update = true;
234 $this->criticalcss->log( 'Job id <' . $jprops['ljid'] . '> result request successful, remote id <' . $jprops['jid'] . '>, status <' . $jprops['jqstat'] . '>, file saved <' . $jprops['file'] . '>', 3 );
235 } elseif ( 'GOOD' == $apireq['resultStatus'] && ( 'BAD' == $apireq['validationStatus'] || 'SCREENSHOT_WARN_BLANK' == $apireq['validationStatus'] ) ) {
236 // SUCCESS: GOOD job with BAD or SCREENSHOT_WARN_BLANK validation
237 // Update job properties.
238 $jprops['jqstat'] = $apireq['status'];
239 $jprops['jrstat'] = $apireq['resultStatus'];
240 $jprops['jvstat'] = $apireq['validationStatus'];
241 $jprops['jftime'] = microtime( true );
242 if ( apply_filters( 'autoptimize_filter_ccss_save_review_rules', true ) ) {
243 $jprops['file'] = $this->ao_ccss_save_file( $apireq['css'], $trule, true );
244 $rule_update = true;
245 $this->criticalcss->log( 'Job id <' . $jprops['ljid'] . '> result request successful, remote id <' . $jprops['jid'] . '>, status <' . $jprops['jqstat'] . ', file saved <' . $jprops['file'] . '> but requires REVIEW', 3 );
246 } else {
247 $this->criticalcss->log( 'Job id <' . $jprops['ljid'] . '> result request successful, remote id <' . $jprops['jid'] . '>, status <' . $jprops['jqstat'] . ', file not saved because it required REVIEW.', 3 );
248 }
249 } elseif ( 'GOOD' != $apireq['resultStatus'] && ( 'GOOD' != $apireq['validationStatus'] || 'WARN' != $apireq['validationStatus'] || 'BAD' != $apireq['validationStatus'] || 'SCREENSHOT_WARN_BLANK' != $apireq['validationStatus'] ) ) {
250 // ERROR: no GOOD, WARN or BAD results
251 // Update job properties.
252 $jprops['jqstat'] = $apireq['status'];
253 $jprops['jrstat'] = $apireq['resultStatus'];
254 $jprops['jvstat'] = $apireq['validationStatus'];
255 $jprops['jftime'] = microtime( true );
256 $this->criticalcss->log( 'Job id <' . $jprops['ljid'] . '> result request successful but job FAILED, status now is <' . $jprops['jqstat'] . '>', 3 );
257 $apireq['css'] = '/* critical css removed for DEBUG logging purposes */';
258 $this->criticalcss->log( 'Job response was: ' . json_encode( $apireq ), 3 );
259 } else {
260 // UNKNOWN: unhandled JOB_DONE exception
261 // Update job properties.
262 $jprops['jqstat'] = 'JOB_UNKNOWN';
263 $jprops['jrstat'] = $apireq['resultStatus'];
264 $jprops['jvstat'] = $apireq['validationStatus'];
265 $jprops['jftime'] = microtime( true );
266 $this->criticalcss->log( 'Job id <' . $jprops['ljid'] . '> result request successful but job is UNKNOWN, status now is <' . $jprops['jqstat'] . '>', 2 );
267 $apireq['css'] = '/* critical css removed for DEBUG logging purposes */';
268 $this->criticalcss->log( 'Job response was: ' . json_encode( $apireq ), 3 );
269 }
270 } elseif ( 'JOB_FAILED' == $apireq['job']['status'] || 'STATUS_JOB_BAD' == $apireq['job']['status'] ) {
271 // ERROR: failed job
272 // Update job properties.
273 $jprops['jqstat'] = $apireq['job']['status'];
274 if ( $apireq['job']['error'] ) {
275 $jprops['jrstat'] = $apireq['job']['error'];
276 } else {
277 $jprops['jrstat'] = 'Baby did a bad bad thing';
278 }
279 $jprops['jvstat'] = 'NONE';
280 $jprops['jftime'] = microtime( true );
281 $this->criticalcss->log( 'Job id <' . $jprops['ljid'] . '> result request successful but job FAILED, status now is <' . $jprops['jqstat'] . '>', 3 );
282 } elseif ( 'This css no longer exists. Please re-generate it.' == $apireq['error'] ) {
283 // ERROR: CSS doesn't exist
284 // Update job properties.
285 $jprops['jqstat'] = 'NO_CSS';
286 $jprops['jrstat'] = $apireq['error'];
287 $jprops['jvstat'] = 'NONE';
288 $jprops['jftime'] = microtime( true );
289 $this->criticalcss->log( 'Job id <' . $jprops['ljid'] . '> result request successful but job FAILED, status now is <' . $jprops['jqstat'] . '>', 3 );
290 } elseif ( empty( $apireq ) ) {
291 // ERROR: no response
292 // Update job properties.
293 $jprops['jqstat'] = 'NO_RESPONSE';
294 $jprops['jrstat'] = 'NONE';
295 $jprops['jvstat'] = 'NONE';
296 $jprops['jftime'] = microtime( true );
297 $this->criticalcss->log( 'Job id <' . $jprops['ljid'] . '> request has no response, status now is <' . $jprops['jqstat'] . '>', 3 );
298 } else {
299 // UNKNOWN: unhandled results exception
300 // Update job properties.
301 $jprops['jqstat'] = 'JOB_UNKNOWN';
302 $jprops['jrstat'] = 'NONE';
303 $jprops['jvstat'] = 'NONE';
304 $jprops['jftime'] = microtime( true );
305 $this->criticalcss->log( 'Job id <' . $jprops['ljid'] . '> result request has an UNKNOWN condition, status now is <' . $jprops['jqstat'] . '>, check log messages above for more information', 2 );
306 }
307
308 // Set queue update flag.
309 $update = true;
310 }
311
312 // Mark DONE jobs for removal.
313 if ( 'JOB_DONE' == $jprops['jqstat'] ) {
314 $update = true;
315 $deljob = true;
316 }
317
318 // Persist updated queue object.
319 if ( $update ) {
320 if ( ! $deljob ) {
321 // Update properties of a NEW or PENDING job...
322 $queue[ $path ] = $jprops;
323 } else {
324 // ...or remove the DONE job.
325 unset( $queue[ $path ] );
326 $this->criticalcss->log( 'Job id <' . $jprops['ljid'] . '> is DONE and was removed from the queue', 3 );
327 }
328
329 // Update queue object.
330 $queue_raw = json_encode( $queue );
331 update_option( 'autoptimize_ccss_queue', $queue_raw, false );
332 $this->criticalcss->log( 'Queue updated by job id <' . $jprops['ljid'] . '>', 3 );
333
334 // Update target rule.
335 if ( $rule_update ) {
336 $this->ao_ccss_rule_update( $jprops['ljid'], $jprops['rtarget'], $jprops['file'], $jprops['hash'] );
337 $this->criticalcss->log( 'Job id <' . $jprops['ljid'] . '> updated the target rule <' . $jprops['rtarget'] . '>', 3 );
338 }
339 } else {
340 // Or log no queue action.
341 $this->criticalcss->log( 'Nothing to do on this job', 3 );
342 }
343
344 // Break the loop if request time limit is (almost exceeded).
345 if ( time() > $mt ) {
346 $this->criticalcss->log( 'The time limit of ' . $rtimelimit . ' seconds was exceeded, queue control must finish now', 3 );
347 break;
348 }
349
350 // Increment job counter.
351 $jc++;
352 }
353
354 // Remove the lock file and log the queue end.
355 if ( file_exists( AO_CCSS_LOCK ) ) {
356 unlink( AO_CCSS_LOCK );
357 $this->criticalcss->log( 'Queue control unlocked', 3 );
358 }
359 $this->criticalcss->log( 'Queue control finished', 3 );
360
361 // Log that queue is locked.
362 } else {
363 $this->criticalcss->log( 'Queue is already running, skipping the attempt to run it again', 3 );
364 }
365 }
366
367 public function ao_ccss_diff_hashes( $ljid, $hash, $hashes, $rule ) {
368 // Compare job hashes
369 // STEP 1: update job hashes.
370 if ( 1 == count( $hashes ) ) {
371 // Job with a single hash
372 // Set job hash.
373 $hash = $hashes[0];
374 $this->criticalcss->log( 'Job id <' . $ljid . '> updated with SINGLE hash <' . $hash . '>', 3 );
375 } else {
376 // Job with multiple hashes
377 // Loop through hashes to concatenate them.
378 $nhash = '';
379 foreach ( $hashes as $shash ) {
380 $nhash .= $shash;
381 }
382
383 // Set job hash.
384 $hash = md5( $nhash );
385 $this->criticalcss->log( 'Job id <' . $ljid . '> updated with a COMPOSITE hash <' . $hash . '>', 3 );
386 }
387
388 // STEP 2: compare job to existing jobs to prevent double submission for same type+hash.
389 $queue = $this->criticalcss->get_option( 'queue' );
390
391 foreach ( $queue as $queue_item ) {
392 $this->criticalcss->log( 'Comparing <' . $rule . $hash . '> with <' . $queue_item['rtarget'] . $queue_item['hash'] . '>', 3 );
393 if ( $queue_item['hash'] == $hash && $queue_item['rtarget'] == $rule && in_array( $queue_item['jqstat'], array( 'JOB_QUEUED', 'JOB_ONGOING', 'JOB_DONE' ) ) ) {
394 $this->criticalcss->log( 'Job id <' . $ljid . '> matches the already pending job <' . $queue_item['ljid'] . '>', 3 );
395 return false;
396 }
397 }
398
399 // STEP 3: compare job and existing rule (if any) hashes
400 // Attach required arrays.
401 $rules = $this->criticalcss->get_option( 'rules' );
402
403 // Prepare rule variables.
404 $trule = explode( '|', $rule );
405 if ( is_array( $trule ) && ! empty( $trule ) && array_key_exists( $trule[1], $rules[ $trule[0] ] ) ) {
406 $srule = $rules[ $trule[0] ][ $trule[1] ];
407 } else {
408 $srule = '';
409 }
410
411 // If hash is empty, set it to now for a "forced job".
412 if ( empty( $hash ) ) {
413 $hash = 'new';
414 $this->criticalcss->log( 'Job id <' . $ljid . '> had no hash, assuming forced job so setting hash to new', 3 );
415 }
416
417 // Check if a MANUAL rule exist and return false.
418 if ( ! empty( $srule ) && ( 0 == $srule['hash'] && 0 != $srule['file'] ) ) {
419 $this->criticalcss->log( 'Job id <' . $ljid . '> matches the MANUAL rule <' . $trule[0] . '|' . $trule[1] . '>', 3 );
420 return false;
421 } elseif ( ! empty( $srule ) ) {
422 // Check if an AUTO rule exist.
423 if ( $hash === $srule['hash'] && is_file( AO_CCSS_DIR . $srule['file'] ) && 0 != filesize( AO_CCSS_DIR . $srule['file'] ) ) {
424 // Check if job hash matches rule, if the CCSS file exists said file is not empty and return FALSE is so.
425 $this->criticalcss->log( 'Job id <' . $ljid . '> with hash <' . $hash . '> MATCH the one in rule <' . $trule[0] . '|' . $trule[1] . '>', 3 );
426 return false;
427 } else {
428 // Or return the new hash if they differ.
429 $this->criticalcss->log( 'Job id <' . $ljid . '> with hash <' . $hash . '> DOES NOT MATCH the one in rule <' . $trule[0] . '|' . $trule[1] . '> or rule\'s CCSS file was invalid.', 3 );
430 return $hash;
431 }
432 } else {
433 // Return the hash for a job that has no rule yet.
434 $this->criticalcss->log( 'Job id <' . $ljid . '> with hash <' . $hash . '> has no rule yet', 3 );
435 return $hash;
436 }
437 }
438
439 public function ao_ccss_api_generate( $path, $debug, $dcode ) {
440 // POST jobs to criticalcss.com and return responses
441 // Get key and key status.
442 $key = $this->criticalcss->get_option( 'key' );
443 $key_status = $this->criticalcss->get_option( 'keyst' );
444 $noptimize = $this->criticalcss->get_option( 'noptimize' );
445
446 // Prepare full URL to request.
447 $site_host = get_site_url();
448 $site_path = parse_url( $site_host, PHP_URL_PATH );
449
450 if ( ! empty( $site_path ) ) {
451 $site_host = str_replace( $site_path, '', $site_host );
452 }
453
454 // Logic to bind to one domain to avoid site clones of sites would
455 // automatically begin spawning requests to criticalcss.com which has
456 // a per domain cost.
457 $domain = $this->criticalcss->get_option( 'domain' );
458 if ( empty( $domain ) ) {
459 // first request being done, update option to allow future requests are only allowed if from same domain.
460 update_option( 'autoptimize_ccss_domain', str_rot13( $site_host ) );
461 } elseif ( trim( $domain, '\'"' ) !== 'none' && parse_url( $site_host, PHP_URL_HOST ) !== parse_url( $domain, PHP_URL_HOST ) && apply_filters( 'autoptimize_filter_ccss_bind_domain', true ) ) {
462 // not the same domain, log as error and return without posting to criticalcss.com.
463 $this->criticalcss->log( 'Request for domain ' . $site_host . ' does not match bound domain ' . $domain . ' so not proceeding.', 2 );
464 return false;
465 }
466
467 $src_url = $site_host . $path;
468
469 // Avoid AO optimizations if required by config or avoid lazyload if lazyload is active in AO.
470 if ( ! empty( $noptimize ) ) {
471 $src_url .= '?ao_noptirocket=1';
472 } elseif ( ( class_exists( 'autoptimizeImages', false ) && autoptimizeImages::should_lazyload_wrapper() ) || apply_filters( 'autoptimize_filter_ccss_enforce_nolazy', false ) ) {
473 $src_url .= '?ao_nolazy=1';
474 }
475
476 $src_url = apply_filters( 'autoptimize_filter_ccss_cron_srcurl', $src_url );
477
478 // Initialize request body.
479 $body = array();
480 $body['url'] = $src_url;
481 $body['aff'] = 1;
482 $body['aocssv'] = AO_CCSS_VER;
483
484 // Prepare and add viewport size to the body if available.
485 $viewport = $this->criticalcss->viewport();
486 if ( ! empty( $viewport['w'] ) && ! empty( $viewport['h'] ) ) {
487 $body['width'] = $viewport['w'];
488 $body['height'] = $viewport['h'];
489 }
490
491 // Prepare and add forceInclude to the body if available.
492 $finclude = $this->criticalcss->get_option( 'finclude' );
493 $finclude = $this->ao_ccss_finclude( $finclude );
494 if ( ! empty( $finclude ) ) {
495 $body['forceInclude'] = $finclude;
496 }
497
498 // Add filter to allow the body array to be altered (e.g. to add customPageHeaders).
499 $body = apply_filters( 'autoptimize_ccss_cron_api_generate_body', $body );
500
501 // Body must be json and log it.
502 $body = json_encode( $body );
503 $this->criticalcss->log( 'criticalcss.com: POST generate request body is ' . $body, 3 );
504
505 // Prepare the request.
506 $url = esc_url_raw( AO_CCSS_API . 'generate?aover=' . AO_CCSS_VER );
507 $args = array(
508 'headers' => apply_filters( 'autoptimize_ccss_cron_api_generate_headers', array(
509 'User-Agent' => 'Autoptimize v' . AO_CCSS_VER,
510 'Content-type' => 'application/json; charset=utf-8',
511 'Authorization' => 'JWT ' . $key,
512 'Connection' => 'close',
513 ) ),
514 'body' => $body,
515 );
516
517 // Dispatch the request and store its response code.
518 $req = wp_safe_remote_post( $url, $args );
519 $code = wp_remote_retrieve_response_code( $req );
520 $body = json_decode( wp_remote_retrieve_body( $req ), true );
521
522 if ( $debug && $dcode ) {
523 // If queue debug is active, change response code.
524 $code = $dcode;
525 }
526
527 if ( 200 == $code ) {
528 // Response code is OK.
529 // Workaround criticalcss.com non-RESTful reponses.
530 if ( 'JOB_QUEUED' == $body['job']['status'] || 'JOB_ONGOING' == $body['job']['status'] || 'STATUS_JOB_BAD' == $body['job']['status'] ) {
531 // Log successful and return encoded request body.
532 $this->criticalcss->log( 'criticalcss.com: POST generate request for path <' . $src_url . '> replied successfully', 3 );
533
534 // This code also means the key is valid, so cache key status for 24h if not already cached.
535 if ( ( ! $key_status || 2 != $key_status ) && $key ) {
536 update_option( 'autoptimize_ccss_keyst', 2 );
537 $this->criticalcss->log( 'criticalcss.com: API key is valid, updating key status', 3 );
538 }
539
540 // Return the request body.
541 return $body;
542 } else {
543 // Log successful requests with invalid reponses.
544 $this->criticalcss->log( 'criticalcss.com: POST generate request for path <' . $src_url . '> replied with code <' . $code . '> and an UNKNOWN error condition, body follows...', 2 );
545 $this->criticalcss->log( print_r( $body, true ), 2 );
546 return $body;
547 }
548 } else {
549 // Response code is anything else.
550 // Log failed request with a valid response code and return body.
551 if ( $code ) {
552 $this->criticalcss->log( 'criticalcss.com: POST generate request for path <' . $src_url . '> replied with error code <' . $code . '>, body follows...', 2 );
553 $this->criticalcss->log( print_r( $body, true ), 2 );
554
555 if ( 401 == $code ) {
556 // If request is unauthorized, also clear key status.
557 update_option( 'autoptimize_ccss_keyst', 1 );
558 $this->criticalcss->log( 'criticalcss.com: API key is invalid, updating key status', 3 );
559 }
560
561 // Return the request body.
562 return $body;
563 } else {
564 // Log failed request with no response and return false.
565 $this->criticalcss->log( 'criticalcss.com: POST generate request for path <' . $src_url . '> has no response, this could be a service timeout', 2 );
566 if ( is_wp_error( $req ) ) {
567 $this->criticalcss->log( $req->get_error_message(), 2 );
568 }
569
570 return false;
571 }
572 }
573 }
574
575 public function ao_ccss_api_results( $jobid, $debug, $dcode ) {
576 // GET jobs from criticalcss.com and return responses
577 // Get key.
578 $key = $this->criticalcss->get_option( 'key' );
579
580 // Prepare the request.
581 $url = AO_CCSS_API . 'results?resultId=' . $jobid;
582 $args = array(
583 'headers' => apply_filters( 'autoptimize_ccss_cron_api_generate_headers', array(
584 'User-Agent' => 'Autoptimize CriticalCSS Power-Up v' . AO_CCSS_VER,
585 'Authorization' => 'JWT ' . $key,
586 'Connection' => 'close',
587 ) ),
588 );
589
590 // Dispatch the request and store its response code.
591 $req = wp_safe_remote_get( $url, $args );
592 $code = wp_remote_retrieve_response_code( $req );
593 $body = json_decode( wp_remote_retrieve_body( $req ), true );
594
595 if ( $debug && $dcode ) {
596 // If queue debug is active, change response code.
597 $code = $dcode;
598 }
599
600 if ( 200 == $code ) {
601 // Response code is OK.
602 if ( is_array( $body ) && ( array_key_exists( 'status', $body ) || array_key_exists( 'job', $body ) ) && ( 'JOB_QUEUED' == $body['status'] || 'JOB_ONGOING' == $body['status'] || 'JOB_DONE' == $body['status'] || 'JOB_FAILED' == $body['status'] || 'JOB_UNKNOWN' == $body['status'] || 'STATUS_JOB_BAD' == $body['job']['status'] ) ) {
603 // Workaround criticalcss.com non-RESTful reponses
604 // Log successful and return encoded request body.
605 $this->criticalcss->log( 'criticalcss.com: GET results request for remote job id <' . $jobid . '> replied successfully', 3 );
606 return $body;
607 } elseif ( is_array( $body ) && ( array_key_exists( 'error', $body ) && 'This css no longer exists. Please re-generate it.' == $body['error'] ) ) {
608 // Handle no CSS reply
609 // Log no CSS error and return encoded request body.
610 $this->criticalcss->log( 'criticalcss.com: GET results request for remote job id <' . $jobid . '> replied successfully but the CSS for it does not exist anymore', 3 );
611 return $body;
612 } else {
613 // Log failed request and return false.
614 $this->criticalcss->log( 'criticalcss.com: GET results request for remote job id <' . $jobid . '> replied with code <' . $code . '> and an UNKNOWN error condition, body follows...', 2 );
615 $this->criticalcss->log( print_r( $body, true ), 2 );
616 return false;
617 }
618 } else {
619 // Response code is anything else
620 // Log failed request with a valid response code and return body.
621 if ( $code ) {
622 $this->criticalcss->log( 'criticalcss.com: GET results request for remote job id <' . $jobid . '> replied with error code <' . $code . '>, body follows...', 2 );
623 $this->criticalcss->log( print_r( $body, true ), 2 );
624 if ( 401 == $code ) {
625 // If request is unauthorized, also clear key status.
626 update_option( 'autoptimize_ccss_keyst', 1 );
627 $this->criticalcss->log( 'criticalcss.com: API key is invalid, updating key status', 3 );
628 }
629
630 // Return the request body.
631 return $body;
632 } else {
633 // Log failed request with no response and return false.
634 $this->criticalcss->log( 'criticalcss.com: GET results request for remote job id <' . $jobid . '> has no response, this could be a service timeout', 2 );
635 return false;
636 }
637 }
638 }
639
640 public function ao_ccss_save_file( $ccss, $target, $review ) {
641 // Save critical CSS into the filesystem and return its filename
642 // Prepare review mark.
643 if ( $review ) {
644 $rmark = '_R';
645 } else {
646 $rmark = '';
647 }
648
649 // Prepare target rule, filename and content.
650 $filename = false;
651 $content = $ccss;
652
653 if ( $this->criticalcss->check_contents( $content ) ) {
654 // Sanitize content, set filename and try to save file.
655 $file = AO_CCSS_DIR . 'ccss_' . md5( $ccss . $target[1] ) . $rmark . '.css';
656 $status = file_put_contents( $file, $content, LOCK_EX );
657 $filename = pathinfo( $file, PATHINFO_BASENAME );
658 $this->criticalcss->log( 'Critical CSS file for the rule <' . $target[0] . '|' . $target[1] . '> was saved as <' . $filename . '>, size in bytes is <' . $status . '>', 3 );
659
660 if ( ! $status ) {
661 // If file has not been saved, reset filename.
662 $this->criticalcss->log( 'Critical CSS file <' . $filename . '> could not be not saved', 2 );
663 $filename = false;
664 return $filename;
665 }
666 } else {
667 $this->criticalcss->log( 'Critical CSS received did not pass content check', 2 );
668 return $filename;
669 }
670
671 // Remove old critical CSS if a previous one existed in the rule and if that file exists in filesystem
672 // Attach required arrays.
673 $rules = $this->criticalcss->get_option( 'rules' );
674
675 // Only proceed if the rule already existed.
676 if ( array_key_exists( $target[1], $rules[ $target[0] ] ) ) {
677 $srule = $rules[ $target[0] ][ $target[1] ];
678 $oldfile = $srule['file'];
679
680 if ( $oldfile && $oldfile !== $filename ) {
681 $delfile = AO_CCSS_DIR . $oldfile;
682 if ( file_exists( $delfile ) ) {
683 $unlinkst = unlink( $delfile );
684 if ( $unlinkst ) {
685 $this->criticalcss->log( 'A previous critical CSS file <' . $oldfile . '> was removed for the rule <' . $target[0] . '|' . $target[1] . '>', 3 );
686 }
687 }
688 }
689 }
690
691 // Return filename or false.
692 return $filename;
693 }
694
695 public function ao_ccss_rule_update( $ljid, $srule, $file, $hash ) {
696 // Update or create a rule
697 // Attach required arrays.
698 $rules = $this->criticalcss->get_option( 'rules' );
699
700 // Prepare rule variables.
701 $trule = explode( '|', $srule );
702 if ( array_key_exists( $trule[1], $rules[$trule[0]] ) ) {
703 $rule = $rules[ $trule[0] ][ $trule[1] ];
704 } else {
705 $rule = array();
706 }
707 $action = false;
708 $rtype = '';
709
710 if ( is_array( $rule ) && 0 === $rule['hash'] && 0 !== $rule['file'] ) {
711 // manual rule, don't ever overwrite.
712 $action = 'NOT UPDATED';
713 $rtype = 'MANUAL';
714 } elseif ( is_array( $rule ) && 0 === $rule['hash'] && 0 === $rule['file'] ) {
715 // If this is an user created AUTO rule with no hash and file yet, update its hash and filename
716 // Set rule hash, file and action flag.
717 $rule['hash'] = $hash;
718 $rule['file'] = $file;
719 $action = 'UPDATED';
720 $rtype = 'AUTO';
721 } elseif ( is_array( $rule ) && 0 !== $rule['hash'] && ctype_alnum( $rule['hash'] ) ) {
722 // If this is an genuine AUTO rule, update its hash and filename
723 // Set rule hash, file and action flag.
724 $rule['hash'] = $hash;
725 $rule['file'] = $file;
726 $action = 'UPDATED';
727 $rtype = 'AUTO';
728 } else {
729 // If rule doesn't exist, create an AUTO rule
730 // AUTO rules were only for types, but will now also work for paths.
731 if ( ( 'types' == $trule[0] || 'paths' == $trule[0] ) && ! empty( $trule[1] ) ) {
732 // Set rule hash and file and action flag.
733 $rule['hash'] = $hash;
734 $rule['file'] = $file;
735 $action = 'CREATED';
736 $rtype = 'AUTO';
737 } else {
738 // Log that no rule was created.
739 $this->criticalcss->log( 'Exception, no AUTO rule created', 3 );
740 }
741 }
742
743 if ( $action ) {
744 // If a rule creation/update is required, persist updated rules object.
745 $rules[ $trule[0] ][ $trule[1] ] = $rule;
746 $rules_raw = json_encode( $rules );
747 update_option( 'autoptimize_ccss_rules', $rules_raw );
748 $this->criticalcss->flush_options();
749 $this->criticalcss->log( 'Target rule <' . $srule . '> of type <' . $rtype . '> was ' . $action . ' for job id <' . $ljid . '>', 3 );
750 } else {
751 $this->criticalcss->log( 'No rule action required', 3 );
752 }
753 }
754
755 function ao_ccss_finclude( $finclude_raw ) {
756 // Prepare forceInclude object.
757 if ( ! empty( $finclude_raw ) ) {
758 // If there are any content
759 // Convert raw string into arra and initialize the returning object.
760 $fincludes = explode( ',', $finclude_raw );
761 $finclude = array();
762
763 // Interacts over every rule.
764 $i = 0;
765 foreach ( $fincludes as $include ) {
766 // Trim leading and trailing whitespaces.
767 $include = trim( $include );
768
769 if ( substr( $include, 0, 2 ) === '//' ) {
770 // Regex rule
771 // Format value as required.
772 $include = str_replace( '//', '/', $include );
773 $include = $include . '/i';
774
775 // Store regex object.
776 $finclude[ $i ]['type'] = 'RegExp';
777 $finclude[ $i ]['value'] = $include;
778 } else {
779 // Simple value rule.
780 $finclude[ $i ]['value'] = $include;
781 }
782
783 $i++;
784 }
785
786 // Return forceInclude object.
787 return $finclude;
788 } else {
789 // Or just return false if empty.
790 return false;
791 }
792 }
793
794 public function ao_ccss_cleaning() {
795 // Perform plugin maintenance
796 // Truncate log file >= 1MB .
797 if ( file_exists( AO_CCSS_LOG ) ) {
798 if ( filesize( AO_CCSS_LOG ) >= 1048576 ) {
799 $logfile = fopen( AO_CCSS_LOG, 'w' );
800 fclose( $logfile );
801 }
802 }
803
804 // Remove lock file.
805 if ( file_exists( AO_CCSS_LOCK ) ) {
806 unlink( AO_CCSS_LOCK );
807 }
808
809 // Make sure queue processing is scheduled, recreate if not.
810 if ( ! wp_next_scheduled( 'ao_ccss_queue' ) ) {
811 wp_schedule_event( time(), apply_filters( 'ao_ccss_queue_schedule', 'ao_ccss' ), 'ao_ccss_queue' );
812 }
813
814 // Queue cleaning.
815 $queue = $this->criticalcss->get_option( 'queue' );
816
817 $queue_purge_threshold = 100;
818 $queue_purge_age = 24 * 60 * 60;
819 $queue_length = count( $queue );
820 $timestamp_yesterday = microtime( true ) - $queue_purge_age;
821 $remove_old_new = false;
822 $queue_altered = false;
823
824 if ( $queue_length > $queue_purge_threshold ) {
825 $remove_old_new = true;
826 }
827
828 foreach ( $queue as $path => $job ) {
829 if ( ( $remove_old_new && 'NEW' == $job['jqstat'] && $job['jctime'] < $timestamp_yesterday ) || in_array( $job['jqstat'], array( 'JOB_FAILED', 'STATUS_JOB_BAD', 'NO_CSS', 'NO_RESPONSE' ) ) ) {
830 unset( $queue[ $path ] );
831 $queue_altered = true;
832 }
833 }
834
835 // save queue to options!
836 if ( $queue_altered ) {
837 $queue_raw = json_encode( $queue );
838 update_option( 'autoptimize_ccss_queue', $queue_raw, false );
839 $this->criticalcss->log( 'Queue cleaning done.', 3 );
840 }
841
842 // re-check key if invalid.
843 $keyst = $this->criticalcss->get_option( 'keyst' );
844 if ( 1 == $keyst ) {
845 $this->ao_ccss_api_generate( '', '', '' );
846 }
847 }
848 }
849