PluginProbe ʕ •ᴥ•ʔ
Asset CleanUp: Page Speed Booster / trunk
Asset CleanUp: Page Speed Booster vtrunk
trunk 1.1.4.6 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.4.1 1.2.4.2 1.2.4.3 1.2.4.4 1.2.5 1.2.5.1 1.2.5.2 1.2.5.3 1.2.6 1.2.6.1 1.2.6.2 1.2.6.3 1.2.6.4 1.2.6.5 1.2.6.6 1.2.6.7 1.2.6.8 1.2.6.9 1.2.7 1.2.7.1 1.2.7.2 1.2.7.3 1.2.7.4 1.2.7.5 1.2.7.6 1.2.7.7 1.2.7.8 1.2.7.9 1.2.8 1.2.8.1 1.2.8.2 1.2.8.3 1.2.8.4 1.2.8.5 1.2.8.6 1.2.8.7 1.2.8.8 1.2.8.9 1.2.9 1.2.9.1 1.2.9.2 1.2.9.3 1.2.9.4 1.2.9.5 1.2.9.6 1.2.9.7 1.2.9.8 1.2.9.9 1.3 1.3.1 1.3.2 1.3.2.1 1.3.2.2 1.3.2.3 1.3.2.4 1.3.2.5 1.3.2.6 1.3.2.7 1.3.2.8 1.3.2.9 1.3.3.0 1.3.3.1 1.3.3.2 1.3.3.3 1.3.3.4 1.3.3.5 1.3.3.6 1.3.3.7 1.3.3.8 1.3.3.9 1.3.4.0 1.3.4.1 1.3.4.2 1.3.4.3 1.3.4.4 1.3.4.5 1.3.4.6 1.3.4.7 1.3.4.8 1.3.4.9 1.3.5.0 1.3.5.1 1.3.5.2 1.3.5.3 1.3.5.4 1.3.5.5 1.3.5.6 1.3.5.7 1.3.5.8 1.3.5.9 1.3.6.0 1.3.6.1 1.3.6.2 1.3.6.3 1.3.6.4 1.3.6.5 1.3.6.6 1.3.6.7 1.3.6.8 1.3.6.9 1.3.7.0 1.3.7.1 1.3.7.2 1.3.7.3 1.3.7.4 1.3.7.5 1.3.7.6 1.3.7.7 1.3.7.8 1.3.7.9 1.3.8.0 1.3.8.1 1.3.8.2 1.3.8.2.1 1.3.8.3 1.3.8.4 1.3.8.5 1.3.8.6 1.3.8.7 1.3.8.8 1.3.8.9 1.3.9.0 1.3.9.1 1.3.9.2 1.3.9.3 1.3.9.4 1.3.9.5 1.3.9.6 1.3.9.7 1.3.9.8 1.3.9.9 1.4 1.4.0.1 1.4.0.2 1.4.0.3 1.4.0.4
wp-asset-clean-up / classes / Regex.php
wp-asset-clean-up / classes Last commit date
Admin 1 week ago OptimiseAssets 1 week ago ThirdParty 1 year ago AdminBar.php 1 week ago AssetsManager.php 1 week ago BulkChanges.php 1 week ago CleanUp.php 2 months ago Debug.php 1 week ago DebugException.php 1 week ago FileSystem.php 1 year ago HardcodedAssets.php 1 week ago LoadExceptions.php 1 week ago Main.php 1 week ago MainFront.php 1 week ago Maintenance.php 1 week ago Menu.php 1 week ago MetaBoxes.php 1 week ago Misc.php 1 week ago MiscArray.php 1 week ago ObjectCache.php 2 months ago OwnAssets.php 1 week ago PluginTracking.php 1 week ago Preloads.php 1 week ago Regex.php 1 week ago Settings.php 1 week ago Tips.php 1 year ago Update.php 1 week ago
Regex.php
526 lines
1 <?php
2 namespace WpAssetCleanUp;
3
4 /**
5 * Handles rules that can be either:
6 * - plain strings, e.g. "contact-form-7" or "/wp-content/plugins/plugin/file.js"
7 * - explicit RegEx patterns, e.g. "#/wp-content/plugins/(plugin-a|plugin-b)/#i"
8 * - legacy loose RegEx patterns, e.g. "/wd-instagram-feed/(.*?).js"
9 *
10 * Important:
11 * * - "/" is not treated as a RegEx delimiter by default because asset URLs often start with "/wp-content/..."
12 * * - For request URI / Plugins Manager rules, "/" can be allowed via $allowSlashDelimiter.
13 * * - For new RegEx rules, prefer "#" as delimiter.
14 *
15 * PHP 5.6 compatible.
16 */
17 class Regex
18 {
19 /**
20 * Preferred delimiter when wrapping legacy loose RegEx patterns.
21 *
22 * @var string
23 */
24 const DEFAULT_DELIMITER = '#';
25
26 /**
27 * Match one rule against a subject.
28 *
29 * The rule can be:
30 * - plain string
31 * - explicitly delimited RegEx
32 * - legacy loose RegEx
33 *
34 * @param string $rule
35 * @param string $subject
36 * @param bool $allowSlashDelimiter
37 * @param bool $logInvalidPattern
38 *
39 * @return bool
40 */
41 public static function matchesRule($rule, $subject, $allowSlashDelimiter = false, $logInvalidPattern = false)
42 {
43 $rule = trim((string)$rule);
44 $subject = (string)$subject;
45
46 if ($rule === '' || $subject === '') {
47 return false;
48 }
49
50 if (self::isDelimitedPattern($rule, $allowSlashDelimiter)) {
51 return self::matchesPattern($rule, $subject, $logInvalidPattern);
52 }
53
54 if (self::startsLikeExplicitRegex($rule)) {
55 if ($logInvalidPattern && function_exists('error_log')) {
56 error_log('"Asset CleanUp Pro" / Invalid RegEx: ' . $rule . ' / Error: malformed explicit RegEx rule');
57 }
58
59 return false;
60 }
61
62 if (self::looksLikeRegex($rule)) {
63 $wrappedPattern = self::wrapLoosePattern($rule);
64
65 return self::matchesPattern($wrappedPattern, $subject, $logInvalidPattern);
66 }
67
68 return strpos($subject, $rule) !== false;
69 }
70
71 /**
72 * Match multiple rules against a subject.
73 *
74 * @param string|array $rules
75 * @param string $subject
76 * @param bool $logInvalidPattern
77 *
78 * @return bool
79 */
80 public static function matchesAnyRule($rules, $subject, $allowSlashDelimiter = false, $logInvalidPattern = false)
81 {
82 foreach (self::splitRules($rules) as $rule) {
83 if (self::matchesRule($rule, $subject, $allowSlashDelimiter, $logInvalidPattern)) {
84 return $rule;
85 }
86 }
87
88 return false;
89 }
90
91 /**
92 * Alias for older code that expects RegEx matching terminology.
93 *
94 * @param string|array $rules
95 * @param string $subject
96 *
97 * @return bool
98 */
99 public static function isRegExMatch($rules, $subject)
100 {
101 return self::matchesAnyRule($rules, $subject);
102 }
103
104 /**
105 * Check whether a rule is valid.
106 *
107 * Plain strings are always valid.
108 * Explicit RegEx and legacy loose RegEx must compile successfully.
109 *
110 * @param string $rule
111 * @param bool $allowSlashDelimiter
112 *
113 * @return bool
114 */
115 public static function isValidRule($rule, $allowSlashDelimiter = false)
116 {
117 $rule = trim((string)$rule);
118
119 if ($rule === '') {
120 return false;
121 }
122
123 if (self::isDelimitedPattern($rule, $allowSlashDelimiter)) {
124 return self::isValidPattern($rule);
125 }
126
127 // If it starts like an explicit RegEx but failed delimiter validation,
128 // do not keep it as a plain string.
129 // "/" is intentionally ignored here because it is too ambiguous.
130 if (self::startsLikeExplicitRegex($rule)) {
131 return false;
132 }
133
134 if (self::looksLikeRegex($rule)) {
135 return self::isValidPattern(self::wrapLoosePattern($rule));
136 }
137
138 return true;
139 }
140
141 /**
142 * Validate a PCRE pattern.
143 *
144 * @param string $pattern
145 *
146 * @return bool
147 */
148 public static function isValidPattern($pattern)
149 {
150 $pattern = trim((string)$pattern);
151
152 if ($pattern === '') {
153 return false;
154 }
155
156 $hasWarning = false;
157
158 set_error_handler(function () use (&$hasWarning) {
159 $hasWarning = true;
160 });
161
162 $result = preg_match($pattern, '');
163
164 restore_error_handler();
165
166 return ! $hasWarning && $result !== false && preg_last_error() === PREG_NO_ERROR;
167 }
168
169 /**
170 * @param string $pattern
171 * @param string $subject
172 * @param bool $logInvalidPattern
173 *
174 * @return bool
175 */
176 public static function matchesPattern($pattern, $subject, $logInvalidPattern = false)
177 {
178 $pattern = trim((string)$pattern);
179
180 if ($pattern === '') {
181 return false;
182 }
183
184 $hasWarning = false;
185 $errorMessage = '';
186 $errorFile = '';
187 $errorLine = '';
188
189 set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$hasWarning, &$errorMessage, &$errorFile, &$errorLine) {
190 $hasWarning = true;
191 $errorMessage = $errstr;
192 $errorFile = $errfile;
193 $errorLine = $errline;
194 });
195
196 $result = preg_match($pattern, (string)$subject);
197
198 restore_error_handler();
199
200 if ($hasWarning || $result === false || preg_last_error() !== PREG_NO_ERROR) {
201 if ($logInvalidPattern && function_exists('error_log')) {
202 $pregErrorMessage = function_exists('preg_last_error_msg') ? preg_last_error_msg() : 'PREG error code: ' . preg_last_error();
203
204 error_log(
205 '"Asset CleanUp Pro" / Invalid RegEx: ' . $pattern .
206 ' / Error: ' . ($errorMessage !== '' ? $errorMessage : $pregErrorMessage) .
207 ($errorFile !== '' ? ' / File: ' . $errorFile : '') .
208 ($errorLine !== '' ? ' / Line: ' . $errorLine : '')
209 );
210 }
211
212 return false;
213 }
214
215 return $result === 1;
216 }
217
218 /**
219 * Get the delimiters accepted for explicit RegEx rules.
220 *
221 * "/" is intentionally excluded because asset URLs often start with "/".
222 * Paired delimiters such as "()", "[]", "{}" are intentionally excluded
223 * because rules such as "(plugin-a|plugin-b)" should be treated as legacy loose RegEx.
224 *
225 * @param bool $allowSlashDelimiter
226 *
227 * @return array
228 */
229 protected static function getAllowedExplicitDelimiters($allowSlashDelimiter = false)
230 {
231 $delimiters = array('#', '~', '@', '!', '%');
232
233 if ($allowSlashDelimiter) {
234 $delimiters[] = '/';
235 }
236
237 return $delimiters;
238 }
239
240 /**
241 * Detect rules that appear to start as explicit RegEx patterns,
242 * but might be malformed.
243 *
244 * Example:
245 * - #valid#
246 * - #invalid#123
247 * - ~something~
248 *
249 * "/" is intentionally ignored because asset URLs often start with "/".
250 *
251 * @param string $value
252 *
253 * @return bool
254 */
255 public static function startsLikeExplicitRegex($value)
256 {
257 $value = trim((string)$value);
258
259 if ($value === '') {
260 return false;
261 }
262
263 $delimiter = substr($value, 0, 1);
264
265 // Never treat "/" as a malformed explicit RegEx starter.
266 // It is too ambiguous because request URIs and asset URLs commonly start with "/".
267 if ($delimiter === '/') {
268 return false;
269 }
270
271 return in_array($delimiter, self::getAllowedExplicitDelimiters(), true);
272 }
273
274 /**
275 * Detect explicit PCRE delimiters.
276 *
277 * Important:
278 * * "/" is intentionally not accepted as a delimiter by default because normal asset URLs
279 * * often start with "/wp-content/...". It can be allowed via $allowSlashDelimiter
280 * * for request URI / Plugins Manager rules.
281 *
282 * Examples considered RegEx:
283 * - #something#i
284 * - ~something~i
285 * - @something@
286 * - !something!
287 *
288 * Examples considered plain strings:
289 * - /wp-content/plugins/plugin/file.js
290 * - /contact-form-7/
291 *
292 * @param string $value
293 * @param bool $allowSlashDelimiter
294 *
295 * @return bool
296 */
297 public static function isDelimitedPattern($value, $allowSlashDelimiter = false)
298 {
299 $value = trim((string)$value);
300
301 if (strlen($value) < 3) {
302 return false;
303 }
304
305 $delimiter = substr($value, 0, 1);
306
307 if (! in_array($delimiter, self::getAllowedExplicitDelimiters($allowSlashDelimiter), true)) {
308 return false;
309 }
310
311 $closingDelimiterPosition = self::findClosingDelimiterPosition($value, $delimiter);
312
313 if ($closingDelimiterPosition === false || $closingDelimiterPosition < 2) {
314 return false;
315 }
316
317 $modifiers = substr($value, $closingDelimiterPosition + 1);
318
319 if ($modifiers !== '' && preg_match('/^[a-zA-Z]*$/', $modifiers) !== 1) {
320 return false;
321 }
322
323 return self::isValidPattern($value);
324 }
325
326 /**
327 * Detect legacy loose RegEx rules.
328 *
329 * Do NOT treat dots alone as RegEx because many asset URLs end in ".js" or ".css".
330 *
331 * Examples returning true:
332 * - /wd-instagram-feed/(.*?).js
333 * - wp-content/plugins/(plugin-a|plugin-b)
334 * - ^/wp-content/
335 * - \.min\.js$
336 *
337 * Examples returning false:
338 * - /wp-content/plugins/plugin/file.js
339 * - contact-form-7
340 *
341 * @param string $value
342 *
343 * @return bool
344 */
345 public static function looksLikeRegex($value)
346 {
347 $value = trim((string)$value);
348
349 if ($value === '') {
350 return false;
351 }
352
353 // Strong RegEx indicators.
354 // Do not include "?", "*" or "+" here because they can appear in URLs/query strings
355 // or filenames and should not automatically convert a plain string into RegEx.
356 if (preg_match('/[\[\]\(\)\{\}\|\^\$]/', $value) === 1) {
357 return true;
358 }
359
360 // Common wildcard-style RegEx sequences.
361 if (strpos($value, '.*') !== false || strpos($value, '.+') !== false || strpos($value, '.?') !== false) {
362 return true;
363 }
364
365 // Escaped RegEx tokens such as \.js, \d, \w, \s, etc.
366 if (preg_match('/\\\\[.dDsSwWbBAZzGQE]/', $value) === 1) {
367 return true;
368 }
369
370 return false;
371 }
372
373 /**
374 * Wrap a legacy loose RegEx pattern using the default delimiter.
375 *
376 * @param string $pattern
377 *
378 * @return string
379 */
380 public static function wrapLoosePattern($pattern)
381 {
382 $pattern = trim((string)$pattern);
383
384 if ($pattern === '') {
385 return '';
386 }
387
388 return self::DEFAULT_DELIMITER . self::escapeDelimiter($pattern, self::DEFAULT_DELIMITER) . self::DEFAULT_DELIMITER;
389 }
390
391 /**
392 * Split textarea/string/array rules into clean non-empty rows.
393 *
394 * @param string|array $rules
395 *
396 * @return array
397 */
398 public static function splitRules($rules)
399 {
400 if (is_array($rules)) {
401 $rows = $rules;
402 } else {
403 $rules = str_replace(array("\r\n", "\r"), "\n", (string)$rules);
404 $rows = explode("\n", $rules);
405 }
406
407 $cleanRules = array();
408
409 foreach ($rows as $row) {
410 $row = trim((string)$row);
411
412 if ($row !== '') {
413 $cleanRules[] = $row;
414 }
415 }
416
417 return $cleanRules;
418 }
419
420 /**
421 * Purify textarea rules.
422 *
423 * Keeps:
424 * - plain strings
425 * - valid explicit RegEx rules
426 * - valid legacy loose RegEx rules
427 *
428 * Removes:
429 * - invalid explicit RegEx rules
430 * - invalid legacy loose RegEx rules
431 *
432 * It does not rewrite valid loose legacy rules for storage.
433 *
434 * @param string $textareaValue
435 * @param bool $allowSlashDelimiter
436 *
437 * @return string
438 */
439 public static function purifyTextareaRules($textareaValue, $allowSlashDelimiter = false)
440 {
441 $validRules = array();
442
443 foreach (self::splitRules($textareaValue) as $rule) {
444 if (self::isValidRule($rule, $allowSlashDelimiter)) {
445 $validRules[] = trim($rule);
446 }
447 }
448
449 return implode("\n", $validRules);
450 }
451
452 /**
453 * Backward-compatible alias for old naming.
454 *
455 * @param string $textareaValue
456 * @param bool $allowSlashDelimiter
457 *
458 * @return string
459 */
460 public static function purifyTextareaRegexValue($textareaValue, $allowSlashDelimiter = false)
461 {
462 return self::purifyTextareaRules($textareaValue, $allowSlashDelimiter);
463 }
464
465 /**
466 * Find the real closing delimiter, ignoring escaped delimiters.
467 *
468 * @param string $pattern
469 * @param string $delimiter
470 *
471 * @return int|false
472 */
473 protected static function findClosingDelimiterPosition($pattern, $delimiter)
474 {
475 $length = strlen($pattern);
476
477 for ($i = $length - 1; $i > 0; $i--) {
478 if ($pattern[$i] !== $delimiter) {
479 continue;
480 }
481
482 if (! self::isEscapedPosition($pattern, $i)) {
483 return $i;
484 }
485 }
486
487 return false;
488 }
489
490 /**
491 * Check whether a character at a given offset is escaped.
492 *
493 * @param string $string
494 * @param int $position
495 *
496 * @return bool
497 */
498 protected static function isEscapedPosition($string, $position)
499 {
500 $backslashes = 0;
501
502 for ($i = $position - 1; $i >= 0; $i--) {
503 if ($string[$i] !== '\\') {
504 break;
505 }
506
507 $backslashes++;
508 }
509
510 return ($backslashes % 2) === 1;
511 }
512
513 /**
514 * Escape the delimiter when wrapping a loose pattern.
515 *
516 * @param string $pattern
517 * @param string $delimiter
518 *
519 * @return string
520 */
521 protected static function escapeDelimiter($pattern, $delimiter)
522 {
523 return str_replace($delimiter, '\\' . $delimiter, $pattern);
524 }
525 }
526