Skip to content

Commit 883b4d3

Browse files
authored
Add Heartbeat feature interval settings (#52)
1 parent a68ba4e commit 883b4d3

24 files changed

+1689
-338
lines changed

.wp-env.json

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
{
2+
"$schema": "https://schemas.wp.org/trunk/wp-env.json",
23
"phpVersion": "7.4",
34
"core": "https://wordpress.org/latest.zip",
45
"plugins": [
5-
".",
6-
"https://downloads.wordpress.org/plugin/user-switching.1.8.0.zip",
7-
"https://downloads.wordpress.org/plugin/query-monitor.3.16.4.zip"
6+
"."
87
],
98
"port": 8801,
109
"config": {

app/Concerns/HasHookName.php

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Syntatis\FeatureFlipper\Concerns;
6+
7+
use Syntatis\FeatureFlipper\Helpers\Option;
8+
9+
trait HasHookName
10+
{
11+
/** @phpstan-param non-empty-string $name */
12+
private static function optionName(string $name): string
13+
{
14+
return 'option_' . Option::name($name);
15+
}
16+
17+
/** @phpstan-param non-empty-string $name */
18+
private static function defaultOptionName(string $name): string
19+
{
20+
return 'default_option_' . Option::name($name);
21+
}
22+
}

app/Concerns/HasURI.php

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Syntatis\FeatureFlipper\Concerns;
6+
7+
use function is_string;
8+
use function parse_url;
9+
use function sprintf;
10+
use function trim;
11+
12+
use const PHP_URL_PATH;
13+
14+
trait HasURI
15+
{
16+
private static function getCurrentUrl(): string
17+
{
18+
$schema = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://';
19+
$host = isset($_SERVER['HTTP_HOST']) && is_string($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
20+
$uri = isset($_SERVER['REQUEST_URI']) && is_string($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
21+
22+
return trim($host) !== '' ? sprintf('%s%s%s', $schema, $host, $uri) : '';
23+
}
24+
25+
private static function getCurrentUrlPath(): ?string
26+
{
27+
$path = parse_url(self::getCurrentUrl(), PHP_URL_PATH);
28+
29+
return is_string($path) ? $path : null;
30+
}
31+
}

app/Features/Heartbeat.php

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Syntatis\FeatureFlipper\Features;
6+
7+
use SSFV\Codex\Contracts\Extendable;
8+
use SSFV\Codex\Contracts\Hookable;
9+
use SSFV\Codex\Foundation\Hooks\Hook;
10+
use SSFV\Psr\Container\ContainerInterface;
11+
use Syntatis\FeatureFlipper\Concerns\HasHookName;
12+
use Syntatis\FeatureFlipper\Features\Heartbeat\ManageAdmin;
13+
use Syntatis\FeatureFlipper\Features\Heartbeat\ManagePostEditor;
14+
use Syntatis\FeatureFlipper\Helpers\Option;
15+
16+
use const PHP_INT_MAX;
17+
18+
/**
19+
* Manage WordPress Heartbeat API feature.
20+
*
21+
* The WordPress Heartbeat API is a core feature in WordPress that enables
22+
* near real-time communication between a web browser and the server.
23+
*
24+
* The Heartbeat API uses AJAX to send requests to the server at intervals.
25+
* By default, this is every 15 seconds in the post editor and every 60
26+
* seconds on the dashboard, while a user is logged into the WordPress
27+
* admin.
28+
*
29+
* @see https://developer.wordpress.org/plugins/javascript/heartbeat-api/
30+
*/
31+
class Heartbeat implements Hookable, Extendable
32+
{
33+
use HasHookName;
34+
35+
/**
36+
* Whether the Heartbeat API is enabled.
37+
*/
38+
private bool $heartbeat;
39+
40+
public function __construct()
41+
{
42+
$this->heartbeat = (bool) Option::get('heartbeat');
43+
}
44+
45+
public function hook(Hook $hook): void
46+
{
47+
$hook->addAction('init', [$this, 'deregisterScripts'], PHP_INT_MAX);
48+
}
49+
50+
public function deregisterScripts(): void
51+
{
52+
if ($this->heartbeat) {
53+
return;
54+
}
55+
56+
/**
57+
* If the feature is disabled, deregister the Heartbeat API script, which
58+
* effectively stopping all the Heartbeat API requests on all pages.
59+
*/
60+
wp_deregister_script('heartbeat');
61+
}
62+
63+
/** @return iterable<object> */
64+
public function getInstances(ContainerInterface $container): iterable
65+
{
66+
yield new ManageAdmin();
67+
yield new ManagePostEditor();
68+
}
69+
}
+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Syntatis\FeatureFlipper\Features\Heartbeat;
6+
7+
use SSFV\Codex\Contracts\Hookable;
8+
use SSFV\Codex\Foundation\Hooks\Hook;
9+
use Syntatis\FeatureFlipper\Concerns\HasHookName;
10+
use Syntatis\FeatureFlipper\Helpers\Option;
11+
12+
use function is_numeric;
13+
14+
use const PHP_INT_MAX;
15+
16+
class ManageAdmin implements Hookable
17+
{
18+
use HasHookName;
19+
20+
private bool $heartbeat;
21+
22+
public function __construct()
23+
{
24+
$this->heartbeat = (bool) Option::get('heartbeat');
25+
}
26+
27+
public function hook(Hook $hook): void
28+
{
29+
$hook->addAction('admin_init', [$this, 'deregisterScripts'], PHP_INT_MAX);
30+
$hook->addFilter('heartbeat_settings', [$this, 'filterSettings'], PHP_INT_MAX);
31+
$hook->addFilter(
32+
self::optionName('heartbeat_admin'),
33+
fn ($value) => $this->heartbeat ? $value : false,
34+
);
35+
$hook->addFilter(
36+
self::defaultOptionName('heartbeat_admin'),
37+
fn ($value) => $this->heartbeat ? $value : false,
38+
);
39+
$hook->addFilter(
40+
self::optionName('heartbeat_admin_interval'),
41+
fn ($value) => (bool) Option::get('heartbeat_admin') && $this->heartbeat ? $value : null,
42+
);
43+
$hook->addFilter(
44+
self::defaultOptionName('heartbeat_admin_interval'),
45+
fn ($value) => (bool) Option::get('heartbeat_admin') && $this->heartbeat ? $value : null,
46+
);
47+
}
48+
49+
public function deregisterScripts(): void
50+
{
51+
if (! is_admin() || self::isPostEditor() || (bool) Option::get('heartbeat_admin')) {
52+
return;
53+
}
54+
55+
wp_deregister_script('heartbeat');
56+
}
57+
58+
/**
59+
* @param array<string,mixed> $settings
60+
*
61+
* @return array<string,mixed>
62+
*/
63+
public function filterSettings(array $settings): array
64+
{
65+
/**
66+
* If it's not admin, return the settings as is.
67+
*
68+
* In the post editor, even though that it is on the admin area, settings
69+
* should also return as is as well, since the settings for post editor
70+
* would be applied from the `ManagePostEditor` class.
71+
*
72+
* @see \Syntatis\FeatureFlipper\Features\Heartbeat\ManagePostEditor::filterSettings()
73+
*/
74+
if (! is_admin() || self::isPostEditor()) {
75+
return $settings;
76+
}
77+
78+
$interval = Option::get('heartbeat_admin_interval');
79+
80+
if (is_numeric($interval)) {
81+
$interval = absint($interval);
82+
83+
$settings['interval'] = $interval;
84+
$settings['minimalInterval'] = $interval;
85+
}
86+
87+
return $settings;
88+
}
89+
90+
private static function isPostEditor(): bool
91+
{
92+
$pagenow = $GLOBALS['pagenow'] ?? '';
93+
94+
return is_admin() && ($pagenow === 'post.php' || $pagenow === 'post-new.php');
95+
}
96+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Syntatis\FeatureFlipper\Features\Heartbeat;
6+
7+
use SSFV\Codex\Contracts\Hookable;
8+
use SSFV\Codex\Foundation\Hooks\Hook;
9+
use Syntatis\FeatureFlipper\Concerns\HasHookName;
10+
use Syntatis\FeatureFlipper\Helpers\Option;
11+
12+
use function is_numeric;
13+
14+
use const PHP_INT_MAX;
15+
16+
class ManagePostEditor implements Hookable
17+
{
18+
use HasHookName;
19+
20+
private bool $heartbeat;
21+
22+
public function __construct()
23+
{
24+
$this->heartbeat = (bool) Option::get('heartbeat');
25+
}
26+
27+
public function hook(Hook $hook): void
28+
{
29+
$hook->addAction('admin_init', [$this, 'deregisterScripts'], PHP_INT_MAX);
30+
$hook->addFilter('heartbeat_settings', [$this, 'filterSettings'], PHP_INT_MAX);
31+
$hook->addFilter(
32+
self::optionName('heartbeat_post_editor'),
33+
fn ($value) => $this->heartbeat ? $value : $this->heartbeat,
34+
);
35+
$hook->addFilter(
36+
self::defaultOptionName('heartbeat_post_editor'),
37+
fn ($value) => $this->heartbeat ? $value : $this->heartbeat,
38+
);
39+
$hook->addFilter(
40+
self::optionName('heartbeat_post_editor_interval'),
41+
fn ($value) => (bool) Option::get('heartbeat_post_editor') && $this->heartbeat ? $value : null,
42+
);
43+
$hook->addFilter(
44+
self::defaultOptionName('heartbeat_post_editor_interval'),
45+
fn ($value) => (bool) Option::get('heartbeat_post_editor') && $this->heartbeat ? $value : null,
46+
);
47+
}
48+
49+
public function deregisterScripts(): void
50+
{
51+
if (! self::isPostEditor() || (bool) Option::get('heartbeat_post_editor')) {
52+
return;
53+
}
54+
55+
wp_deregister_script('heartbeat');
56+
}
57+
58+
/**
59+
* @param array<string,mixed> $settings
60+
*
61+
* @return array<string,mixed>
62+
*/
63+
public function filterSettings(array $settings): array
64+
{
65+
// If it's not a post edit screen, return the settings as is.
66+
if (! is_admin() || ! self::isPostEditor()) {
67+
return $settings;
68+
}
69+
70+
$interval = Option::get('heartbeat_post_editor_interval');
71+
72+
if (is_numeric($interval)) {
73+
$interval = absint($interval);
74+
75+
$settings['interval'] = $interval;
76+
$settings['minimalInterval'] = $interval;
77+
}
78+
79+
return $settings;
80+
}
81+
82+
private static function isPostEditor(): bool
83+
{
84+
$pagenow = $GLOBALS['pagenow'] ?? '';
85+
86+
return is_admin() && ($pagenow === 'post.php' || $pagenow === 'post-new.php');
87+
}
88+
}

app/Features/SitePrivate.php

+4-17
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,19 @@
77
use SSFV\Codex\Contracts\Hookable;
88
use SSFV\Codex\Facades\App;
99
use SSFV\Codex\Foundation\Hooks\Hook;
10+
use Syntatis\FeatureFlipper\Concerns\HasURI;
1011
use Syntatis\FeatureFlipper\Helpers\Option;
1112

1213
use function defined;
13-
use function is_string;
1414
use function preg_replace;
1515
use function printf;
16-
use function sprintf;
17-
use function trim;
1816

1917
use const PHP_INT_MIN;
2018

2119
class SitePrivate implements Hookable
2220
{
21+
use HasURI;
22+
2323
public function hook(Hook $hook): void
2424
{
2525
if (! (bool) Option::get('site_private')) {
@@ -33,11 +33,7 @@ public function hook(Hook $hook): void
3333

3434
public function forceLogin(): void
3535
{
36-
if (
37-
( defined('DOING_AJAX') && DOING_AJAX ) ||
38-
( defined('DOING_CRON') && DOING_CRON ) ||
39-
( defined('WP_CLI') && WP_CLI )
40-
) {
36+
if (wp_doing_ajax() || wp_doing_cron() || ( defined('WP_CLI') && WP_CLI )) {
4137
return;
4238
}
4339

@@ -69,13 +65,4 @@ public function showRightNowStatus(): void
6965
esc_html(__('The site is currently in private mode', 'syntatis-feature-flipper')),
7066
);
7167
}
72-
73-
private static function getCurrentUrl(): string
74-
{
75-
$schema = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://';
76-
$host = isset($_SERVER['HTTP_HOST']) && is_string($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
77-
$uri = isset($_SERVER['REQUEST_URI']) && is_string($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
78-
79-
return trim($host) !== '' ? sprintf('%s%s%s', $schema, $host, $uri) : '';
80-
}
8168
}

0 commit comments

Comments
 (0)