PluginProbe
Autoptimize / 2.7.8
Autoptimize v2.7.8
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 2.7.8, at classes/autoptimizeCriticalCSSCron.php

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