PluginProbe
Autoptimize / 3.1.4
Autoptimize v3.1.4
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.4, at classes/autoptimizeCriticalCSSCron.php

863 lines 44.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 ( 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 if ( true !== autoptimizeUtils::is_local_server( parse_url( $src_url, PHP_URL_HOST ) ) ) {
481 // Initialize request body.
482 $body = array();
483 $body['url'] = $src_url;
484 $body['aff'] = 1;
485 $body['aocssv'] = AO_CCSS_VER;
486
487 // Prepare and add viewport size to the body if available.
488 $viewport = $this->criticalcss->viewport();
489 if ( ! empty( $viewport['w'] ) && ! empty( $viewport['h'] ) ) {
490 $body['width'] = $viewport['w'];
491 $body['height'] = $viewport['h'];
492 }
493
494 // Prepare and add forceInclude to the body if available.
495 $finclude = $this->criticalcss->get_option( 'finclude' );
496 $finclude = $this->ao_ccss_finclude( $finclude );
497 if ( ! empty( $finclude ) ) {
498 $body['forceInclude'] = $finclude;
499 }
500
501 // Add filter to allow the body array to be altered (e.g. to add customPageHeaders).
502 $body = apply_filters( 'autoptimize_ccss_cron_api_generate_body', $body );
503
504 // Body must be json and log it.
505 $body = json_encode( $body, JSON_UNESCAPED_SLASHES );
506 $this->criticalcss->log( 'criticalcss.com: POST generate request body is ' . $body, 3 );
507
508 // Prepare the request.
509 $url = esc_url_raw( AO_CCSS_API . 'generate?aover=' . AO_CCSS_VER );
510 $args = array(
511 'headers' => apply_filters(
512 'autoptimize_ccss_cron_api_generate_headers',
513 array(
514 'User-Agent' => 'Autoptimize v' . AO_CCSS_VER,
515 'Content-type' => 'application/json; charset=utf-8',
516 'Authorization' => 'JWT ' . $key,
517 'Connection' => 'close',
518 )
519 ),
520 'body' => $body,
521 );
522
523 // Dispatch the request and store its response code.
524 $req = wp_safe_remote_post( $url, $args );
525 $code = wp_remote_retrieve_response_code( $req );
526 $body = json_decode( wp_remote_retrieve_body( $req ), true );
527
528 if ( $debug && $dcode ) {
529 // If queue debug is active, change response code.
530 $code = $dcode;
531 }
532
533 if ( 200 == $code ) {
534 // Response code is OK.
535 // Workaround criticalcss.com non-RESTful reponses.
536 if ( 'JOB_QUEUED' == $body['job']['status'] || 'JOB_ONGOING' == $body['job']['status'] || 'STATUS_JOB_BAD' == $body['job']['status'] ) {
537 // Log successful and return encoded request body.
538 $this->criticalcss->log( 'criticalcss.com: POST generate request for path <' . $src_url . '> replied successfully', 3 );
539
540 // This code also means the key is valid, so cache key status for 24h if not already cached.
541 if ( ( ! $key_status || 2 != $key_status ) && $key ) {
542 update_option( 'autoptimize_ccss_keyst', 2 );
543 $this->criticalcss->log( 'criticalcss.com: API key is valid, updating key status', 3 );
544 }
545
546 // Return the request body.
547 return $body;
548 } else {
549 // Log successful requests with invalid reponses.
550 $this->criticalcss->log( 'criticalcss.com: POST generate request for path <' . $src_url . '> replied with code <' . $code . '> and an UNKNOWN error condition, body follows...', 2 );
551 $this->criticalcss->log( print_r( $body, true ), 2 );
552 return $body;
553 }
554 } else {
555 // Response code is anything else.
556 // Log failed request with a valid response code and return body.
557 if ( $code ) {
558 $this->criticalcss->log( 'criticalcss.com: POST generate request for path <' . $src_url . '> replied with error code <' . $code . '>, body follows...', 2 );
559 $this->criticalcss->log( print_r( $body, true ), 2 );
560
561 if ( 401 == $code ) {
562 // If request is unauthorized, also clear key status.
563 update_option( 'autoptimize_ccss_keyst', 1 );
564 $this->criticalcss->log( 'criticalcss.com: API key is invalid, updating key status', 3 );
565 }
566
567 // Return the request body.
568 return $body;
569 } else {
570 // Log failed request with no response and return false.
571 $this->criticalcss->log( 'criticalcss.com: POST generate request for path <' . $src_url . '> has no response, this could be a service timeout', 2 );
572 if ( is_wp_error( $req ) ) {
573 $this->criticalcss->log( $req->get_error_message(), 2 );
574 }
575
576 return false;
577 }
578 }
579 } else {
580 // localhost/ private network server, no CCSS possible.
581 $this->criticalcss->log( 'ccss cron: job not created at ccss.com as for local server', 3 );
582 return false;
583 }
584 }
585
586 public function ao_ccss_api_results( $jobid, $debug, $dcode ) {
587 // GET jobs from criticalcss.com and return responses
588 // Get key.
589 $key = $this->criticalcss->get_option( 'key' );
590
591 // Prepare the request.
592 $url = AO_CCSS_API . 'results?resultId=' . $jobid;
593 $args = array(
594 'headers' => apply_filters(
595 'autoptimize_ccss_cron_api_generate_headers',
596 array(
597 'User-Agent' => 'Autoptimize CriticalCSS Power-Up v' . AO_CCSS_VER,
598 'Authorization' => 'JWT ' . $key,
599 'Connection' => 'close',
600 )
601 ),
602 );
603
604 // Dispatch the request and store its response code.
605 $req = wp_safe_remote_get( $url, $args );
606 $code = wp_remote_retrieve_response_code( $req );
607 $body = json_decode( wp_remote_retrieve_body( $req ), true );
608
609 if ( $debug && $dcode ) {
610 // If queue debug is active, change response code.
611 $code = $dcode;
612 }
613
614 if ( 200 == $code ) {
615 // Response code is OK.
616 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'] ) ) {
617 // Workaround criticalcss.com non-RESTful reponses
618 // Log successful and return encoded request body.
619 $this->criticalcss->log( 'criticalcss.com: GET results request for remote job id <' . $jobid . '> replied successfully', 3 );
620 return $body;
621 } elseif ( is_array( $body ) && ( array_key_exists( 'error', $body ) && 'This css no longer exists. Please re-generate it.' == $body['error'] ) ) {
622 // Handle no CSS reply
623 // Log no CSS error and return encoded request body.
624 $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 );
625 return $body;
626 } else {
627 // Log failed request and return false.
628 $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 );
629 $this->criticalcss->log( print_r( $body, true ), 2 );
630 return false;
631 }
632 } else {
633 // Response code is anything else
634 // Log failed request with a valid response code and return body.
635 if ( $code ) {
636 $this->criticalcss->log( 'criticalcss.com: GET results request for remote job id <' . $jobid . '> replied with error code <' . $code . '>, body follows...', 2 );
637 $this->criticalcss->log( print_r( $body, true ), 2 );
638 if ( 401 == $code ) {
639 // If request is unauthorized, also clear key status.
640 update_option( 'autoptimize_ccss_keyst', 1 );
641 $this->criticalcss->log( 'criticalcss.com: API key is invalid, updating key status', 3 );
642 }
643
644 // Return the request body.
645 return $body;
646 } else {
647 // Log failed request with no response and return false.
648 $this->criticalcss->log( 'criticalcss.com: GET results request for remote job id <' . $jobid . '> has no response, this could be a service timeout', 2 );
649 return false;
650 }
651 }
652 }
653
654 public function ao_ccss_save_file( $ccss, $target, $review ) {
655 // Save critical CSS into the filesystem and return its filename
656 // Prepare review mark.
657 if ( $review ) {
658 $rmark = '_R';
659 } else {
660 $rmark = '';
661 }
662
663 // Prepare target rule, filename and content.
664 $filename = false;
665 $content = $ccss;
666
667 if ( $this->criticalcss->check_contents( $content ) ) {
668 // Sanitize content, set filename and try to save file.
669 $file = AO_CCSS_DIR . 'ccss_' . md5( $ccss . $target[1] ) . $rmark . '.css';
670 $status = file_put_contents( $file, $content, LOCK_EX );
671 $filename = pathinfo( $file, PATHINFO_BASENAME );
672 $this->criticalcss->log( 'Critical CSS file for the rule <' . $target[0] . '|' . $target[1] . '> was saved as <' . $filename . '>, size in bytes is <' . $status . '>', 3 );
673
674 if ( ! $status ) {
675 // If file has not been saved, reset filename.
676 $this->criticalcss->log( 'Critical CSS file <' . $filename . '> could not be not saved', 2 );
677 $filename = false;
678 return $filename;
679 }
680 } else {
681 $this->criticalcss->log( 'Critical CSS received did not pass content check', 2 );
682 return $filename;
683 }
684
685 // Remove old critical CSS if a previous one existed in the rule and if that file exists in filesystem
686 // Attach required arrays.
687 $rules = $this->criticalcss->get_option( 'rules' );
688
689 // Only proceed if the rule already existed.
690 if ( array_key_exists( $target[1], $rules[ $target[0] ] ) ) {
691 $srule = $rules[ $target[0] ][ $target[1] ];
692 $oldfile = $srule['file'];
693
694 if ( $oldfile && $oldfile !== $filename ) {
695 $delfile = AO_CCSS_DIR . $oldfile;
696 if ( file_exists( $delfile ) ) {
697 $unlinkst = unlink( $delfile );
698 if ( $unlinkst ) {
699 $this->criticalcss->log( 'A previous critical CSS file <' . $oldfile . '> was removed for the rule <' . $target[0] . '|' . $target[1] . '>', 3 );
700 }
701 }
702 }
703 }
704
705 // Return filename or false.
706 return $filename;
707 }
708
709 public function ao_ccss_rule_update( $ljid, $srule, $file, $hash ) {
710 // Update or create a rule
711 // Attach required arrays.
712 $rules = $this->criticalcss->get_option( 'rules' );
713
714 // Prepare rule variables.
715 $trule = explode( '|', $srule );
716 if ( array_key_exists( $trule[1], $rules[ $trule[0] ] ) ) {
717 $rule = $rules[ $trule[0] ][ $trule[1] ];
718 } else {
719 $rule = array();
720 }
721 $action = false;
722 $rtype = '';
723
724 if ( is_array( $rule ) && 0 === $rule['hash'] && 0 !== $rule['file'] ) {
725 // manual rule, don't ever overwrite.
726 $action = 'NOT UPDATED';
727 $rtype = 'MANUAL';
728 } elseif ( is_array( $rule ) && 0 === $rule['hash'] && 0 === $rule['file'] ) {
729 // If this is an user created AUTO rule with no hash and file yet, update its hash and filename
730 // Set rule hash, file and action flag.
731 $rule['hash'] = $hash;
732 $rule['file'] = $file;
733 $action = 'UPDATED';
734 $rtype = 'AUTO';
735 } elseif ( is_array( $rule ) && 0 !== $rule['hash'] && ctype_alnum( $rule['hash'] ) ) {
736 // If this is an genuine AUTO rule, update its hash and filename
737 // Set rule hash, file and action flag.
738 $rule['hash'] = $hash;
739 $rule['file'] = $file;
740 $action = 'UPDATED';
741 $rtype = 'AUTO';
742 } else {
743 // If rule doesn't exist, create an AUTO rule
744 // AUTO rules were only for types, but will now also work for paths.
745 if ( ( 'types' == $trule[0] || 'paths' == $trule[0] ) && ! empty( $trule[1] ) ) {
746 // Set rule hash and file and action flag.
747 $rule['hash'] = $hash;
748 $rule['file'] = $file;
749 $action = 'CREATED';
750 $rtype = 'AUTO';
751 } else {
752 // Log that no rule was created.
753 $this->criticalcss->log( 'Exception, no AUTO rule created', 3 );
754 }
755 }
756
757 if ( $action ) {
758 // If a rule creation/update is required, persist updated rules object.
759 $rules[ $trule[0] ][ $trule[1] ] = $rule;
760 $rules_raw = json_encode( $rules );
761 update_option( 'autoptimize_ccss_rules', $rules_raw );
762 $this->criticalcss->flush_options();
763 $this->criticalcss->log( 'Target rule <' . $srule . '> of type <' . $rtype . '> was ' . $action . ' for job id <' . $ljid . '>', 3 );
764 } else {
765 $this->criticalcss->log( 'No rule action required', 3 );
766 }
767 }
768
769 function ao_ccss_finclude( $finclude_raw ) {
770 // Prepare forceInclude object.
771 if ( ! empty( $finclude_raw ) ) {
772 // If there are any content
773 // Convert raw string into arra and initialize the returning object.
774 $fincludes = explode( ',', $finclude_raw );
775 $finclude = array();
776
777 // Interacts over every rule.
778 $i = 0;
779 foreach ( $fincludes as $include ) {
780 // Trim leading and trailing whitespaces.
781 $include = trim( $include );
782
783 if ( substr( $include, 0, 2 ) === '//' ) {
784 // Regex rule
785 // Format value as required.
786 $include = str_replace( '//', '/', $include );
787 $include = $include . '/i';
788
789 // Store regex object.
790 $finclude[ $i ]['type'] = 'RegExp';
791 $finclude[ $i ]['value'] = $include;
792 } else {
793 // Simple value rule.
794 $finclude[ $i ]['value'] = $include;
795 }
796
797 $i++;
798 }
799
800 // Return forceInclude object.
801 return $finclude;
802 } else {
803 // Or just return false if empty.
804 return false;
805 }
806 }
807
808 public function ao_ccss_cleaning() {
809 // Perform plugin maintenance
810 // Truncate log file >= 1MB .
811 if ( file_exists( AO_CCSS_LOG ) ) {
812 if ( filesize( AO_CCSS_LOG ) >= 1048576 ) {
813 $logfile = fopen( AO_CCSS_LOG, 'w' );
814 fclose( $logfile );
815 }
816 }
817
818 // Remove lock file.
819 if ( file_exists( AO_CCSS_LOCK ) ) {
820 unlink( AO_CCSS_LOCK );
821 }
822
823 // Make sure queue processing is scheduled, recreate if not.
824 if ( ! wp_next_scheduled( 'ao_ccss_queue' ) ) {
825 wp_schedule_event( time(), apply_filters( 'ao_ccss_queue_schedule', 'ao_ccss' ), 'ao_ccss_queue' );
826 }
827
828 // Queue cleaning.
829 $queue = $this->criticalcss->get_option( 'queue' );
830
831 $queue_purge_threshold = 100;
832 $queue_purge_age = 24 * 60 * 60;
833 $queue_length = count( $queue );
834 $timestamp_yesterday = microtime( true ) - $queue_purge_age;
835 $remove_old_new = false;
836 $queue_altered = false;
837
838 if ( $queue_length > $queue_purge_threshold ) {
839 $remove_old_new = true;
840 }
841
842 foreach ( $queue as $path => $job ) {
843 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' ) ) ) {
844 unset( $queue[ $path ] );
845 $queue_altered = true;
846 }
847 }
848
849 // save queue to options!
850 if ( $queue_altered ) {
851 $queue_raw = json_encode( $queue );
852 update_option( 'autoptimize_ccss_queue', $queue_raw, false );
853 $this->criticalcss->log( 'Queue cleaning done.', 3 );
854 }
855
856 // re-check key if invalid.
857 $keyst = $this->criticalcss->get_option( 'keyst' );
858 if ( 1 == $keyst ) {
859 $this->ao_ccss_api_generate( '', '', '' );
860 }
861 }
862 }
863