Resolved directory, keyed by site and settledness. */ private static $resolved = array(); /** * The directory this request's diagnostic journals live in, with a * trailing separator, or '' when even the uploads directory is unknown. * * Never throws and never creates anything: callers that need the directory * to exist create it themselves, because "resolved but unusable" and * "unknown" are different findings and collapsing them is what made an * empty journal unattributable. * * @param array $context Request context for the * `abj404_ajax_trace_directory` filter. A non-empty context is resolved * live: it is request data, so memoizing it under a site-wide key would * hand one caller's answer to another. */ public static function resolve(array $context = array()): string { if ($context !== array()) { return self::resolveThroughFilters($context); } $key = self::siteKey() . '|' . (self::filtersHaveSettled() ? 'settled' : 'boot'); if (array_key_exists($key, self::$resolved)) { return self::$resolved[$key]; } $directory = self::resolveThroughFilters($context); self::$resolved[$key] = $directory; return $directory; } /** * Discard the memoized answers. The seam the test suite uses to give a * PHPUnit worker the end-of-request it never gets * (ABJ404_RequestScopedStateReset). */ public static function flush(): void { self::$resolved = array(); } /** Test seam, and the request-scoped reset the harness calls by name. */ public static function resetForTests(): void { self::flush(); } /** * Have the plugins that filter the uploads directory had their chance to * register? Answered by observation so this class registers nothing. */ private static function filtersHaveSettled(): bool { if (!function_exists('did_action')) { return false; } foreach (self::SETTLED_HOOKS as $hook) { if (did_action($hook) > 0) { return true; } } return false; } /** * @param array $context */ private static function resolveThroughFilters(array $context): string { $directory = function_exists('abj404_getUploadsDir') ? abj404_getUploadsDir() : ''; if (function_exists('apply_filters')) { $directory = apply_filters('abj404_ajax_trace_directory', $directory, $context); } // A filter that returns an array or an object without __toString would // otherwise warn or fatal on the cast. Anything that is not a scalar // reads as "no directory", which every caller already handles. $directory = is_scalar($directory) ? (string)$directory : ''; return $directory === '' ? '' : rtrim($directory, '/\\') . DIRECTORY_SEPARATOR; } /** * The uploads directory is per-site on multisite, so a request that * switch_to_blog()s must not be handed the previous site's journal path. * Read from the global rather than through get_current_blog_id() because * this runs during boot, in shutdown handlers, and inside the fatal path, * where a function call into WordPress may not be available. */ private static function siteKey(): string { $blogId = $GLOBALS['blog_id'] ?? ''; return is_scalar($blogId) ? (string)$blogId : ''; } }