| 1 |
<?php |
| 2 |
|
| 3 |
if (!defined('ABSPATH')) { |
| 4 |
exit; |
| 5 |
} |
| 6 |
|
| 7 |
// Every capability boundary this file CALLS is required by path, never |
| 8 |
// autoloaded: this IS the autoloader, so a class it needs while refreshing the |
| 9 |
// classmap cannot be resolved through the classmap it is in the middle of |
| 10 |
// rereading. Splitting the boundary into per-extension files made that easy to |
| 11 |
// forget once; ClassmapIndependentBootReferencesTest now fails the build if any |
| 12 |
// referenced ABJ_404_Solution_* class here lacks its require_once. |
| 13 |
require_once dirname(__DIR__) . '/core/PhpRuntimeCapabilityAdapter.php'; |
| 14 |
require_once dirname(__DIR__) . '/core/OpcacheAdapter.php'; |
| 15 |
|
| 16 |
/** |
| 17 |
* Plugin class autoloader. |
| 18 |
* |
| 19 |
* Resolves ABJ_404_Solution_* classes via a deterministic classmap |
| 20 |
* (includes/classmap.php) to avoid runtime glob() scans on real sites. Before |
| 21 |
* loading a "host" facade class it verifies that the collaborator files it |
| 22 |
* depends on are present, using the composition-dependency map stored in |
| 23 |
* includes/data/autoloader-trait-dependencies.php; a missing collaborator is |
| 24 |
* recorded in $GLOBALS['abj404_missing_files'] and the host class load is |
| 25 |
* skipped so a corrupt install surfaces a degraded admin page instead of an |
| 26 |
* uncatchable compile-time fatal. |
| 27 |
* |
| 28 |
* The classmap is read once per request and then kept in a static, which makes |
| 29 |
* it OLDER THAN THE FILES IT DESCRIBES for the length of a plugin self-update: |
| 30 |
* WordPress replaces the plugin directory underneath a request that is already |
| 31 |
* running, so from that moment the request reads NEW class files through the |
| 32 |
* PREVIOUS release's map. Any collaborator whose class is new in the incoming |
| 33 |
* release is then unresolvable. That has reached production twice -- report 266 |
| 34 |
* (TableReadinessGate, 4.3.2 to 4.3.3, save_post during wp-cron) and the |
| 35 |
* 4.3.3-beta.2 PiiRedactor fatal -- so the map now re-reads itself when |
| 36 |
* classmap.php changes on disk. See abj404_autoloader_classmap_stamp(). |
| 37 |
* |
| 38 |
* The spl_autoload_register() call stays in 404-solution.php (the boot |
| 39 |
* sequence); this file only defines the function. It is required with a plain |
| 40 |
* require FIRST in 404-solution.php, before any class use. |
| 41 |
*/ |
| 42 |
// allow-no-test-found: boot-time global function (abj404_autoloader) wired via spl_autoload_register in 404-solution.php; no isolated unit-file seam. The classmap+missing-collaborator resolution behavior is exercised in BootResilienceTest (which references abj404_autoloader directly); the mid-request classmap swap is exercised in AutoloaderStaleClassmapRefreshTest. |
| 43 |
|
| 44 |
if (!function_exists('abj404_autoloader_classmap_stamp')) { |
| 45 |
/** |
| 46 |
* Identity of includes/classmap.php as it exists on disk RIGHT NOW. |
| 47 |
* |
| 48 |
* Compared against the stamp taken when the in-memory map was built, this is |
| 49 |
* what tells a running request that its plugin directory has been replaced. |
| 50 |
* mtime alone would answer in production (a release's files carry that |
| 51 |
* release's extraction time), but a same-second rewrite is cheap to also cover, |
| 52 |
* so the size goes in too. |
| 53 |
* |
| 54 |
* The stat cache must be cleared first: the file is rewritten by a DIFFERENT |
| 55 |
* process (the WordPress updater), so this request's cached stat predates the |
| 56 |
* swap and would report the file as unchanged forever. |
| 57 |
* |
| 58 |
* @param string $mapFile Absolute path to includes/classmap.php. |
| 59 |
* @return string '' when the file is absent or unreadable, which is the |
| 60 |
* updater's directory-move window and means "cannot tell". |
| 61 |
*/ |
| 62 |
function abj404_autoloader_classmap_stamp($mapFile) { |
| 63 |
clearstatcache(true, $mapFile); |
| 64 |
if (!is_file($mapFile)) { |
| 65 |
return ''; |
| 66 |
} |
| 67 |
$mtime = @filemtime($mapFile); |
| 68 |
$size = @filesize($mapFile); |
| 69 |
if ($mtime === false || $size === false) { |
| 70 |
return ''; |
| 71 |
} |
| 72 |
return $mtime . ':' . $size; |
| 73 |
} |
| 74 |
} |
| 75 |
|
| 76 |
if (!function_exists('abj404_autoloader_read_classmap')) { |
| 77 |
/** |
| 78 |
* Read and normalize includes/classmap.php. |
| 79 |
* |
| 80 |
* @param string $mapFile Absolute path to includes/classmap.php. |
| 81 |
* @param bool $refreshBytecode True when the file is known to have changed |
| 82 |
* since it was last read. opcache would |
| 83 |
* otherwise hand `require` back the previous |
| 84 |
* release's compiled copy of the very file we |
| 85 |
* are re-reading BECAUSE it changed. Guarded the |
| 86 |
* same way OpcacheUpgradeGuard guards its own |
| 87 |
* calls: a host with opcache.restrict_api set |
| 88 |
* raises a warning and refuses, and that warning |
| 89 |
* reaches the error reporter. |
| 90 |
* @return array<string, string> Class name => absolute file path. Empty when |
| 91 |
* the file is absent, unreadable, half-written, |
| 92 |
* or does not return an array. The caller treats |
| 93 |
* empty as "keep the map you already have". |
| 94 |
*/ |
| 95 |
function abj404_autoloader_read_classmap($mapFile, $refreshBytecode = false) { |
| 96 |
if (!is_file($mapFile)) { |
| 97 |
return array(); |
| 98 |
} |
| 99 |
if ($refreshBytecode && ABJ_404_Solution_PhpRuntimeCapabilityAdapter::isFunctionAvailable('opcache_invalidate') |
| 100 |
&& (!function_exists('abj404_opcache_api_is_restricted') |
| 101 |
|| !abj404_opcache_api_is_restricted(ini_get('opcache.restrict_api'), __FILE__))) { |
| 102 |
ABJ_404_Solution_OpcacheAdapter::invalidate($mapFile, true); |
| 103 |
} |
| 104 |
try { |
| 105 |
$loadedMap = require $mapFile; |
| 106 |
} catch (\Throwable $readFailure) { |
| 107 |
// A refresh reads this file at the one moment another process is |
| 108 |
// REWRITING it, so `require` can compile a truncated copy and raise a |
| 109 |
// ParseError. Reporting no map lets the caller keep the working one; |
| 110 |
// the next miss re-reads the by-then-complete file. |
| 111 |
if (function_exists('abj404_logRuntimeWarning')) { |
| 112 |
abj404_logRuntimeWarning('abj404_autoloader: could not read ' . $mapFile, $readFailure); |
| 113 |
} |
| 114 |
return array(); |
| 115 |
} |
| 116 |
$normalizedMap = array(); |
| 117 |
if (is_array($loadedMap)) { |
| 118 |
foreach ($loadedMap as $mappedClass => $mappedFile) { |
| 119 |
if (is_string($mappedClass) && is_string($mappedFile)) { |
| 120 |
$normalizedMap[$mappedClass] = $mappedFile; |
| 121 |
} |
| 122 |
} |
| 123 |
} |
| 124 |
return $normalizedMap; |
| 125 |
} |
| 126 |
} |
| 127 |
|
| 128 |
if (!function_exists('abj404_autoloader')) { |
| 129 |
/** |
| 130 |
* @param string $class |
| 131 |
* @return void |
| 132 |
*/ |
| 133 |
function abj404_autoloader($class) { |
| 134 |
// some people were having issues with possibly parent classes not being loaded before their children. |
| 135 |
$childParentMap = [ |
| 136 |
'ABJ_404_Solution_FunctionsMBString' => 'ABJ_404_Solution_Functions', |
| 137 |
'ABJ_404_Solution_FunctionsPreg' => 'ABJ_404_Solution_Functions', |
| 138 |
'ABJ_404_Solution_MbStringAdapterMb' => 'ABJ_404_Solution_MbStringAdapter', |
| 139 |
'ABJ_404_Solution_MbStringAdapterPreg' => 'ABJ_404_Solution_MbStringAdapter', |
| 140 |
'ABJ_404_Solution_RegexHelperMb' => 'ABJ_404_Solution_RegexHelper', |
| 141 |
'ABJ_404_Solution_RegexHelperPreg' => 'ABJ_404_Solution_RegexHelper', |
| 142 |
]; |
| 143 |
|
| 144 |
// only pay attention if it's for us. don't bother for other things. |
| 145 |
if (substr($class, 0, 16) !== 'ABJ_404_Solution') { |
| 146 |
return; |
| 147 |
} |
| 148 |
|
| 149 |
// Use a deterministic classmap to avoid runtime glob() scans on real sites. |
| 150 |
/** @var array<string, string>|null $abj404_autoLoaderClassMap */ |
| 151 |
static $abj404_autoLoaderClassMap = null; |
| 152 |
/** @var string $abj404_autoLoaderMapStamp Disk identity of the map above. */ |
| 153 |
static $abj404_autoLoaderMapStamp = ''; |
| 154 |
// Declared here rather than beside its first use: a `static` statement |
| 155 |
// rebinds the variable to static storage when it executes, so an |
| 156 |
// assignment placed before it (the refresh below invalidates this map) |
| 157 |
// would be silently discarded on every call after the first. |
| 158 |
/** @var array<string, array<int, string>>|null $traitDependencies */ |
| 159 |
static $traitDependencies = null; |
| 160 |
/** @var string|null $mapFile */ |
| 161 |
static $mapFile = null; |
| 162 |
|
| 163 |
if ($mapFile === null) { |
| 164 |
$mapFile = dirname(__DIR__) . '/classmap.php'; |
| 165 |
} |
| 166 |
if ($abj404_autoLoaderClassMap === null) { |
| 167 |
$abj404_autoLoaderClassMap = abj404_autoloader_read_classmap($mapFile); |
| 168 |
$abj404_autoLoaderMapStamp = abj404_autoloader_classmap_stamp($mapFile); |
| 169 |
} |
| 170 |
|
| 171 |
if (!array_key_exists($class, $abj404_autoLoaderClassMap)) { |
| 172 |
// STALE-MAP REFRESH. A miss is normally the truth -- the class is not |
| 173 |
// ours -- but it is also exactly what a plugin self-update looks like |
| 174 |
// from inside a request that booted on the previous release: same |
| 175 |
// paths, new file contents, and a collaborator this map has never |
| 176 |
// heard of (production report 266, TableReadinessGate). Re-reading the |
| 177 |
// map is the only way to tell those two apart, and the stamp is what |
| 178 |
// keeps the re-read off the hot path: an ordinary miss costs one stat. |
| 179 |
$currentStamp = abj404_autoloader_classmap_stamp($mapFile); |
| 180 |
if ($currentStamp === '' || $currentStamp === $abj404_autoLoaderMapStamp) { |
| 181 |
return; |
| 182 |
} |
| 183 |
$refreshedMap = abj404_autoloader_read_classmap($mapFile, true); |
| 184 |
if ($refreshedMap === array()) { |
| 185 |
// Half-written or unreadable while the updater moves directories. |
| 186 |
// Keep the map we already have (and its stamp, so the next miss |
| 187 |
// tries again): trading a working map for an empty one would break |
| 188 |
// every remaining autoload in this request. |
| 189 |
return; |
| 190 |
} |
| 191 |
$abj404_autoLoaderClassMap = $refreshedMap; |
| 192 |
$abj404_autoLoaderMapStamp = $currentStamp; |
| 193 |
// Derived from the classmap, so it is stale for the same reason. |
| 194 |
$traitDependencies = null; |
| 195 |
if (!array_key_exists($class, $abj404_autoLoaderClassMap)) { |
| 196 |
return; |
| 197 |
} |
| 198 |
} |
| 199 |
|
| 200 |
// Composition dependency pre-check: parent facade classes depend on files |
| 201 |
// being present before they are loaded. Verify those files exist first so |
| 202 |
// a corrupt install can surface a degraded admin page instead of a fatal. |
| 203 |
// The class->collaborators map is DATA, loaded from an external file. |
| 204 |
if ($traitDependencies === null) { |
| 205 |
$mapDataFile = dirname(__DIR__) . '/data/autoloader-trait-dependencies.php'; |
| 206 |
$traitDependencyClasses = file_exists($mapDataFile) ? require $mapDataFile : array(); |
| 207 |
if (!is_array($traitDependencyClasses)) { |
| 208 |
$traitDependencyClasses = array(); |
| 209 |
} |
| 210 |
$traitDependencies = array(); |
| 211 |
foreach ($traitDependencyClasses as $hostClass => $dependencyClasses) { |
| 212 |
$traitDependencies[$hostClass] = array(); |
| 213 |
if (!is_array($dependencyClasses)) { |
| 214 |
continue; |
| 215 |
} |
| 216 |
foreach ($dependencyClasses as $dependencyClass) { |
| 217 |
if (is_string($dependencyClass) && isset($abj404_autoLoaderClassMap[$dependencyClass])) { |
| 218 |
$traitDependencies[$hostClass][] = $abj404_autoLoaderClassMap[$dependencyClass]; |
| 219 |
} |
| 220 |
} |
| 221 |
} |
| 222 |
} |
| 223 |
|
| 224 |
if (isset($traitDependencies[$class])) { |
| 225 |
foreach ($traitDependencies[$class] as $dependencyFile) { |
| 226 |
if (!file_exists($dependencyFile)) { |
| 227 |
abj404_record_missing_file($dependencyFile); |
| 228 |
// Don't load the parent class: the compile-time fatal is uncatchable. |
| 229 |
return; |
| 230 |
} |
| 231 |
} |
| 232 |
} |
| 233 |
|
| 234 |
// Ensure the parent class is loaded first. |
| 235 |
if (array_key_exists($class, $childParentMap)) { |
| 236 |
$parentClass = $childParentMap[$class]; |
| 237 |
if (!class_exists($parentClass, false) && array_key_exists($parentClass, $abj404_autoLoaderClassMap)) { |
| 238 |
$parentFile = $abj404_autoLoaderClassMap[$parentClass]; |
| 239 |
if (!file_exists($parentFile)) { |
| 240 |
abj404_record_missing_file($parentFile); |
| 241 |
return; |
| 242 |
} |
| 243 |
require_once $parentFile; |
| 244 |
} |
| 245 |
} |
| 246 |
|
| 247 |
$classFile = $abj404_autoLoaderClassMap[$class]; |
| 248 |
if (!file_exists($classFile)) { |
| 249 |
abj404_record_missing_file($classFile); |
| 250 |
return; |
| 251 |
} |
| 252 |
|
| 253 |
require_once $classFile; |
| 254 |
} |
| 255 |
} |
| 256 |
|