Skip to content

Commit

Permalink
Add isWpActivate()
Browse files Browse the repository at this point in the history
  • Loading branch information
gmazzap committed Aug 3, 2021
1 parent 4fe60a2 commit 6b33771
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 72 deletions.
84 changes: 39 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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:

Expand All @@ -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.

Expand All @@ -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;
Expand All @@ -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:

Expand All @@ -144,22 +136,24 @@ 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

The team at [Inpsyde](https://inpsyde.com) is engineering the Web since 2006.



## License

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.
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
"dev-main": "1.x-dev"
}
}
}
73 changes: 51 additions & 22 deletions src/WpContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -28,6 +29,7 @@ class WpContext implements \JsonSerializable
self::LOGIN,
self::REST,
self::XML_RPC,
self::WP_ACTIVATE,
];

/**
Expand Down Expand Up @@ -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;
Expand All @@ -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,
]
);

Expand Down Expand Up @@ -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, '/');
}

/**
Expand All @@ -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;
}

Expand Down Expand Up @@ -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
*/
Expand All @@ -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);
},
Expand Down Expand Up @@ -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();
}
}
Loading

0 comments on commit 6b33771

Please sign in to comment.