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

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