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

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