PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.7
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.7
1.3.7 1.3.6 1.3.5 1.3.4 1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 All 33 releases
xspeed / includes / modules / Privacy / PrivacyModule.php

PrivacyModule.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.3.7, at includes/modules/Privacy/PrivacyModule.php

183 lines 6.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Privacy module — the dashboard control for usage-analytics consent.
4 *
5 * Consent was collectable in exactly one place, the setup wizard, and
6 * withdrawable in none: `Onboarding::apply()` was the only writer in Free or
7 * Pro, no module schema carried a field for it, and the strings only ever
8 * appeared in the wizard bundle. So the wizard's own "Change it anytime from
9 * the dashboard" and readme.txt's "disable it later from the xSpeed Cache
10 * dashboard" were both untrue — the only way to withdraw consent was to
11 * re-run the whole wizard. (#437)
12 *
13 * This module is that missing control, and being a Module rather than a
14 * bespoke panel is what gives it the dashboard, `wp xspeed privacy` and MCP
15 * `run_command` in one go (IMPLEMENTATION.md §17).
16 *
17 * The setting is a VIEW over WP Insights' own `wpins_allow_tracking` row, not
18 * a copy of it:
19 * - reads come from the tracker via `xspeed_setting_external_source`, so
20 * the panel can never disagree with what the tracker actually believes;
21 * - writes go through `Usage_Tracker::opt_in()` on `xspeed_settings_saved`,
22 * because consent is not just a flag — opting in schedules the cron and
23 * registers the install, opting out clears the cron. Writing the option
24 * row alone would leave a site "opted out" in the UI with the daily send
25 * still scheduled.
26 *
27 * @package XSpeed
28 */
29
30 declare(strict_types=1);
31
32 namespace XSpeed\Modules\Privacy;
33
34 defined( 'ABSPATH' ) || exit;
35
36 use XSpeed\Module;
37 use XSpeed\Plugin;
38
39 final class PrivacyModule extends Module {
40
41 public const SLUG = 'privacy';
42 public const TIER = self::TIER_FREE;
43 public const VERSION = '1.0.0';
44
45 public function ui_metadata(): array {
46 return array(
47 'label' => __( 'Privacy & usage data', 'xspeed' ),
48 'icon' => 'ShieldCheck',
49 'description' => __( 'Change the usage data choice you made in the setup wizard.', 'xspeed' ),
50 'group' => 'settings',
51 );
52 }
53
54 public function settings_schema(): array {
55 return array(
56 'usage_tracking' => array(
57 'type' => 'bool',
58 'default' => false,
59 'label' => __( 'Share usage data', 'xspeed' ),
60 'description' => __( 'Sends your WordPress and PHP versions, theme, plugins, server type and the features you use, never page content. Turn it off to stop all sending.', 'xspeed' ),
61 ),
62 );
63 }
64
65 public function boot(): void {
66 // Read the tracker's own state rather than our option row. The row is
67 // still written (Settings_Manager owns that), but it is never the
68 // source of truth: a site that opted in through the wizard has no
69 // row at all, and would otherwise render as opted out.
70 add_filter( 'xspeed_setting_external_source', array( $this, 'read_consent' ), 10, 3 );
71 add_action( 'xspeed_settings_saved', array( $this, 'apply_consent' ), 10, 2 );
72 }
73
74 /**
75 * Answer reads of `privacy.usage_tracking` from the tracker.
76 *
77 * @param mixed $value Value resolved so far (null = not ours).
78 * @param string $slug Module slug being read.
79 * @param string $key Setting key being read.
80 * @return mixed
81 */
82 public function read_consent( $value, $slug, $key ) {
83 if ( self::SLUG !== $slug || 'usage_tracking' !== $key ) {
84 return $value;
85 }
86 $tracker = $this->tracker();
87
88 return ( $tracker && method_exists( $tracker, 'is_opted_in' ) ) ? (bool) $tracker->is_opted_in() : false;
89 }
90
91 /**
92 * The consent authority.
93 *
94 * Filterable so this module can be exercised against a double — the
95 * Plugin accessor is typed to the concrete Usage_Tracker, so there is
96 * otherwise no seam that does not require booting the whole singleton.
97 *
98 * @return object|null
99 */
100 private function tracker() {
101 /**
102 * Filter the object that answers usage-analytics consent.
103 *
104 * @param object|null $tracker Usage_Tracker instance, or null.
105 */
106 return apply_filters( 'xspeed_usage_consent_tracker', Plugin::instance()->usage_tracker() );
107 }
108
109 /**
110 * Route a save through the tracker so the cron follows the flag.
111 *
112 * Only acts when the key was actually part of the save: a write to some
113 * other module, or a partial save that never mentioned consent, must not
114 * be read as the admin revoking it.
115 *
116 * @param string $slug Module whose settings were saved.
117 * @param array<string,mixed> $clean Settings as stored.
118 */
119 public function apply_consent( $slug, $clean ): void {
120 if ( self::SLUG !== $slug || ! is_array( $clean ) || ! array_key_exists( 'usage_tracking', $clean ) ) {
121 return;
122 }
123 $tracker = $this->tracker();
124 if ( ! $tracker || ! method_exists( $tracker, 'opt_in' ) ) {
125 return;
126 }
127 $wanted = ! empty( $clean['usage_tracking'] );
128 // opt_in( true ) sends immediately to register the install, so only
129 // call it on an actual change — re-saving an unrelated field on this
130 // panel must not fire a payload.
131 if ( method_exists( $tracker, 'is_opted_in' ) && (bool) $tracker->is_opted_in() === $wanted ) {
132 return;
133 }
134 $tracker->opt_in( $wanted );
135 }
136
137 /**
138 * CLI surface — and with it MCP, which dispatches to these same
139 * callbacks. `status` is the minimum every module owes
140 * tests/e2e/50-cli-mcp-coverage.spec.ts.
141 */
142 public function cli_commands(): array {
143 return array(
144 array(
145 'name' => 'xspeed privacy',
146 'callback' => array( $this, 'cli_privacy' ),
147 'shortdesc' => 'Show or change usage-analytics consent.',
148 'ai_hint' => 'Whether this site shares usage analytics, and the way to turn that on or off. Use for "am I sending telemetry", "stop sharing usage data", or any consent/privacy question about analytics.',
149 'synopsis' => array(
150 array(
151 'type' => 'positional',
152 'name' => 'action',
153 'optional' => true,
154 'options' => array( 'status', 'enable', 'disable' ),
155 ),
156 ),
157 ),
158 );
159 }
160
161 /**
162 * @param array<int,string> $args Positional args.
163 * @param array<string,string> $assoc_args Flags.
164 */
165 public function cli_privacy( $args = array(), $assoc_args = array() ): void {
166 unset( $assoc_args );
167 $action = isset( $args[0] ) ? (string) $args[0] : 'status';
168 $tracker = $this->tracker();
169 if ( ! $tracker ) {
170 \WP_CLI::error( 'Usage tracker unavailable.' );
171 return;
172 }
173
174 if ( 'enable' === $action || 'disable' === $action ) {
175 $this->update_settings( array( 'usage_tracking' => 'enable' === $action ) );
176 }
177
178 $on = method_exists( $tracker, 'is_opted_in' ) && $tracker->is_opted_in();
179 \WP_CLI::log( 'usage analytics: ' . ( $on ? 'on' : 'off' ) );
180 \WP_CLI::log( 'scheduled send: ' . ( wp_next_scheduled( \XSpeed\Usage_Tracker::EVENT_HOOK ) ? 'scheduled' : 'none' ) );
181 }
182 }
183