PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.3
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.3
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 1.1.5 1.1.6 1.1.7 1.1.8 All 29 releases
xspeed / includes / modules / Privacy / PrivacyModule.php

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

182 lines 6.6 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' => __( 'Control the anonymous usage analytics you were asked about in the setup wizard.', 'xspeed' ),
50 );
51 }
52
53 public function settings_schema(): array {
54 return array(
55 'usage_tracking' => array(
56 'type' => 'bool',
57 'default' => false,
58 'label' => __( 'Share anonymous usage data', 'xspeed' ),
59 'description' => __( 'Share anonymous basics — WordPress & PHP version, active theme & plugins, server type, and which features you enable. Never personal data or page content. Turning this off stops all collection and clears the scheduled send.', 'xspeed' ),
60 ),
61 );
62 }
63
64 public function boot(): void {
65 // Read the tracker's own state rather than our option row. The row is
66 // still written (Settings_Manager owns that), but it is never the
67 // source of truth: a site that opted in through the wizard has no
68 // row at all, and would otherwise render as opted out.
69 add_filter( 'xspeed_setting_external_source', array( $this, 'read_consent' ), 10, 3 );
70 add_action( 'xspeed_settings_saved', array( $this, 'apply_consent' ), 10, 2 );
71 }
72
73 /**
74 * Answer reads of `privacy.usage_tracking` from the tracker.
75 *
76 * @param mixed $value Value resolved so far (null = not ours).
77 * @param string $slug Module slug being read.
78 * @param string $key Setting key being read.
79 * @return mixed
80 */
81 public function read_consent( $value, $slug, $key ) {
82 if ( self::SLUG !== $slug || 'usage_tracking' !== $key ) {
83 return $value;
84 }
85 $tracker = $this->tracker();
86
87 return ( $tracker && method_exists( $tracker, 'is_opted_in' ) ) ? (bool) $tracker->is_opted_in() : false;
88 }
89
90 /**
91 * The consent authority.
92 *
93 * Filterable so this module can be exercised against a double — the
94 * Plugin accessor is typed to the concrete Usage_Tracker, so there is
95 * otherwise no seam that does not require booting the whole singleton.
96 *
97 * @return object|null
98 */
99 private function tracker() {
100 /**
101 * Filter the object that answers usage-analytics consent.
102 *
103 * @param object|null $tracker Usage_Tracker instance, or null.
104 */
105 return apply_filters( 'xspeed_usage_consent_tracker', Plugin::instance()->usage_tracker() );
106 }
107
108 /**
109 * Route a save through the tracker so the cron follows the flag.
110 *
111 * Only acts when the key was actually part of the save: a write to some
112 * other module, or a partial save that never mentioned consent, must not
113 * be read as the admin revoking it.
114 *
115 * @param string $slug Module whose settings were saved.
116 * @param array<string,mixed> $clean Settings as stored.
117 */
118 public function apply_consent( $slug, $clean ): void {
119 if ( self::SLUG !== $slug || ! is_array( $clean ) || ! array_key_exists( 'usage_tracking', $clean ) ) {
120 return;
121 }
122 $tracker = $this->tracker();
123 if ( ! $tracker || ! method_exists( $tracker, 'opt_in' ) ) {
124 return;
125 }
126 $wanted = ! empty( $clean['usage_tracking'] );
127 // opt_in( true ) sends immediately to register the install, so only
128 // call it on an actual change — re-saving an unrelated field on this
129 // panel must not fire a payload.
130 if ( method_exists( $tracker, 'is_opted_in' ) && (bool) $tracker->is_opted_in() === $wanted ) {
131 return;
132 }
133 $tracker->opt_in( $wanted );
134 }
135
136 /**
137 * CLI surface — and with it MCP, which dispatches to these same
138 * callbacks. `status` is the minimum every module owes
139 * tests/e2e/50-cli-mcp-coverage.spec.ts.
140 */
141 public function cli_commands(): array {
142 return array(
143 array(
144 'name' => 'xspeed privacy',
145 'callback' => array( $this, 'cli_privacy' ),
146 'shortdesc' => 'Show or change usage-analytics consent.',
147 'ai_hint' => 'Whether this site shares anonymous 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.',
148 'synopsis' => array(
149 array(
150 'type' => 'positional',
151 'name' => 'action',
152 'optional' => true,
153 'options' => array( 'status', 'enable', 'disable' ),
154 ),
155 ),
156 ),
157 );
158 }
159
160 /**
161 * @param array<int,string> $args Positional args.
162 * @param array<string,string> $assoc_args Flags.
163 */
164 public function cli_privacy( $args = array(), $assoc_args = array() ): void {
165 unset( $assoc_args );
166 $action = isset( $args[0] ) ? (string) $args[0] : 'status';
167 $tracker = $this->tracker();
168 if ( ! $tracker ) {
169 \WP_CLI::error( 'Usage tracker unavailable.' );
170 return;
171 }
172
173 if ( 'enable' === $action || 'disable' === $action ) {
174 $this->update_settings( array( 'usage_tracking' => 'enable' === $action ) );
175 }
176
177 $on = method_exists( $tracker, 'is_opted_in' ) && $tracker->is_opted_in();
178 \WP_CLI::log( 'usage analytics: ' . ( $on ? 'on' : 'off' ) );
179 \WP_CLI::log( 'scheduled send: ' . ( wp_next_scheduled( \XSpeed\Usage_Tracker::EVENT_HOOK ) ? 'scheduled' : 'none' ) );
180 }
181 }
182