*/ private $actionCallbacks = []; /** * @return WpContext */ final public static function new(): WpContext { return new self(array_fill_keys(self::ALL, false)); } /** * @return WpContext */ final public static function determine(): WpContext { /** @psalm-suppress RedundantCondition */ $installing = defined('WP_INSTALLING') && WP_INSTALLING; /** @psalm-suppress RedundantCondition */ $xmlRpc = defined('XMLRPC_REQUEST') && XMLRPC_REQUEST; $isCore = defined('ABSPATH'); $isCli = defined('WP_CLI'); $notInstalling = $isCore && !$installing; $isAjax = $notInstalling && wp_doing_ajax(); $isAdmin = $notInstalling && is_admin() && !$isAjax; $isCron = $notInstalling && wp_doing_cron(); $isWpActivate = $installing && is_multisite() && self::isWpActivateRequest(); $undetermined = $notInstalling && !$isAdmin && !$isCron && !$isCli && !$xmlRpc && !$isAjax; $isRest = $undetermined && static::isRestRequest(); $isLogin = $undetermined && !$isRest && static::isLoginRequest(); // When nothing else matches, we assume it is a front-office request. $isFront = $undetermined && !$isRest && !$isLogin; /* * Note that when core is installing **only** `INSTALLING` will be true, not even `CORE`. * This is done to do as less as possible during installation, when most of WP does not act * as expected. */ $instance = new self( [ self::AJAX => $isAjax, self::BACKOFFICE => $isAdmin, self::CLI => $isCli, self::CORE => ($isCore || $xmlRpc) && (!$installing || $isWpActivate), self::CRON => $isCron, self::FRONTOFFICE => $isFront, self::INSTALLING => $installing && !$isWpActivate, self::LOGIN => $isLogin, self::REST => $isRest, self::XML_RPC => $xmlRpc && !$installing, self::WP_ACTIVATE => $isWpActivate, ] ); $instance->addActionHooks(); return $instance; } /** * @return bool */ private static function isRestRequest(): bool { if ( (defined('REST_REQUEST') && REST_REQUEST) || !empty($_GET['rest_route']) // phpcs:ignore ) { return true; } if (!get_option('permalink_structure')) { return false; } /* * This is needed because, if called early, global $wp_rewrite is not defined but required * by get_rest_url(). WP will reuse what we set here, or in worst case will replace, but no * consequences for us in any case. */ if (empty($GLOBALS['wp_rewrite'])) { $GLOBALS['wp_rewrite'] = new \WP_Rewrite(); } $currentPath = trim((string)parse_url((string)add_query_arg([]), PHP_URL_PATH), '/') . '/'; $restPath = trim((string)parse_url((string)get_rest_url(), PHP_URL_PATH), '/') . '/'; return strpos($currentPath, $restPath) === 0; } /** * @return bool */ private static function isLoginRequest(): bool { /** * New core function with WordPress 6.1 * @link https://make.wordpress.org/core/2022/09/11/new-is_login-function-for-determining-if-a-page-is-the-login-screen/ */ if (function_exists('is_login')) { return is_login() !== false; } if (!empty($_REQUEST['interim-login'])) { // phpcs:ignore return true; } /** * Fallback and 1:1 copy from is_login() in case, the function is * not available for WP < 6.1. * phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotValidated * phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash * phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized */ $scriptName = $_SERVER['SCRIPT_NAME'] ?? ''; return false !== stripos(wp_login_url(), $scriptName); } /** * @return bool */ private static function isWpActivateRequest(): bool { return static::isPageNow('wp-activate.php', network_site_url('wp-activate.php')); } /** * @param string $page * @param string $url * @return bool */ private static function isPageNow(string $page, string $url): bool { $pageNow = (string)($GLOBALS['pagenow'] ?? ''); if ($pageNow && (basename($pageNow) === $page)) { return true; } $currentPath = (string)parse_url(add_query_arg([]), PHP_URL_PATH); $targetPath = (string)parse_url($url, PHP_URL_PATH); return trim($currentPath, '/') === trim($targetPath, '/'); } /** * @param array $data */ private function __construct(array $data) { $this->data = $data; } /** * @param string $context * @return WpContext */ final public function force(string $context): WpContext { if (!in_array($context, self::ALL, true)) { throw new \LogicException("'{$context}' is not a valid context."); } $this->removeActionHooks(); $data = array_fill_keys(self::ALL, false); $data[$context] = true; if (!in_array($context, [self::INSTALLING, self::CLI, self::CORE], true)) { $data[self::CORE] = true; } $this->data = $data; return $this; } /** * @return WpContext */ final public function withCli(): WpContext { $this->data[self::CLI] = true; return $this; } /** * @param string $context * @param string ...$contexts * @return bool */ final public function is(string $context, string ...$contexts): bool { array_unshift($contexts, $context); foreach ($contexts as $context) { if (($this->data[$context] ?? null)) { return true; } } return false; } /** * @return bool */ public function isCore(): bool { return $this->is(self::CORE); } /** * @return bool */ public function isFrontoffice(): bool { return $this->is(self::FRONTOFFICE); } /** * @return bool */ public function isBackoffice(): bool { return $this->is(self::BACKOFFICE); } /** * @return bool */ public function isAjax(): bool { return $this->is(self::AJAX); } /** * @return bool */ public function isLogin(): bool { return $this->is(self::LOGIN); } /** * @return bool */ public function isRest(): bool { return $this->is(self::REST); } /** * @return bool */ public function isCron(): bool { return $this->is(self::CRON); } /** * @return bool */ public function isWpCli(): bool { return $this->is(self::CLI); } /** * @return bool */ public function isXmlRpc(): bool { return $this->is(self::XML_RPC); } /** * @return bool */ public function isInstalling(): bool { return $this->is(self::INSTALLING); } /** * @return bool */ public function isWpActivate(): bool { return $this->is(self::WP_ACTIVATE); } /** * @return array */ public function jsonSerialize(): array { return $this->data; } /** * When context is determined very early we do our best to understand some context like * login, rest and front-office even if WordPress normally would require a later hook. * When that later hook happen, we change what we have determined, leveraging the more * "core-compliant" approach. * * @return void */ private function addActionHooks(): void { $this->actionCallbacks = [ 'login_init' => function (): void { $this->resetAndForce(self::LOGIN); }, 'rest_api_init' => function (): void { $this->resetAndForce(self::REST); }, 'activate_header' => function (): void { $this->resetAndForce(self::WP_ACTIVATE); }, 'template_redirect' => function (): void { $this->resetAndForce(self::FRONTOFFICE); }, 'current_screen' => function (\WP_Screen $screen): void { $screen->in_admin() and $this->resetAndForce(self::BACKOFFICE); }, ]; foreach ($this->actionCallbacks as $action => $callback) { add_action($action, $callback, PHP_INT_MIN); } } /** * When "force" is called on an instance created via `determine()` we need to remove added hooks * or what we are forcing might be overridden. * * @return void */ private function removeActionHooks(): void { foreach ($this->actionCallbacks as $action => $callback) { remove_action($action, $callback, PHP_INT_MIN); } $this->actionCallbacks = []; } /** * @param string $context * @return void */ private function resetAndForce(string $context): void { $cli = $this->isWpCli(); $this->force($context); $cli and $this->withCli(); } }