PluginProbe
Autoptimize / 3.1.11
Autoptimize v3.1.11
2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.5.0 2.5.1 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 All 107 releases
autoptimize / classes / autoptimizeCriticalCSSCron.php

autoptimizeCriticalCSSCron.php in Autoptimize 3.1.11, at classes/autoptimizeCriticalCSSCron.php

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