PluginProbe
Autoptimize / 3.1.2
Autoptimize v3.1.2
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.1.2, at classes/autoptimizeCriticalCSSCron.php

857 lines 43.7 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 ( 0 == $rtimelimit ) {
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 do_action( 'autoptimize_action_ccss_cron_rule_saved', $jprops['rtarget'], $jprops['file'] );
235 $this->criticalcss->log( 'Job id <' . $jprops['ljid'] . '> result request successful, remote id <' . $jprops['jid'] . '>, status <' . $jprops['jqstat'] . '>, file saved <' . $jprops['file'] . '>', 3 );
236 } elseif ( 'GOOD' == $apireq['resultStatus'] && ( 'BAD' == $apireq['validationStatus'] || 'SCREENSHOT_WARN_BLANK' == $apireq['validationStatus'] ) ) {
237 // SUCCESS: GOOD job with BAD or SCREENSHOT_WARN_BLANK validation
238 // Update job properties.
239 $jprops['jqstat'] = $apireq['status'];
240 $jprops['jrstat'] = $apireq['resultStatus'];
241 $jprops['jvstat'] = $apireq['validationStatus'];
242 $jprops['jftime'] = microtime( true );
243 if ( apply_filters( 'autoptimize_filter_ccss_save_review_rules', true ) ) {
244 $jprops['file'] = $this->ao_ccss_save_file( $apireq['css'], $trule, true );
245 $rule_update = true;
246 do_action( 'autoptimize_action_ccss_cron_rule_saved', $jprops['rtarget'], $jprops['file'] );
247 $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 );
248 } else {
249 $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 );
250 }
251 } elseif ( 'GOOD' != $apireq['resultStatus'] && ( 'GOOD' != $apireq['validationStatus'] || 'WARN' != $apireq['validationStatus'] || 'BAD' != $apireq['validationStatus'] || 'SCREENSHOT_WARN_BLANK' != $apireq['validationStatus'] ) ) {
252 // ERROR: no GOOD, WARN or BAD results
253 // Update job properties.
254 $jprops['jqstat'] = $apireq['status'];
255 $jprops['jrstat'] = $apireq['resultStatus'];
256 $jprops['jvstat'] = $apireq['validationStatus'];
257 $jprops['jftime'] = microtime( true );
258 $this->criticalcss->log( 'Job id <' . $jprops['ljid'] . '> result request successful but job FAILED, status now is <' . $jprops['jqstat'] . '>', 3 );
259 $apireq['css'] = '/* critical css removed for DEBUG logging purposes */';
260 $this->criticalcss->log( 'Job response was: ' . json_encode( $apireq ), 3 );
261 } else {
262 // UNKNOWN: unhandled JOB_DONE exception
263 // Update job properties.
264 $jprops['jqstat'] = 'JOB_UNKNOWN';
265 $jprops['jrstat'] = $apireq['resultStatus'];
266 $jprops['jvstat'] = $apireq['validationStatus'];
267 $jprops['jftime'] = microtime( true );
268 $this->criticalcss->log( 'Job id <' . $jprops['ljid'] . '> result request successful but job is UNKNOWN, status now is <' . $jprops['jqstat'] . '>', 2 );
269 $apireq['css'] = '/* critical css removed for DEBUG logging purposes */';
270 $this->criticalcss->log( 'Job response was: ' . json_encode( $apireq ), 3 );
271 }
272 } elseif ( 'JOB_FAILED' == $apireq['job']['status'] || 'STATUS_JOB_BAD' == $apireq['job']['status'] ) {
273 // ERROR: failed job
274 // Update job properties.
275 $jprops['jqstat'] = $apireq['job']['status'];
276 if ( $apireq['job']['error'] ) {
277 $jprops['jrstat'] = $apireq['job']['error'];
278 } else {
279 $jprops['jrstat'] = 'Baby did a bad bad thing';
280 }
281 $jprops['jvstat'] = 'NONE';
282 $jprops['jftime'] = microtime( true );
283 $this->criticalcss->log( 'Job id <' . $jprops['ljid'] . '> result request successful but job FAILED, status now is <' . $jprops['jqstat'] . '>', 3 );
284 } elseif ( 'This css no longer exists. Please re-generate it.' == $apireq['error'] ) {
285 // ERROR: CSS doesn't exist
286 // Update job properties.
287 $jprops['jqstat'] = 'NO_CSS';
288 $jprops['jrstat'] = $apireq['error'];
289 $jprops['jvstat'] = 'NONE';
290 $jprops['jftime'] = microtime( true );
291 $this->criticalcss->log( 'Job id <' . $jprops['ljid'] . '> result request successful but job FAILED, status now is <' . $jprops['jqstat'] . '>', 3 );
292 } elseif ( empty( $apireq ) ) {
293 // ERROR: no response
294 // Update job properties.
295 $jprops['jqstat'] = 'NO_RESPONSE';
296 $jprops['jrstat'] = 'NONE';
297 $jprops['jvstat'] = 'NONE';
298 $jprops['jftime'] = microtime( true );
299 $this->criticalcss->log( 'Job id <' . $jprops['ljid'] . '> request has no response, status now is <' . $jprops['jqstat'] . '>', 3 );
300 } else {
301 // UNKNOWN: unhandled results exception
302 // Update job properties.
303 $jprops['jqstat'] = 'JOB_UNKNOWN';
304 $jprops['jrstat'] = 'NONE';
305 $jprops['jvstat'] = 'NONE';
306 $jprops['jftime'] = microtime( true );
307 $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 );
308 }
309
310 // Set queue update flag.
311 $update = true;
312 }
313
314 // Mark DONE jobs for removal.
315 if ( 'JOB_DONE' == $jprops['jqstat'] ) {
316 $update = true;
317 $deljob = true;
318 }
319
320 // Persist updated queue object.
321 if ( $update ) {
322 if ( ! $deljob ) {
323 // Update properties of a NEW or PENDING job...
324 $queue[ $path ] = $jprops;
325 } else {
326 // ...or remove the DONE job.
327 unset( $queue[ $path ] );
328 $this->criticalcss->log( 'Job id <' . $jprops['ljid'] . '> is DONE and was removed from the queue', 3 );
329 }
330
331 // Update queue object.
332 $queue_raw = json_encode( $queue );
333 update_option( 'autoptimize_ccss_queue', $queue_raw, false );
334 $this->criticalcss->log( 'Queue updated by job id <' . $jprops['ljid'] . '>', 3 );
335
336 // Update target rule.
337 if ( $rule_update ) {
338 $this->ao_ccss_rule_update( $jprops['ljid'], $jprops['rtarget'], $jprops['file'], $jprops['hash'] );
339 $this->criticalcss->log( 'Job id <' . $jprops['ljid'] . '> updated the target rule <' . $jprops['rtarget'] . '>', 3 );
340 }
341 } else {
342 // Or log no queue action.
343 $this->criticalcss->log( 'Nothing to do on this job', 3 );
344 }
345
346 // Break the loop if request time limit is (almost exceeded).
347 if ( time() > $mt ) {
348 $this->criticalcss->log( 'The time limit of ' . $rtimelimit . ' seconds was exceeded, queue control must finish now', 3 );
349 break;
350 }
351
352 // Increment job counter.
353 $jc++;
354 }
355
356 // Remove the lock file and log the queue end.
357 if ( file_exists( AO_CCSS_LOCK ) ) {
358 unlink( AO_CCSS_LOCK );
359 $this->criticalcss->log( 'Queue control unlocked', 3 );
360 }
361 $this->criticalcss->log( 'Queue control finished', 3 );
362
363 // Log that queue is locked.
364 } else {
365 $this->criticalcss->log( 'Queue is already running, skipping the attempt to run it again', 3 );
366 }
367 }
368
369 public function ao_ccss_diff_hashes( $ljid, $hash, $hashes, $rule ) {
370 // Compare job hashes
371 // STEP 1: update job hashes.
372 if ( 1 == count( $hashes ) ) {
373 // Job with a single hash
374 // Set job hash.
375 $hash = $hashes[0];
376 $this->criticalcss->log( 'Job id <' . $ljid . '> updated with SINGLE hash <' . $hash . '>', 3 );
377 } else {
378 // Job with multiple hashes
379 // Loop through hashes to concatenate them.
380 $nhash = '';
381 foreach ( $hashes as $shash ) {
382 $nhash .= $shash;
383 }
384
385 // Set job hash.
386 $hash = md5( $nhash );
387 $this->criticalcss->log( 'Job id <' . $ljid . '> updated with a COMPOSITE hash <' . $hash . '>', 3 );
388 }
389
390 // STEP 2: compare job to existing jobs to prevent double submission for same type+hash.
391 $queue = $this->criticalcss->get_option( 'queue' );
392
393 foreach ( $queue as $queue_item ) {
394 $this->criticalcss->log( 'Comparing <' . $rule . $hash . '> with <' . $queue_item['rtarget'] . $queue_item['hash'] . '>', 3 );
395 if ( $queue_item['hash'] == $hash && $queue_item['rtarget'] == $rule && in_array( $queue_item['jqstat'], array( 'JOB_QUEUED', 'JOB_ONGOING', 'JOB_DONE' ) ) ) {
396 $this->criticalcss->log( 'Job id <' . $ljid . '> matches the already pending job <' . $queue_item['ljid'] . '>', 3 );
397 return false;
398 }
399 }
400
401 // STEP 3: compare job and existing rule (if any) hashes
402 // Attach required arrays.
403 $rules = $this->criticalcss->get_option( 'rules' );
404
405 // Prepare rule variables.
406 $trule = explode( '|', $rule );
407 if ( is_array( $trule ) && ! empty( $trule ) && array_key_exists( $trule[1], $rules[ $trule[0] ] ) ) {
408 $srule = $rules[ $trule[0] ][ $trule[1] ];
409 } else {
410 $srule = '';
411 }
412
413 // If hash is empty, set it to now for a "forced job".
414 if ( empty( $hash ) ) {
415 $hash = 'new';
416 $this->criticalcss->log( 'Job id <' . $ljid . '> had no hash, assuming forced job so setting hash to new', 3 );
417 }
418
419 // Check if a MANUAL rule exist and return false.
420 if ( ! empty( $srule ) && ( 0 == $srule['hash'] && 0 != $srule['file'] ) ) {
421 $this->criticalcss->log( 'Job id <' . $ljid . '> matches the MANUAL rule <' . $trule[0] . '|' . $trule[1] . '>', 3 );
422 return false;
423 } elseif ( ! empty( $srule ) ) {
424 // Check if an AUTO rule exist.
425 if ( $hash === $srule['hash'] && is_file( AO_CCSS_DIR . $srule['file'] ) && 0 != filesize( AO_CCSS_DIR . $srule['file'] ) ) {
426 // Check if job hash matches rule, if the CCSS file exists said file is not empty and return FALSE is so.
427 $this->criticalcss->log( 'Job id <' . $ljid . '> with hash <' . $hash . '> MATCH the one in rule <' . $trule[0] . '|' . $trule[1] . '>', 3 );
428 return false;
429 } else {
430 // Or return the new hash if they differ.
431 $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 );
432 return $hash;
433 }
434 } else {
435 // Return the hash for a job that has no rule yet.
436 $this->criticalcss->log( 'Job id <' . $ljid . '> with hash <' . $hash . '> has no rule yet', 3 );
437 return $hash;
438 }
439 }
440
441 public function ao_ccss_api_generate( $path, $debug, $dcode ) {
442 // POST jobs to criticalcss.com and return responses
443 // Get key and key status.
444 $key = $this->criticalcss->get_option( 'key' );
445 $key_status = $this->criticalcss->get_option( 'keyst' );
446 $noptimize = $this->criticalcss->get_option( 'noptimize' );
447
448 // Prepare full URL to request.
449 $site_host = get_site_url();
450 $site_path = parse_url( $site_host, PHP_URL_PATH );
451
452 if ( ! empty( $site_path ) ) {
453 $site_host = str_replace( $site_path, '', $site_host );
454 }
455
456 // Logic to bind to one domain to avoid site clones of sites would
457 // automatically begin spawning requests to criticalcss.com which has
458 // a per domain cost.
459 $domain = $this->criticalcss->get_option( 'domain' );
460 if ( empty( $domain ) ) {
461 // first request being done, update option to allow future requests are only allowed if from same domain.
462 update_option( 'autoptimize_ccss_domain', str_rot13( $site_host ) );
463 } 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 ) ) {
464 // not the same domain, log as error and return without posting to criticalcss.com.
465 $this->criticalcss->log( 'Request for domain ' . $site_host . ' does not match bound domain ' . $domain . ' so not proceeding.', 2 );
466 return false;
467 }
468
469 $src_url = $site_host . $path;
470
471 // Avoid AO optimizations if required by config or avoid lazyload if lazyload is active in AO.
472 if ( ! empty( $noptimize ) ) {
473 $src_url .= '?ao_noptirocket=1';
474 } elseif ( ( class_exists( 'autoptimizeImages', false ) && autoptimizeImages::should_lazyload_wrapper() ) || apply_filters( 'autoptimize_filter_ccss_enforce_nolazy', false ) ) {
475 $src_url .= '?ao_nolazy=1';
476 }
477
478 $src_url = apply_filters( 'autoptimize_filter_ccss_cron_srcurl', $src_url );
479
480 // Initialize request body.
481 $body = array();
482 $body['url'] = $src_url;
483 $body['aff'] = 1;
484 $body['aocssv'] = AO_CCSS_VER;
485
486 // Prepare and add viewport size to the body if available.
487 $viewport = $this->criticalcss->viewport();
488 if ( ! empty( $viewport['w'] ) && ! empty( $viewport['h'] ) ) {
489 $body['width'] = $viewport['w'];
490 $body['height'] = $viewport['h'];
491 }
492
493 // Prepare and add forceInclude to the body if available.
494 $finclude = $this->criticalcss->get_option( 'finclude' );
495 $finclude = $this->ao_ccss_finclude( $finclude );
496 if ( ! empty( $finclude ) ) {
497 $body['forceInclude'] = $finclude;
498 }
499
500 // Add filter to allow the body array to be altered (e.g. to add customPageHeaders).
501 $body = apply_filters( 'autoptimize_ccss_cron_api_generate_body', $body );
502
503 // Body must be json and log it.
504 $body = json_encode( $body );
505 $this->criticalcss->log( 'criticalcss.com: POST generate request body is ' . $body, 3 );
506
507 // Prepare the request.
508 $url = esc_url_raw( AO_CCSS_API . 'generate?aover=' . AO_CCSS_VER );
509 $args = array(
510 'headers' => apply_filters(
511 'autoptimize_ccss_cron_api_generate_headers',
512 array(
513 'User-Agent' => 'Autoptimize v' . AO_CCSS_VER,
514 'Content-type' => 'application/json; charset=utf-8',
515 'Authorization' => 'JWT ' . $key,
516 'Connection' => 'close',
517 )
518 ),
519 'body' => $body,
520 );
521
522 // Dispatch the request and store its response code.
523 $req = wp_safe_remote_post( $url, $args );
524 $code = wp_remote_retrieve_response_code( $req );
525 $body = json_decode( wp_remote_retrieve_body( $req ), true );
526
527 if ( $debug && $dcode ) {
528 // If queue debug is active, change response code.
529 $code = $dcode;
530 }
531
532 if ( 200 == $code ) {
533 // Response code is OK.
534 // Workaround criticalcss.com non-RESTful reponses.
535 if ( 'JOB_QUEUED' == $body['job']['status'] || 'JOB_ONGOING' == $body['job']['status'] || 'STATUS_JOB_BAD' == $body['job']['status'] ) {
536 // Log successful and return encoded request body.
537 $this->criticalcss->log( 'criticalcss.com: POST generate request for path <' . $src_url . '> replied successfully', 3 );
538
539 // This code also means the key is valid, so cache key status for 24h if not already cached.
540 if ( ( ! $key_status || 2 != $key_status ) && $key ) {
541 update_option( 'autoptimize_ccss_keyst', 2 );
542 $this->criticalcss->log( 'criticalcss.com: API key is valid, updating key status', 3 );
543 }
544
545 // Return the request body.
546 return $body;
547 } else {
548 // Log successful requests with invalid reponses.
549 $this->criticalcss->log( 'criticalcss.com: POST generate request for path <' . $src_url . '> replied with code <' . $code . '> and an UNKNOWN error condition, body follows...', 2 );
550 $this->criticalcss->log( print_r( $body, true ), 2 );
551 return $body;
552 }
553 } else {
554 // Response code is anything else.
555 // Log failed request with a valid response code and return body.
556 if ( $code ) {
557 $this->criticalcss->log( 'criticalcss.com: POST generate request for path <' . $src_url . '> replied with error code <' . $code . '>, body follows...', 2 );
558 $this->criticalcss->log( print_r( $body, true ), 2 );
559
560 if ( 401 == $code ) {
561 // If request is unauthorized, also clear key status.
562 update_option( 'autoptimize_ccss_keyst', 1 );
563 $this->criticalcss->log( 'criticalcss.com: API key is invalid, updating key status', 3 );
564 }
565
566 // Return the request body.
567 return $body;
568 } else {
569 // Log failed request with no response and return false.
570 $this->criticalcss->log( 'criticalcss.com: POST generate request for path <' . $src_url . '> has no response, this could be a service timeout', 2 );
571 if ( is_wp_error( $req ) ) {
572 $this->criticalcss->log( $req->get_error_message(), 2 );
573 }
574
575 return false;
576 }
577 }
578 }
579
580 public function ao_ccss_api_results( $jobid, $debug, $dcode ) {
581 // GET jobs from criticalcss.com and return responses
582 // Get key.
583 $key = $this->criticalcss->get_option( 'key' );
584
585 // Prepare the request.
586 $url = AO_CCSS_API . 'results?resultId=' . $jobid;
587 $args = array(
588 'headers' => apply_filters(
589 'autoptimize_ccss_cron_api_generate_headers',
590 array(
591 'User-Agent' => 'Autoptimize CriticalCSS Power-Up v' . AO_CCSS_VER,
592 'Authorization' => 'JWT ' . $key,
593 'Connection' => 'close',
594 )
595 ),
596 );
597
598 // Dispatch the request and store its response code.
599 $req = wp_safe_remote_get( $url, $args );
600 $code = wp_remote_retrieve_response_code( $req );
601 $body = json_decode( wp_remote_retrieve_body( $req ), true );
602
603 if ( $debug && $dcode ) {
604 // If queue debug is active, change response code.
605 $code = $dcode;
606 }
607
608 if ( 200 == $code ) {
609 // Response code is OK.
610 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'] ) ) {
611 // Workaround criticalcss.com non-RESTful reponses
612 // Log successful and return encoded request body.
613 $this->criticalcss->log( 'criticalcss.com: GET results request for remote job id <' . $jobid . '> replied successfully', 3 );
614 return $body;
615 } elseif ( is_array( $body ) && ( array_key_exists( 'error', $body ) && 'This css no longer exists. Please re-generate it.' == $body['error'] ) ) {
616 // Handle no CSS reply
617 // Log no CSS error and return encoded request body.
618 $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 );
619 return $body;
620 } else {
621 // Log failed request and return false.
622 $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 );
623 $this->criticalcss->log( print_r( $body, true ), 2 );
624 return false;
625 }
626 } else {
627 // Response code is anything else
628 // Log failed request with a valid response code and return body.
629 if ( $code ) {
630 $this->criticalcss->log( 'criticalcss.com: GET results request for remote job id <' . $jobid . '> replied with error code <' . $code . '>, body follows...', 2 );
631 $this->criticalcss->log( print_r( $body, true ), 2 );
632 if ( 401 == $code ) {
633 // If request is unauthorized, also clear key status.
634 update_option( 'autoptimize_ccss_keyst', 1 );
635 $this->criticalcss->log( 'criticalcss.com: API key is invalid, updating key status', 3 );
636 }
637
638 // Return the request body.
639 return $body;
640 } else {
641 // Log failed request with no response and return false.
642 $this->criticalcss->log( 'criticalcss.com: GET results request for remote job id <' . $jobid . '> has no response, this could be a service timeout', 2 );
643 return false;
644 }
645 }
646 }
647
648 public function ao_ccss_save_file( $ccss, $target, $review ) {
649 // Save critical CSS into the filesystem and return its filename
650 // Prepare review mark.
651 if ( $review ) {
652 $rmark = '_R';
653 } else {
654 $rmark = '';
655 }
656
657 // Prepare target rule, filename and content.
658 $filename = false;
659 $content = $ccss;
660
661 if ( $this->criticalcss->check_contents( $content ) ) {
662 // Sanitize content, set filename and try to save file.
663 $file = AO_CCSS_DIR . 'ccss_' . md5( $ccss . $target[1] ) . $rmark . '.css';
664 $status = file_put_contents( $file, $content, LOCK_EX );
665 $filename = pathinfo( $file, PATHINFO_BASENAME );
666 $this->criticalcss->log( 'Critical CSS file for the rule <' . $target[0] . '|' . $target[1] . '> was saved as <' . $filename . '>, size in bytes is <' . $status . '>', 3 );
667
668 if ( ! $status ) {
669 // If file has not been saved, reset filename.
670 $this->criticalcss->log( 'Critical CSS file <' . $filename . '> could not be not saved', 2 );
671 $filename = false;
672 return $filename;
673 }
674 } else {
675 $this->criticalcss->log( 'Critical CSS received did not pass content check', 2 );
676 return $filename;
677 }
678
679 // Remove old critical CSS if a previous one existed in the rule and if that file exists in filesystem
680 // Attach required arrays.
681 $rules = $this->criticalcss->get_option( 'rules' );
682
683 // Only proceed if the rule already existed.
684 if ( array_key_exists( $target[1], $rules[ $target[0] ] ) ) {
685 $srule = $rules[ $target[0] ][ $target[1] ];
686 $oldfile = $srule['file'];
687
688 if ( $oldfile && $oldfile !== $filename ) {
689 $delfile = AO_CCSS_DIR . $oldfile;
690 if ( file_exists( $delfile ) ) {
691 $unlinkst = unlink( $delfile );
692 if ( $unlinkst ) {
693 $this->criticalcss->log( 'A previous critical CSS file <' . $oldfile . '> was removed for the rule <' . $target[0] . '|' . $target[1] . '>', 3 );
694 }
695 }
696 }
697 }
698
699 // Return filename or false.
700 return $filename;
701 }
702
703 public function ao_ccss_rule_update( $ljid, $srule, $file, $hash ) {
704 // Update or create a rule
705 // Attach required arrays.
706 $rules = $this->criticalcss->get_option( 'rules' );
707
708 // Prepare rule variables.
709 $trule = explode( '|', $srule );
710 if ( array_key_exists( $trule[1], $rules[ $trule[0] ] ) ) {
711 $rule = $rules[ $trule[0] ][ $trule[1] ];
712 } else {
713 $rule = array();
714 }
715 $action = false;
716 $rtype = '';
717
718 if ( is_array( $rule ) && 0 === $rule['hash'] && 0 !== $rule['file'] ) {
719 // manual rule, don't ever overwrite.
720 $action = 'NOT UPDATED';
721 $rtype = 'MANUAL';
722 } elseif ( is_array( $rule ) && 0 === $rule['hash'] && 0 === $rule['file'] ) {
723 // If this is an user created AUTO rule with no hash and file yet, update its hash and filename
724 // Set rule hash, file and action flag.
725 $rule['hash'] = $hash;
726 $rule['file'] = $file;
727 $action = 'UPDATED';
728 $rtype = 'AUTO';
729 } elseif ( is_array( $rule ) && 0 !== $rule['hash'] && ctype_alnum( $rule['hash'] ) ) {
730 // If this is an genuine AUTO rule, update its hash and filename
731 // Set rule hash, file and action flag.
732 $rule['hash'] = $hash;
733 $rule['file'] = $file;
734 $action = 'UPDATED';
735 $rtype = 'AUTO';
736 } else {
737 // If rule doesn't exist, create an AUTO rule
738 // AUTO rules were only for types, but will now also work for paths.
739 if ( ( 'types' == $trule[0] || 'paths' == $trule[0] ) && ! empty( $trule[1] ) ) {
740 // Set rule hash and file and action flag.
741 $rule['hash'] = $hash;
742 $rule['file'] = $file;
743 $action = 'CREATED';
744 $rtype = 'AUTO';
745 } else {
746 // Log that no rule was created.
747 $this->criticalcss->log( 'Exception, no AUTO rule created', 3 );
748 }
749 }
750
751 if ( $action ) {
752 // If a rule creation/update is required, persist updated rules object.
753 $rules[ $trule[0] ][ $trule[1] ] = $rule;
754 $rules_raw = json_encode( $rules );
755 update_option( 'autoptimize_ccss_rules', $rules_raw );
756 $this->criticalcss->flush_options();
757 $this->criticalcss->log( 'Target rule <' . $srule . '> of type <' . $rtype . '> was ' . $action . ' for job id <' . $ljid . '>', 3 );
758 } else {
759 $this->criticalcss->log( 'No rule action required', 3 );
760 }
761 }
762
763 function ao_ccss_finclude( $finclude_raw ) {
764 // Prepare forceInclude object.
765 if ( ! empty( $finclude_raw ) ) {
766 // If there are any content
767 // Convert raw string into arra and initialize the returning object.
768 $fincludes = explode( ',', $finclude_raw );
769 $finclude = array();
770
771 // Interacts over every rule.
772 $i = 0;
773 foreach ( $fincludes as $include ) {
774 // Trim leading and trailing whitespaces.
775 $include = trim( $include );
776
777 if ( substr( $include, 0, 2 ) === '//' ) {
778 // Regex rule
779 // Format value as required.
780 $include = str_replace( '//', '/', $include );
781 $include = $include . '/i';
782
783 // Store regex object.
784 $finclude[ $i ]['type'] = 'RegExp';
785 $finclude[ $i ]['value'] = $include;
786 } else {
787 // Simple value rule.
788 $finclude[ $i ]['value'] = $include;
789 }
790
791 $i++;
792 }
793
794 // Return forceInclude object.
795 return $finclude;
796 } else {
797 // Or just return false if empty.
798 return false;
799 }
800 }
801
802 public function ao_ccss_cleaning() {
803 // Perform plugin maintenance
804 // Truncate log file >= 1MB .
805 if ( file_exists( AO_CCSS_LOG ) ) {
806 if ( filesize( AO_CCSS_LOG ) >= 1048576 ) {
807 $logfile = fopen( AO_CCSS_LOG, 'w' );
808 fclose( $logfile );
809 }
810 }
811
812 // Remove lock file.
813 if ( file_exists( AO_CCSS_LOCK ) ) {
814 unlink( AO_CCSS_LOCK );
815 }
816
817 // Make sure queue processing is scheduled, recreate if not.
818 if ( ! wp_next_scheduled( 'ao_ccss_queue' ) ) {
819 wp_schedule_event( time(), apply_filters( 'ao_ccss_queue_schedule', 'ao_ccss' ), 'ao_ccss_queue' );
820 }
821
822 // Queue cleaning.
823 $queue = $this->criticalcss->get_option( 'queue' );
824
825 $queue_purge_threshold = 100;
826 $queue_purge_age = 24 * 60 * 60;
827 $queue_length = count( $queue );
828 $timestamp_yesterday = microtime( true ) - $queue_purge_age;
829 $remove_old_new = false;
830 $queue_altered = false;
831
832 if ( $queue_length > $queue_purge_threshold ) {
833 $remove_old_new = true;
834 }
835
836 foreach ( $queue as $path => $job ) {
837 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' ) ) ) {
838 unset( $queue[ $path ] );
839 $queue_altered = true;
840 }
841 }
842
843 // save queue to options!
844 if ( $queue_altered ) {
845 $queue_raw = json_encode( $queue );
846 update_option( 'autoptimize_ccss_queue', $queue_raw, false );
847 $this->criticalcss->log( 'Queue cleaning done.', 3 );
848 }
849
850 // re-check key if invalid.
851 $keyst = $this->criticalcss->get_option( 'keyst' );
852 if ( 1 == $keyst ) {
853 $this->ao_ccss_api_generate( '', '', '' );
854 }
855 }
856 }
857