| 1 |
<?php |
| 2 |
|
| 3 |
if (!defined('ABSPATH')) { |
| 4 |
exit; |
| 5 |
} |
| 6 |
|
| 7 |
/** |
| 8 |
* Which cache implementation owns this site's request caches. |
| 9 |
* |
| 10 |
* Two questions with two different answers, and support reports need both. |
| 11 |
* A drop-in file declares WHO INSTALLED the cache (`advanced-cache.php` and |
| 12 |
* `object-cache.php` carry a `Plugin Name` header); the running constants, |
| 13 |
* classes and extensions declare WHAT IS ACTUALLY RUNNING. They disagree more |
| 14 |
* often than not on a site that has switched caching plugins without cleaning |
| 15 |
* up, and that disagreement is itself the finding. |
| 16 |
* |
| 17 |
* Split from ABJ_404_Solution_FeedbackEnvironmentExtras_PlatformFingerprint, |
| 18 |
* which answers a different question about a different subject: what the site |
| 19 |
* is permanently sitting ON (host, panel, PHP execution stack). Cache |
| 20 |
* ownership changes whenever an admin installs a plugin; hosting identity does |
| 21 |
* not change for the life of the install, and the two marker vocabularies have |
| 22 |
* nothing in common. |
| 23 |
* |
| 24 |
* No PII: only presence booleans, the declared plugin name from a drop-in |
| 25 |
* header, and matched marker keys are returned. No file body and no path. |
| 26 |
* |
| 27 |
* Owned by ABJ_404_Solution_FeedbackEnvironmentExtras via composition; see that |
| 28 |
* class's collect() method for the keyed probe registry that wraps each call |
| 29 |
* below in recordProbe() for failure isolation. |
| 30 |
*/ |
| 31 |
class ABJ_404_Solution_FeedbackEnvironmentExtras_CacheFingerprint { |
| 32 |
|
| 33 |
/** |
| 34 |
* How the presence of one object-cache backend is detected. A closed set, |
| 35 |
* because the alternative -- a bare string beside two other bare strings -- |
| 36 |
* lets one typo silently switch a probe off. These values are also the |
| 37 |
* `backend_detail` prefix, so they are part of what a support payload says. |
| 38 |
*/ |
| 39 |
const MARKER_CONSTANT = 'const'; |
| 40 |
const MARKER_CLASS = 'class'; |
| 41 |
const MARKER_EXTENSION = 'ext'; |
| 42 |
|
| 43 |
/** |
| 44 |
* The detector for each marker kind. |
| 45 |
* |
| 46 |
* A registry rather than three `if ($kind === ...)` branches: adding a |
| 47 |
* fourth kind is then one entry that cannot be half-wired, and |
| 48 |
* FeedbackEnvironmentExtras_CacheFingerprintTest asserts every kind used by |
| 49 |
* probeObjectCacheBackend() has an entry here, so a marker whose kind has |
| 50 |
* no detector fails a test instead of quietly never matching. |
| 51 |
* |
| 52 |
* @return array<string, callable(string): bool> |
| 53 |
*/ |
| 54 |
private static function markerDetectors(): array { |
| 55 |
return array( |
| 56 |
self::MARKER_CONSTANT => static function (string $marker): bool { |
| 57 |
return defined($marker); |
| 58 |
}, |
| 59 |
// Autoloading is deliberately off: a support probe must observe what |
| 60 |
// is already loaded, not cause a class to load as a side effect. |
| 61 |
self::MARKER_CLASS => static function (string $marker): bool { |
| 62 |
return class_exists($marker, false); |
| 63 |
}, |
| 64 |
self::MARKER_EXTENSION => static function (string $marker): bool { |
| 65 |
return extension_loaded($marker); |
| 66 |
}, |
| 67 |
); |
| 68 |
} |
| 69 |
|
| 70 |
/** |
| 71 |
* Report whether one of WordPress's two cache drop-ins is installed and |
| 72 |
* the owner declared by its `Plugin Name` header. No file body, path, or |
| 73 |
* other header is returned. |
| 74 |
* |
| 75 |
* The directory searched is WP_CONTENT_DIR (or ABSPATH/wp-content when |
| 76 |
* that constant is absent), passed through the |
| 77 |
* `abj404_cache_dropin_directory` filter so a site can point the probe |
| 78 |
* somewhere else: installs that load their drop-ins from a relocated |
| 79 |
* content directory, and anything that needs the probe scoped away from |
| 80 |
* the live one, would otherwise be reported as having no cache drop-in at |
| 81 |
* all. Same shape as `abj404_host_pressure_probe_paths` and |
| 82 |
* `abj404_ajax_trace_directory`. A non-string or empty return leaves the |
| 83 |
* computed default in force, so a misbehaving filter degrades to today's |
| 84 |
* behaviour rather than probing '/'. Throwing is safe too: every probe |
| 85 |
* runs inside FeedbackEnvironmentExtras::recordProbe(), which records the |
| 86 |
* failure and substitutes the default. |
| 87 |
* |
| 88 |
* @param string $dropinKey One of `advanced_cache` or `object_cache`. |
| 89 |
* @return array{present: bool, owner: string} |
| 90 |
*/ |
| 91 |
public function probeCacheDropin(string $dropinKey): array { |
| 92 |
$dropinFiles = array( |
| 93 |
'advanced_cache' => 'advanced-cache.php', |
| 94 |
'object_cache' => 'object-cache.php', |
| 95 |
); |
| 96 |
if (!isset($dropinFiles[$dropinKey])) { |
| 97 |
return array('present' => false, 'owner' => ''); |
| 98 |
} |
| 99 |
|
| 100 |
$contentDirectory = defined('WP_CONTENT_DIR') |
| 101 |
? rtrim((string)WP_CONTENT_DIR, '/\\') |
| 102 |
: rtrim((string)ABSPATH, '/\\') . '/wp-content'; |
| 103 |
if (function_exists('apply_filters')) { |
| 104 |
$filtered = apply_filters('abj404_cache_dropin_directory', $contentDirectory, $dropinKey); |
| 105 |
if (is_string($filtered) && trim($filtered) !== '') { |
| 106 |
$contentDirectory = rtrim($filtered, '/\\'); |
| 107 |
} |
| 108 |
} |
| 109 |
$dropinPath = $contentDirectory . '/' . $dropinFiles[$dropinKey]; |
| 110 |
if (!is_file($dropinPath)) { |
| 111 |
return array('present' => false, 'owner' => ''); |
| 112 |
} |
| 113 |
if (!function_exists('get_file_data')) { |
| 114 |
return array('present' => true, 'owner' => 'unknown'); |
| 115 |
} |
| 116 |
|
| 117 |
$headers = get_file_data($dropinPath, array('owner' => 'Plugin Name'), 'plugin'); |
| 118 |
if (!is_array($headers) || !isset($headers['owner']) || !is_scalar($headers['owner'])) { |
| 119 |
return array('present' => true, 'owner' => 'unknown'); |
| 120 |
} |
| 121 |
$owner = trim(strip_tags((string)$headers['owner'])); |
| 122 |
$owner = preg_replace('/[[:cntrl:]]+/', ' ', $owner); |
| 123 |
$owner = is_string($owner) ? trim($owner) : ''; |
| 124 |
|
| 125 |
return array( |
| 126 |
'present' => true, |
| 127 |
'owner' => $owner !== '' ? $owner : 'unknown', |
| 128 |
); |
| 129 |
} |
| 130 |
|
| 131 |
/** |
| 132 |
* Object-cache backend NAME. The base payload's `object_cache` enum |
| 133 |
* answers "external or default"; this answers "external WHAT": Redis |
| 134 |
* (predis vs phpredis vs Redis Object Cache plugin), Memcached, |
| 135 |
* APCu, W3TC, LiteSpeed, WP Engine native, Pantheon, etc. |
| 136 |
* |
| 137 |
* @return array<string, mixed> |
| 138 |
*/ |
| 139 |
public function probeObjectCacheBackend(): array { |
| 140 |
$out = array( |
| 141 |
'using_ext_cache' => false, |
| 142 |
'backend' => 'unknown', |
| 143 |
'backend_detail' => '', |
| 144 |
); |
| 145 |
if (function_exists('wp_using_ext_object_cache')) { |
| 146 |
$out['using_ext_cache'] = (bool)wp_using_ext_object_cache(); |
| 147 |
} |
| 148 |
// Known constants/classes/extensions from popular object-cache |
| 149 |
// drop-ins. The first match wins, so a Redis Object Cache Pro install |
| 150 |
// is not also tagged as plain Redis. |
| 151 |
// |
| 152 |
// Keyed rather than a (name, kind, marker) tuple, and the kind is one |
| 153 |
// of the MARKER_* constants rather than a bare string, because a |
| 154 |
// mistyped kind used to fail OPEN: no branch matched, the check simply |
| 155 |
// never ran, and the probe reported "no object cache" for a host that |
| 156 |
// has one. A detector that silently stops detecting is worse than one |
| 157 |
// that reports nothing, since the answer still looks like an answer. |
| 158 |
$checks = array( |
| 159 |
array('backend' => 'redis_object_cache_pro', 'kind' => self::MARKER_CONSTANT, 'marker' => 'WP_REDIS_VERSION'), |
| 160 |
array('backend' => 'redis_object_cache_pro', 'kind' => self::MARKER_CLASS, 'marker' => 'RedisCachePro\\Plugin'), |
| 161 |
array('backend' => 'redis_object_cache', 'kind' => self::MARKER_CLASS, 'marker' => 'WP_Object_Cache'), |
| 162 |
array('backend' => 'memcached', 'kind' => self::MARKER_CLASS, 'marker' => 'Memcached'), |
| 163 |
array('backend' => 'apcu', 'kind' => self::MARKER_EXTENSION, 'marker' => 'apcu'), |
| 164 |
array('backend' => 'w3_total_cache', 'kind' => self::MARKER_CONSTANT, 'marker' => 'W3TC_VERSION'), |
| 165 |
array('backend' => 'litespeed_cache', 'kind' => self::MARKER_CONSTANT, 'marker' => 'LSCWP_DIR'), |
| 166 |
array('backend' => 'wp_engine_native', 'kind' => self::MARKER_CONSTANT, 'marker' => 'WPE_APIKEY'), |
| 167 |
array('backend' => 'pantheon', 'kind' => self::MARKER_CONSTANT, 'marker' => 'PANTHEON_ENVIRONMENT'), |
| 168 |
); |
| 169 |
$detectors = self::markerDetectors(); |
| 170 |
foreach ($checks as $check) { |
| 171 |
$detector = $detectors[$check['kind']]; |
| 172 |
if ($detector($check['marker'])) { |
| 173 |
$out['backend'] = $check['backend']; |
| 174 |
$out['backend_detail'] = $check['kind'] . ':' . $check['marker']; |
| 175 |
return $out; |
| 176 |
} |
| 177 |
} |
| 178 |
// Default WP object cache used in-memory per request. |
| 179 |
if (!$out['using_ext_cache']) { |
| 180 |
$out['backend'] = 'default'; |
| 181 |
$out['backend_detail'] = 'wp_object_cache:in_memory'; |
| 182 |
} |
| 183 |
return $out; |
| 184 |
} |
| 185 |
} |
| 186 |
|