PluginProbe
Autoptimize / trunk
Autoptimize vtrunk
3.1.16 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 All 108 releases
autoptimize / classes / autoptimizeCriticalCSSEnqueue.php

autoptimizeCriticalCSSEnqueue.php in Autoptimize trunk, at classes/autoptimizeCriticalCSSEnqueue.php

342 lines 16.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Critical CSS job enqueue logic.
4 */
5
6 if ( ! defined( 'ABSPATH' ) ) {
7 exit;
8 }
9
10 class autoptimizeCriticalCSSEnqueue {
11 /**
12 * Critical CSS object.
13 *
14 * @var object
15 */
16 protected $criticalcss;
17
18 public function __construct() {
19 $this->criticalcss = autoptimize()->criticalcss();
20 }
21
22 public function ao_ccss_enqueue( $hash = '', $path = '', $type = 'is_page' ) {
23 // Get key status.
24 $key = $this->criticalcss->key_status( false );
25
26 // Queue is available to anyone...
27 $enqueue = true;
28
29 // ... which are not the ones below.
30 if ( true === autoptimizeUtils::is_local_server() ) {
31 $enqueue = false;
32 $this->criticalcss->log( 'cant enqueue as local/ private', 3 );
33 } elseif ( 'nokey' == $key['status'] || 'invalid' == $key['status'] ) {
34 $enqueue = false;
35 $this->criticalcss->log( 'Job queuing is not available: no valid API key found.', 3 );
36 } elseif ( ! empty( $hash ) && ( is_user_logged_in() || is_feed() || is_404() || is_search() || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) || $this->ao_ccss_ua() || false === apply_filters( 'autoptimize_filter_ccss_enqueue_should_enqueue', true ) ) ) {
37 $enqueue = false;
38 $this->criticalcss->log( 'Job queuing is not available for WordPress\'s logged in users, feeds, error pages, ajax calls or calls from criticalcss.com itself.', 3 );
39 } elseif ( empty( $hash ) && empty( $path ) || ( ( 'is_single' !== $type ) && ( 'is_page' !== $type ) ) ) {
40 $enqueue = false;
41 $this->criticalcss->log( 'Forced job queuing failed, no path or not right type', 3 );
42 }
43
44 if ( ! $enqueue ) {
45 return;
46 }
47
48 // Continue if queue is available
49 // Attach required arrays/ vars.
50 $rules = $this->criticalcss->get_option( 'rules' );
51 $queue_raw = $this->criticalcss->get_option( 'queue_raw' );
52 $queue = $this->criticalcss->get_option( 'queue' );
53 $forcepath = $this->criticalcss->get_option( 'forcepath' );
54
55 // Get request path and page type, and initialize the queue update flag.
56 if ( ! empty( $hash ) ) {
57 $req_orig = $_SERVER['REQUEST_URI'];
58 $req_type = $this->ao_ccss_get_type();
59 } elseif ( ! empty( $path ) ) {
60 $req_orig = $path;
61 if ( '/' === $path ) {
62 $req_type = 'is_front_page';
63 } else {
64 $req_type = $type;
65 }
66 }
67 $req_path = strtok( $req_orig, '?#' ); // remove querystring and fragment
68
69 // now that we really have the path, check if there's no garbage in there (due to some themes serving a non 404 page even if the resource does not exist resulting in all sorts of nonsense rules).
70 if ( true === apply_filters( 'autoptimize_filter_ccss_enqueue_block_garbage' , true ) && str_ireplace( apply_filters( 'autoptimize_filter_ccss_enqueue_blocklist', array( '.php', 'data:text/javascript;base64', '/.', '/null', '.jpeg', '.jpg', '.png' ) ), '', $req_path ) !== $req_path ) {
71 $this->criticalcss->log( 'Job not enqueued looks like the path is just garbage; ' . $req_path, 3 );
72 return;
73 }
74
75 // ensure the path does not contain a potentially malicious payload
76 if ( $req_path !== esc_url( $req_path ) ) {
77 $this->criticalcss->log( 'Job not enqueued looks like the path contains malicious ; ' . esc_url( $req_path ), 3 );
78 return;
79 }
80
81 // Check if we have a lang param. we need to keep as WPML can switch languages based on that
82 // and that includes RTL -> LTR so diff. structure, so rules would be RTL vs LTR
83 // but this needs changes in the structure of the rule object so off by default for now
84 // as now this will simply result in conditional rules being overwritten.
85 if ( apply_filters( 'autoptimize_filter_ccss_coreenqueue_honor_lang', false ) && strpos( $req_orig, 'lang=' ) !== false ) {
86 $req_params = strtok( '?' );
87 parse_str( $req_params, $req_params_arr );
88 if ( array_key_exists( 'lang', $req_params_arr ) && ! empty( $req_params_arr['lang'] ) ) {
89 $req_path .= '?lang=' . $req_params_arr['lang'];
90 }
91 }
92
93 $job_qualify = false;
94 $target_rule = false;
95 $rule_properties = false;
96 $queue_update = false;
97
98 // Match for paths in rules.
99 foreach ( $rules['paths'] as $path => $props ) {
100
101 // Prepare rule target and log.
102 $target_rule = 'paths|' . $path;
103 $this->criticalcss->log( 'Qualifying path <' . $req_path . '> for job submission by rule <' . $target_rule . '>', 3 );
104
105 // Path match
106 // -> exact match needed for AUTO rules
107 // -> partial match OK for MANUAL rules (which have empty hash and a file with CCSS).
108 if ( $path === $req_path || ( false == $props['hash'] && false != $props['file'] && preg_match( '|' . $path . '|', $req_path ) ) ) {
109
110 // There's a path match in the rule, so job QUALIFIES with a path rule match.
111 $job_qualify = true;
112 $rule_properties = $props;
113 $this->criticalcss->log( 'Path <' . $req_path . '> QUALIFIED for job submission by rule <' . $target_rule . '>', 3 );
114
115 // Stop processing other path rules.
116 break;
117 }
118 }
119
120 // Match for types in rules if no path rule matches and if we're not enforcing paths.
121 if ( '' !== $hash && ! $job_qualify && ( ! $forcepath || ! in_array( $req_type, apply_filters( 'autoptimize_filter_ccss_coreenqueue_forcepathfortype', array( 'is_page' ) ) ) || ! apply_filters( 'autoptimize_filter_ccss_coreenqueue_ignorealltypes', false ) ) ) {
122 foreach ( $rules['types'] as $type => $props ) {
123
124 // Prepare rule target and log.
125 $target_rule = 'types|' . $type;
126 $this->criticalcss->log( 'Qualifying page type <' . $req_type . '> on path <' . $req_path . '> for job submission by rule <' . $target_rule . '>', 3 );
127
128 if ( $req_type == $type ) {
129 // Type match.
130 // There's a type match in the rule, so job QUALIFIES with a type rule match.
131 $job_qualify = true;
132 $rule_properties = $props;
133 $this->criticalcss->log( 'Page type <' . $req_type . '> on path <' . $req_path . '> QUALIFIED for job submission by rule <' . $target_rule . '>', 3 );
134
135 // Stop processing other type rules.
136 break;
137 }
138 }
139 }
140
141 if ( $job_qualify && ( ( false == $rule_properties['hash'] && false != $rule_properties['file'] ) || strpos( $req_type, 'template_' ) !== false ) ) {
142 // If job qualifies but rule hash is false and file isn't false (MANUAL rule) or if template, job does not qualify despite what previous evaluations says.
143 $job_qualify = false;
144 $this->criticalcss->log( 'Job submission DISQUALIFIED by MANUAL rule <' . $target_rule . '> with hash <' . $rule_properties['hash'] . '> and file <' . $rule_properties['file'] . '>', 3 );
145 } elseif ( ! $job_qualify && empty( $rule_properties ) ) {
146 // But if job does not qualify and rule properties are set, job qualifies as there is no matching rule for it yet
147 // Fill-in the new target rule.
148 $job_qualify = true;
149
150 // Should we switch to path-base AUTO-rules? Conditions:
151 // 1. forcepath option has to be enabled (off by default)
152 // 2. request type should be (by default, but filterable) one of is_page (removed for now: woo_is_product or woo_is_product_category).
153 if ( ( $forcepath && in_array( $req_type, apply_filters( 'autoptimize_filter_ccss_coreenqueue_forcepathfortype', array( 'is_page' ) ) ) ) || apply_filters( 'autoptimize_filter_ccss_coreenqueue_ignorealltypes', false ) || empty( $hash ) ) {
154 if ( '/' !== $req_path ) {
155 $target_rule = 'paths|' . $req_path;
156 } else {
157 // Exception; we don't want a path-based rule for "/" as that messes things up, hard-switch this to a type-based is_front_page rule.
158 $target_rule = 'types|' . 'is_front_page';
159 }
160 } else {
161 $target_rule = 'types|' . $req_type;
162 }
163 $this->criticalcss->log( 'Job submission QUALIFIED by MISSING rule for page type <' . $req_type . '> on path <' . $req_path . '>, new rule target is <' . $target_rule . '>', 3 );
164 } else {
165 // Or just log a job qualified by a matching rule.
166 $this->criticalcss->log( 'Job submission QUALIFIED by AUTO rule <' . $target_rule . '> with hash <' . $rule_properties['hash'] . '> and file <' . $rule_properties['file'] . '>', 3 );
167 }
168
169 // Submit job.
170 if ( $job_qualify ) {
171 if ( ! array_key_exists( $req_path, $queue ) ) {
172 // This is a NEW job
173 // Merge job into the queue.
174 $queue[ $req_path ] = $this->ao_ccss_define_job(
175 $req_path,
176 $target_rule,
177 $req_type,
178 $hash,
179 null,
180 null,
181 null,
182 null,
183 true
184 );
185 // Set update flag.
186 $queue_update = true;
187 } else {
188 // This is an existing job
189 // The job is still NEW, most likely this is extra CSS file for the same page that needs a hash.
190 if ( 'NEW' == $queue[ $req_path ]['jqstat'] ) {
191 // Add hash if it's not already in the job.
192 if ( ! in_array( $hash, $queue[ $req_path ]['hashes'] ) ) {
193 // Push new hash to its array and update flag.
194 $queue_update = array_push( $queue[ $req_path ]['hashes'], $hash );
195
196 // Log job update.
197 $this->criticalcss->log( 'Hashes UPDATED on local job id <' . $queue[ $req_path ]['ljid'] . '>, job status NEW, target rule <' . $queue[ $req_path ]['rtarget'] . '>, hash added: ' . $hash, 3 );
198
199 // Return from here as the hash array is already updated.
200 return true;
201 }
202 } elseif ( 'NEW' != $queue[ $req_path ]['jqstat'] && 'JOB_QUEUED' != $queue[ $req_path ]['jqstat'] && 'JOB_ONGOING' != $queue[ $req_path ]['jqstat'] ) {
203 // Allow requeuing jobs that are not NEW, JOB_QUEUED or JOB_ONGOING
204 // Merge new job keeping some previous job values.
205 $queue[ $req_path ] = $this->ao_ccss_define_job(
206 $req_path,
207 $target_rule,
208 $req_type,
209 $hash,
210 $queue[ $req_path ]['file'],
211 $queue[ $req_path ]['jid'],
212 $queue[ $req_path ]['jrstat'],
213 $queue[ $req_path ]['jvstat'],
214 false
215 );
216 // Set update flag.
217 $queue_update = true;
218 }
219 }
220
221 if ( $queue_update ) {
222 // Persist the job to the queue and return.
223 $queue_raw = json_encode( $queue );
224 update_option( 'autoptimize_ccss_queue', $queue_raw, false );
225 $this->criticalcss->flush_options();
226 return true;
227 } else {
228 // Or just return false if no job was added.
229 $this->criticalcss->log( 'A job for path <' . $req_path . '> already exist with NEW or PENDING status, skipping job creation', 3 );
230 return false;
231 }
232 }
233 }
234
235 public function ao_ccss_get_type() {
236 // Get the type of a page
237 // Attach the conditional tags array.
238 $types = $this->criticalcss->get_types();
239 $forcepath = $this->criticalcss->get_option( 'forcepath' );
240
241 // By default, a page type is false.
242 $page_type = false;
243
244 // Iterates over the array to match a type.
245 foreach ( $types as $type ) {
246 if ( is_404() ) {
247 $page_type = 'is_404';
248 break;
249 } elseif ( is_front_page() ) {
250 // identify frontpage immediately to avoid it also matching a CPT or template.
251 $page_type = 'is_front_page';
252 break;
253 } elseif ( strpos( $type, 'custom_post_' ) !== false && ( ! $forcepath || ! is_page() ) && is_singular() ) {
254 // Match custom post types and not page or page not forced to path-based.
255 if ( get_post_type( get_the_ID() ) === substr( $type, 12 ) ) {
256 $page_type = $type;
257 break;
258 }
259 } elseif ( strpos( $type, 'template_' ) !== false && ( ! $forcepath || ! is_page() ) ) {
260 // Match templates if not page or if page is not forced to path-based.
261 if ( is_page_template( substr( $type, 9 ) ) ) {
262 $page_type = $type;
263 break;
264 }
265 } else {
266 // Match all other existing types
267 // but remove prefix to be able to check if the function exists & returns true.
268 $_type = str_replace( array( 'woo_', 'bp_', 'bbp_', 'edd_' ), '', $type );
269 if ( function_exists( $_type ) && call_user_func( $_type ) ) {
270 // Make sure we only return for one page, not for the "paged pages" (/page/2 ..).
271 if ( ! is_page() || ! is_paged() ) {
272 $page_type = $type;
273 break;
274 }
275 }
276 }
277 }
278
279 // Return the page type.
280 return $page_type;
281 }
282
283 public function ao_ccss_define_job( $path, $target, $type, $hash, $file, $jid, $jrstat, $jvstat, $create ) {
284 // Define a job entry to be created or updated
285 // Define commom job properties.
286 $path = array();
287 $path['ljid'] = $this->ao_ccss_job_id();
288 $path['rtarget'] = $target;
289 $path['ptype'] = $type;
290 $path['hashes'] = array( $hash );
291 $path['hash'] = $hash;
292 $path['file'] = $file;
293 $path['jid'] = $jid;
294 $path['jqstat'] = 'NEW';
295 $path['jrstat'] = $jrstat;
296 $path['jvstat'] = $jvstat;
297 $path['jctime'] = microtime( true );
298 $path['jftime'] = null;
299
300 // Set operation requested.
301 if ( $create ) {
302 $operation = 'CREATED';
303 } else {
304 $operation = 'UPDATED';
305 }
306
307 // Log job creation.
308 $this->criticalcss->log( 'Job ' . $operation . ' with local job id <' . $path['ljid'] . '> for target rule <' . $target . '>', 3 );
309
310 return $path;
311 }
312
313 public function ao_ccss_job_id( $length = 6 ) {
314 // Generate random strings for the local job ID
315 // Based on https://stackoverflow.com/a/4356295 .
316 $characters = '0123456789abcdefghijklmnopqrstuvwxyz';
317 $characters_length = strlen( $characters );
318 $random_string = 'j-';
319 for ( $i = 0; $i < $length; $i++ ) {
320 $random_string .= $characters[ rand( 0, $characters_length - 1 ) ];
321 }
322 return $random_string;
323 }
324
325 public function ao_ccss_ua() {
326 // Check for criticalcss.com user agent.
327 $agent = '';
328 if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
329 $agent = $_SERVER['HTTP_USER_AGENT'];
330 }
331
332 // Check for UA and return TRUE when criticalcss.com is the detected UA, false when not.
333 $rtn = strpos( $agent, AO_CCSS_URL );
334 if ( 0 === $rtn ) {
335 $rtn = true;
336 } else {
337 $rtn = false;
338 }
339 return ( $rtn );
340 }
341 }
342