PluginProbe
404 Solution / trunk
404 Solution vtrunk
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / feedback / FeedbackEnvironmentExtras_CacheFingerprint.php

FeedbackEnvironmentExtras_CacheFingerprint.php in 404 Solution trunk, at includes/feedback/FeedbackEnvironmentExtras_CacheFingerprint.php

186 lines 8.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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