From 6b3377177d7dec2fc58b47479493cc541d7a4d54 Mon Sep 17 00:00:00 2001 From: Giuseppe Mazzapica Date: Tue, 3 Aug 2021 20:00:24 +0200 Subject: [PATCH] Add isWpActivate() --- README.md | 84 ++++++++++++++---------------- composer.json | 2 +- src/WpContext.php | 73 ++++++++++++++++++-------- tests/WpContextTest.php | 112 ++++++++++++++++++++++++++++++++++++++-- 4 files changed, 199 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index 2d31dbe..ec2ddad 100644 --- a/README.md +++ b/README.md @@ -10,19 +10,15 @@ A single-class utility to check the current request context in WordPress sites. This is a Composer package, not a plugin, so first it needs to be installed via Composer. -After that, assuming Composer autoload file is loaded, very early in the load process it is possible -to instantiate the `WpContext` like this: +After that, assuming Composer autoload file is loaded, very early in the load process it is possible to instantiate the `WpContext` like this: ```php $context = Inpsyde\WpContext::determine(); ``` -The library does not implement singleton pattern, nor caches the retrieval of the current context, -so it might be a good idea to save the created instance somewhere globally accessible in your -plugin/theme/package/application. +The library does not implement singleton pattern, nor caches the retrieval of the current context, so it might be a good idea to save the created instance somewhere globally accessible in your plugin/theme/package/application. -Having an instance of `WpContext`, it is possible to check the current context via its `is` method, -or context-specific methods. +Having an instance of `WpContext`, it is possible to check the current context via its `is` method, or context-specific methods. For example: @@ -37,8 +33,7 @@ if ($context->is(WpContext::AJAX, WpContext::CRON)) { } ``` -The method `WpContext::is()` is convenient to check multiple contexts, context-specific methods are -probably better to check a single context. +The method `WpContext::is()` is convenient to check multiple contexts, context-specific methods are probably better to check a single context. The full list of contexts that can be checked is: @@ -52,22 +47,18 @@ The full list of contexts that can be checked is: - `->is(WpContext::CLI)` / `->isWpCli()` - `->is(WpContext::XML_RPC)` / `->isXmlRpc()` - `->is(WpContext::INSTALLING)` / `->isInstalling()` +- `->is(WpContext::WP_ACTIVATE)` / `->isWpActivate()` ### About "core" and "installing" contexts -`WpContext::isCore()` checks for the constants `ABSPATH` being defined, which means that it will -normally be true when all the check for other contexts is also true, but `WpContext::isInstalling()` -is an exception to that (more on this below). +`WpContext::isCore()` checks for the constants `ABSPATH` being defined, which means that it will normally be true when all the check for other contexts is also true, but `WpContext::isInstalling()` is an exception to that (more on this below). Another possible exception is WP CLI commands that run before WordPress is loaded. -`WpContext::isInstalling()` is true when the constant `WP_INSTALLING` is defined and true, that is -when WordPress is installing or upgrading. +`WpContext::isInstalling()` is true when the constant `WP_INSTALLING` is defined and true, that is when WordPress is installing or upgrading. -In this phase, `WpContext` returns `false` for all the other contexts (except for `WpContext::isWpCli()`, -which will be true if the installation/update is happening via WP CLI). +In this phase, `WpContext` returns `false` for all the other contexts (except for `WpContext::isWpCli()`, which will be true if the installation/update is happening via WP CLI). -For example, if a cron request is started, and WordPress for any reason sets the `WP_INSTALLING` -constant during that request, `WpContext::isCron()` will be `false`, just like `WpContext::isCore()`. +For example, if a cron request is started, and WordPress for any reason sets the `WP_INSTALLING` constant during that request, `WpContext::isCron()` will be `false`, just like `WpContext::isCore()`. The reason for this is that WordPress is likely not behaving as expected during installation. @@ -79,39 +70,43 @@ if (Inpsyde\WpContext::determine()->isCore()) { } ``` -which might look very fine, could break if `WP_INSTALLING` is true, considering in that case the -options table might not be there at all. Thanks to the fact that `WpContext::isCore()` returns false -when `WP_INSTALLING` is true the `get_option` call above is not executed during installation (when +which might look very fine, could break if `WP_INSTALLING` is true, considering in that case the options table might not be there at all. Thanks to the fact that `WpContext::isCore()` returns false when `WP_INSTALLING` is true the `get_option` call above is not executed during installation (when it is not safe to call). +### About "installing" and "activate" contexts + +The previous section states: + +> `WpContext::isInstalling()` is true when the constant `WP_INSTALLING` is defined and true + +but there's an exception to that. + +When visiting `/wp-activate.php` the constant `WP_INSTALLING` is defined and true, however the issues that usually apply in that case (WP not fully reliable) don't apply there, in fact, no "installations" happens when in `/wp-activate.php` and WP is fully loaded. + +This is why `/wp-activate.php` is a sort of "special case" and WP Context can determine that case via `WpContext::isWpActivate()`. When that returns true, `WpContext::isInstalling()` will return false, and `WpContext::isCore()` will return true, even if `WP_INSTALLING` is defined and true. + +Please note that `/wp-activate.php` is only available for multisite installations. + ## Ok, but why? -WordPress has core functions and constants to determine the context of current request, so why an -additional package? +WordPress has core functions and constants to determine the context of current request, so why an additional package? There are multiple reasons: -- Not all contexts have a way to be determined. For example how do you determine when in a "front-office" - context? And what about login screen? -- Some contexts have a dedicated constant/function, but only available late in the request flow. - For example, REST requests can be checked via the `REST_REQUEST` constant, but that is only defined - pretty late. `WpContext::isRest()` instead, can be used very early. -- Unit tests. Any logic that depends on PHP constants makes unit-testing hard, because it forces running - tests in separate processes to be able to test different values for the same constant. - On top of that, when running tests without WordPress being loaded it might be needed to "mock" - a few WordPress functions, constants, global variables, etc. As documented below this package make - tests very easy. +- Not all contexts have a way to be determined. For example how do you determine when in a "front-office" context? And what about login screen? +- Some contexts have a dedicated constant/function, but only available late in the request flow. For example, REST requests can be checked via the `REST_REQUEST` constant, but that is only defined pretty late. `WpContext::isRest()` instead, can be used very early. +- Unit tests. Any logic that depends on PHP constants makes unit-testing hard, because it forces running tests in separate processes to be able to test different values for the same constant. + On top of that, when running tests without WordPress being loaded it might be needed to "mock" a few WordPress functions, constants, global variables, etc. As documented below this package make tests very easy. + ## Testing code that uses `WpContext` -Considering that `WpContext` uses constants and functions to determine current WordPress context -it could be hard to unit-test code that make use of it, especially when WordPress is not loaded at all. +Considering that `WpContext` uses constants and functions to determine current WordPress context it could be hard to unit-test code that make use of it, especially when WordPress is not loaded at all. -In tests, it is possible to obtain an instance of `WpContext` by calling `WpContext::new` instead -of `WpContext::determine()` and then use `WpContext::force()` method to set it to the wanted context +In tests, it is possible to obtain an instance of `WpContext` by calling `WpContext::new` instead of `WpContext::determine()` and then use `WpContext::force()` method to set it to the wanted context ```php use Inpsyde\WpContext; @@ -122,15 +117,12 @@ assert($context->isAjax()); assert($context->isCore()); ``` -When "forcing" a content different from `INSTALLING` or `CLI`, the context `CORE` is also set to -true, not being possible to have, for example, an WordPress AJAX request outside of WordPress core. +When "forcing" a content different from `INSTALLING` or `CLI`, the context `CORE` is also set to true, not being possible to have, for example, an WordPress AJAX request outside of WordPress core. The only context, besides `CORE`, that can be associated with other contexts is `CLI`. -However, `force` method only accepts a single context, so by using it is not possible to "simulate" -a request that is, for example, both `CLI` and `CRON`. +However, `force` method only accepts a single context, so by using it is not possible to "simulate" a request that is, for example, both `CLI` and `CRON`. -For this scope, `WpContext` has a `withCli` method, that unlike `force` does not override current -context, but only "appends" `CLI` context. +For this scope, `WpContext` has a `withCli` method, that unlike `force` does not override current context, but only "appends" `CLI` context. For example: @@ -144,8 +136,8 @@ assert($context->isCore()); assert($context->isWpCli()); ``` -Note that `$context->force(WpContext::CLI)` can still be used to "simulate" requests that are _only_ -WP CLI, not even `CORE`. +Note that `$context->force(WpContext::CLI)` can still be used to "simulate" requests that are _only_ WP CLI, not even `CORE`. + ## Crafted by Inpsyde @@ -153,6 +145,7 @@ WP CLI, not even `CORE`. The team at [Inpsyde](https://inpsyde.com) is engineering the Web since 2006. + ## License Copyright (c) 2020 Inpsyde GmbH @@ -160,6 +153,7 @@ Copyright (c) 2020 Inpsyde GmbH This library is released under ["GPL 2.0 or later" License](LICENSE). + ## Contributing All feedback / bug reports / pull requests are welcome. diff --git a/composer.json b/composer.json index 1d6c84d..97b3523 100644 --- a/composer.json +++ b/composer.json @@ -54,7 +54,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-main": "1.x-dev" } } } diff --git a/src/WpContext.php b/src/WpContext.php index 3131d19..257d4c8 100644 --- a/src/WpContext.php +++ b/src/WpContext.php @@ -16,6 +16,7 @@ class WpContext implements \JsonSerializable public const LOGIN = 'login'; public const REST = 'rest'; public const XML_RPC = 'xml-rpc'; + public const WP_ACTIVATE = 'wp-activate'; private const ALL = [ self::AJAX, @@ -28,6 +29,7 @@ class WpContext implements \JsonSerializable self::LOGIN, self::REST, self::XML_RPC, + self::WP_ACTIVATE, ]; /** @@ -58,14 +60,15 @@ final public static function determine(): WpContext $isCore = defined('ABSPATH'); $isCli = defined('WP_CLI'); $notInstalling = $isCore && !$installing; - $isAjax = $notInstalling ? wp_doing_ajax() : false; - $isAdmin = $notInstalling ? (is_admin() && !$isAjax) : false; - $isCron = $notInstalling ? wp_doing_cron() : false; + $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() : false; - $isLogin = ($undetermined && !$isRest) ? static::isLoginRequest() : false; + $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; @@ -78,16 +81,17 @@ final public static function determine(): WpContext $instance = new self( [ - self::CORE => ($isCore || $xmlRpc) && !$installing, - self::FRONTOFFICE => $isFront, + 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::AJAX => $isAjax, self::REST => $isRest, - self::CRON => $isCron, - self::CLI => $isCli, self::XML_RPC => $xmlRpc && !$installing, - self::INSTALLING => $installing, + self::WP_ACTIVATE => $isWpActivate, ] ); @@ -133,15 +137,33 @@ private static function isLoginRequest(): bool return true; } + return static::isPageNow('wp-login.php', wp_login_url()); + } + + /** + * @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) === 'wp-login.php')) { + if ($pageNow && (basename($pageNow) === $page)) { return true; } $currentPath = (string)parse_url(add_query_arg([]), PHP_URL_PATH); - $loginPath = (string)parse_url(wp_login_url(), PHP_URL_PATH); + $targetPath = (string)parse_url($url, PHP_URL_PATH); - return rtrim($currentPath, '/') === rtrim($loginPath, '/'); + return trim($currentPath, '/') === trim($targetPath, '/'); } /** @@ -166,7 +188,7 @@ final public function force(string $context): WpContext $data = array_fill_keys(self::ALL, false); $data[$context] = true; - if ($context !== self::INSTALLING && $context !== self::CORE && $context !== self::CLI) { + if (!in_array($context, [self::INSTALLING, self::CLI, self::CORE], true)) { $data[self::CORE] = true; } @@ -283,6 +305,14 @@ public function isInstalling(): bool return $this->is(self::INSTALLING); } + /** + * @return bool + */ + public function isWpActivate(): bool + { + return $this->is(self::WP_ACTIVATE); + } + /** * @return array */ @@ -308,6 +338,9 @@ private function addActionHooks(): void '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); }, @@ -341,12 +374,8 @@ private function removeActionHooks(): void */ private function resetAndForce(string $context): void { - $cli = $this->data[self::CLI]; - $this->data = array_fill_keys(self::ALL, false); - $this->data[self::CORE] = true; - $this->data[self::CLI] = $cli; - $this->data[$context] = true; - - $this->removeActionHooks(); + $cli = $this->isWpCli(); + $this->force($context); + $cli and $this->withCli(); } } diff --git a/tests/WpContextTest.php b/tests/WpContextTest.php index d799ee9..496d6dc 100644 --- a/tests/WpContextTest.php +++ b/tests/WpContextTest.php @@ -292,6 +292,101 @@ public function testIsAjax(): void static::assertTrue($context->is(WpContext::CRON, WpContext::BACKOFFICE, WpContext::CORE)); } + /** + * @test + */ + public function testIsInstalling(): void + { + define('ABSPATH', __DIR__); + define('WP_INSTALLING', true); + Monkey\Functions\when('wp_doing_ajax')->justReturn(false); + Monkey\Functions\when('is_admin')->justReturn(false); + Monkey\Functions\when('wp_doing_cron')->justReturn(false); + $this->mockIsRestRequest(false); + $this->mockIsLoginRequest(false); + $this->mockIsActivateRequest(false); + + $context = WpContext::determine(); + + static::assertFalse($context->isCore()); + static::assertFalse($context->isWpActivate()); + static::assertTrue($context->isInstalling()); + static::assertFalse($context->isLogin()); + static::assertFalse($context->isRest()); + static::assertFalse($context->isCron()); + static::assertFalse($context->isFrontoffice()); + static::assertFalse($context->isBackoffice()); + static::assertFalse($context->isAjax()); + static::assertFalse($context->isWpCli()); + static::assertFalse($context->isXmlRpc()); + + static::assertTrue($context->is(WpContext::INSTALLING)); + static::assertTrue($context->is(WpContext::INSTALLING, WpContext::BACKOFFICE)); + static::assertFalse($context->is(WpContext::CRON, WpContext::BACKOFFICE)); + static::assertFalse($context->is(WpContext::CRON, WpContext::BACKOFFICE, WpContext::CORE)); + } + + /** + * @test + */ + public function testIsWpActivate(): void + { + define('ABSPATH', __DIR__); + define('WP_INSTALLING', true); + Monkey\Functions\when('wp_doing_ajax')->justReturn(false); + Monkey\Functions\when('is_admin')->justReturn(false); + Monkey\Functions\when('wp_doing_cron')->justReturn(false); + $this->mockIsRestRequest(false); + $this->mockIsLoginRequest(false); + $this->mockIsActivateRequest(true); + + $context = WpContext::determine(); + + static::assertTrue($context->isCore()); + static::assertTrue($context->isWpActivate()); + static::assertFalse($context->isInstalling()); + static::assertFalse($context->isLogin()); + static::assertFalse($context->isRest()); + static::assertFalse($context->isCron()); + static::assertFalse($context->isFrontoffice()); + static::assertFalse($context->isBackoffice()); + static::assertFalse($context->isAjax()); + static::assertFalse($context->isWpCli()); + + static::assertTrue($context->is(WpContext::WP_ACTIVATE)); + static::assertTrue($context->is(WpContext::WP_ACTIVATE, WpContext::BACKOFFICE)); + static::assertFalse($context->is(WpContext::CRON, WpContext::BACKOFFICE)); + static::assertTrue($context->is(WpContext::CRON, WpContext::BACKOFFICE, WpContext::CORE)); + } + + /** + * @test + */ + public function testIsWpActivateLate(): void + { + define('ABSPATH', __DIR__); + Monkey\Functions\when('wp_doing_ajax')->justReturn(false); + Monkey\Functions\when('is_admin')->justReturn(false); + Monkey\Functions\when('wp_doing_cron')->justReturn(false); + $this->mockIsRestRequest(false); + $this->mockIsLoginRequest(false); + $this->mockIsActivateRequest(false); + + $onActivateHeader = null; + Monkey\Actions\expectAdded('activate_header') + ->whenHappen(static function (callable $callback) use (&$onActivateHeader) { + $onActivateHeader = $callback; + }); + + $context = WpContext::determine(); + + static::assertTrue($context->isCore()); + static::assertFalse($context->isWpActivate()); + /** @var callable $onActivateHeader */ + $onActivateHeader(); + static::assertTrue($context->isWpActivate()); + } + /** * @test */ @@ -366,9 +461,18 @@ private function mockIsLoginRequest(bool $is): void { $is and $this->currentPath = '/wp-login.php'; Monkey\Functions\when('wp_login_url')->justReturn('https://example.com/wp-login.php'); - Monkey\Functions\when('home_url') - ->alias(static function (string $path = ''): string { - return 'https://example.com/' . ltrim($path, '/'); - }); + } + + /** + * @param bool $is + */ + private function mockIsActivateRequest(bool $is): void + { + Monkey\Functions\when('is_multisite')->justReturn($is); + + $is and $this->currentPath = '/wp-activate.php'; + Monkey\Functions\when('network_site_url')->alias(static function (string $path): string { + return 'https://example.com/' . ltrim($path, '/'); + }); } }