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 / root-boot / Autoloader.php

Autoloader.php in 404 Solution trunk, at includes/root-boot/Autoloader.php

256 lines 11.0 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 // 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