PluginProbe
Blocks – Reusable Content, Shortcodes & Site Variables / trunk
Blocks – Reusable Content, Shortcodes & Site Variables vtrunk
26.09.03.15 26.08.31.21 26.08.22.20 26.08.22.22 26.08.22.17 26.08.22.13 26.08.21.19 26.08.07.23 26.07.19.14 26.07.13.21 26.07.13.17 26.07.12.13 026.07.07.21 026.07.05.18 026.06.26.20 026.06.26.21 026.06.08.20 026.05.13.14 026.04.29.10 trunk 026.02.22.22 026.03.16.23 026.04.23.13
blocks / src / Signls / Runtime.php

Runtime.php in Blocks – Reusable Content, Shortcodes & Site Variables trunk, at src/Signls/Runtime.php

272 lines 10.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace RenzoJohnson\Blocks\Signls;
6
7 \defined( 'ABSPATH' ) || exit;
8
9 /** Coordinates Blocks consent and lifecycle with the shared Signls SDK. */
10 final class Runtime {
11
12 public const PRODUCT = 'blocks';
13 public const SDK_VERSION = '1.1.10';
14 public const CONSENT_OPTION = 'blocks_signls_sharing_enabled';
15 public const INSTALL_ID_OPTION = 'blocks_signls_install_id';
16 public const LIFECYCLE_OPTION = 'blocks_signls_lifecycle';
17
18 private const NOTICE_VERSION = 1;
19
20 private bool $created_default = false;
21 private bool $adapter_booted = false;
22
23 public function __construct( private readonly \Blocks_Settings_Repository $settings ) {
24 }
25
26 public function register_hooks(): void {
27 $this->created_default = $this->initialize_consent();
28
29 $loader = BLOCKS_PLUGIN_DIR . '/includes/signls/loader.php';
30 if ( ! \is_file( $loader ) ) {
31 return;
32 }
33
34 require_once $loader;
35 \Blocks_Lifecycle::register_for( self::CONSENT_OPTION, $this->consent_changed( ... ) );
36 \add_action( 'admin_init', $this->add_policy_content( ... ) );
37 \add_action( 'activated_plugin', $this->activation_complete( ... ), 1, 2 );
38 if ( 0 < \did_action( 'plugins_loaded' ) ) {
39 $this->after_boot();
40 return;
41 }
42 \add_action( 'plugins_loaded', $this->after_boot( ... ), PHP_INT_MAX );
43 }
44
45 public function after_boot(): void {
46 if ( $this->adapter_booted || ! \class_exists( 'Signls_Sdk_Bridge_1_1_7', false ) ) {
47 return;
48 }
49
50 if (
51 ! \interface_exists( 'Signls\\Sdk\\V1\\ProductAdapterInterface' )
52 && \is_callable( array( 'Signls_Sdk_Loader', 'boot' ) )
53 ) {
54 \Signls_Sdk_Loader::boot();
55 }
56 if ( ! \interface_exists( 'Signls\\Sdk\\V1\\ProductAdapterInterface' ) ) {
57 return;
58 }
59
60 $adapter = new Adapter( $this->settings );
61 if ( ! \Signls_Sdk_Bridge_1_1_7::register(
62 array(
63 'product_slug' => self::PRODUCT,
64 'main_file' => BLOCKS_PLUGIN,
65 'sdk_path' => BLOCKS_PLUGIN_DIR . '/includes/signls',
66 'sdk_version' => self::SDK_VERSION,
67 'adapter' => $adapter,
68 )
69 ) ) {
70 return;
71 }
72 $this->adapter_booted = true;
73 foreach ( $adapter->observed_options() as $option_name ) {
74 \Blocks_Lifecycle::register_for( $option_name, $this->relevant_option_changed( ... ) );
75 }
76 $this->synchronize( $this->created_default ? 'default_enabled' : 'product_state_sync' );
77 if ( self::record_upgrade() ) {
78 \Signls_Sdk_Bridge_1_1_7::relevant_change( self::PRODUCT );
79 }
80 }
81
82 public function activation_complete( string $plugin, bool $network_wide ): void {
83 unset( $network_wide );
84 if ( BLOCKS_PLUGIN_BASENAME !== $plugin ) {
85 return;
86 }
87 $this->after_boot();
88 if ( ! $this->adapter_booted || ! self::enabled() ) {
89 return;
90 }
91 \Signls_Sdk_Bridge_1_1_7::relevant_change( self::PRODUCT );
92 \Signls_Sdk_Bridge_1_1_7::flush_immediate( self::PRODUCT );
93 }
94
95 public static function enabled(): bool {
96 $value = \get_option( self::CONSENT_OPTION, 1 );
97 return \is_scalar( $value ) && 0 < (int) $value;
98 }
99
100 public static function install_id(): string {
101 $stored = \get_option( self::INSTALL_ID_OPTION, '' );
102 $stored = \is_string( $stored ) ? \strtolower( $stored ) : '';
103 if ( 1 === \preg_match( '/^[a-f0-9]{32}$/D', $stored ) ) {
104 return $stored;
105 }
106
107 $generated = \bin2hex( \random_bytes( 16 ) );
108 if ( false === \get_option( self::INSTALL_ID_OPTION, false ) ) {
109 \add_option( self::INSTALL_ID_OPTION, $generated, '', false );
110 } else {
111 \update_option( self::INSTALL_ID_OPTION, $generated, false );
112 }
113
114 $saved = \get_option( self::INSTALL_ID_OPTION, '' );
115 if ( ! \is_string( $saved ) || $generated !== $saved ) {
116 throw new \RuntimeException( 'Unable to persist the Blocks installation identity.' );
117 }
118 return $generated;
119 }
120
121 public static function clear_schedules(): void {
122 $hash = \substr( \hash( 'sha256', self::PRODUCT ), 0, 12 );
123 \wp_clear_scheduled_hook( 'signls_sdk_v1_' . $hash . '_routine' );
124 \wp_clear_scheduled_hook( 'signls_sdk_v1_' . $hash . '_refresh' );
125 }
126
127 public static function record_activation(): void {
128 self::record_lifecycle_event( 'activations', true );
129 }
130
131 public static function record_deactivation(): void {
132 self::record_lifecycle_event( 'deactivations', false );
133 }
134
135 public static function deactivate(): void {
136 if ( \class_exists( 'Signls_Sdk_Bridge_1_1_7', false ) ) {
137 \Signls_Sdk_Bridge_1_1_7::deactivate( self::PRODUCT );
138 return;
139 }
140 self::clear_schedules();
141 }
142
143 public static function uninstall(): void {
144 self::clear_schedules();
145 \delete_option( self::CONSENT_OPTION );
146 \delete_option( self::INSTALL_ID_OPTION );
147 \delete_option( self::LIFECYCLE_OPTION );
148 \delete_option( 'signls_sdk_v1_' . \substr( \hash( 'sha256', self::PRODUCT ), 0, 12 ) );
149
150 $sdk = BLOCKS_PLUGIN_DIR . '/includes/signls/sdk.php';
151 if ( \is_file( $sdk ) ) {
152 require_once $sdk;
153 }
154 if ( \class_exists( 'Signls\\Sdk\\V1\\CounterStore' ) ) {
155 global $wpdb;
156 if ( ! $wpdb instanceof \wpdb ) {
157 return;
158 }
159 $table = \Signls\Sdk\V1\CounterStore::table();
160 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Read-only existence guard before SDK-owned uninstall cleanup.
161 $exists = $table === $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) );
162 if ( ! $exists ) {
163 return;
164 }
165 try {
166 \Signls\Sdk\V1\CounterStore::delete_product( self::PRODUCT );
167 } catch ( \Throwable $error ) {
168 unset( $error );
169 }
170 }
171 }
172
173 public function add_policy_content(): void {
174 if ( ! \function_exists( 'wp_add_privacy_policy_content' ) ) {
175 return;
176 }
177
178 $content = '<p>' . \esc_html__( 'Blocks shares a bounded product snapshot with Signls to guide product improvement, compatibility, support, and reliability work. Sharing is enabled by default on a new installation and an administrator can turn it off at any time under Blocks Settings.', 'blocks' ) . '</p>';
179 $content .= '<p>' . \esc_html__( 'The snapshot contains Blocks, WordPress, and PHP versions; multisite status; and fixed aggregate 0 or 1 states for supported content, builder, commerce, analytics, anti-spam, live chat, performance, security, SVG, compatibility, lifecycle, and reliability capabilities. It uses random pseudonymous installation and device identifiers.', 'blocks' ) . '</p>';
180 $content .= '<p>' . \esc_html__( 'It excludes content, field values, email addresses, physical addresses, URLs, WordPress or commerce IDs, secrets, tokens, raw errors, customer and order data, messages, IP addresses, page journeys, and arbitrary settings or strings. Delivery receipts are retained for 90 days, current snapshots are replaced by newer observations, and daily installation aggregates are retained for long-term product analysis.', 'blocks' ) . '</p>';
181 \wp_add_privacy_policy_content( 'Blocks', \wp_kses_post( $content ) );
182 }
183
184 private function initialize_consent(): bool {
185 if ( null !== \get_option( self::CONSENT_OPTION, null ) ) {
186 return false;
187 }
188 return \add_option( self::CONSENT_OPTION, 1, '', false );
189 }
190
191 private function consent_changed( mixed $old_value, mixed $new_value ): void {
192 $old_enabled = \is_scalar( $old_value ) && 0 < (int) $old_value;
193 $new_enabled = \is_scalar( $new_value ) && 0 < (int) $new_value;
194 if ( $old_enabled === $new_enabled ) {
195 return;
196 }
197 $this->synchronize( 'admin_settings' );
198 }
199
200 private function relevant_option_changed( mixed $old_value, mixed $new_value ): void {
201 if ( $old_value === $new_value || ! self::enabled() || ! \class_exists( 'Signls_Sdk_Bridge_1_1_7', false ) ) {
202 return;
203 }
204 try {
205 \Signls_Sdk_Bridge_1_1_7::relevant_change( self::PRODUCT );
206 } catch ( \Throwable $error ) {
207 unset( $error );
208 }
209 }
210
211 private function synchronize( string $source ): bool {
212 if ( ! \class_exists( 'Signls_Sdk_Bridge_1_1_7', false ) ) {
213 if ( ! self::enabled() ) {
214 self::clear_schedules();
215 }
216 return false;
217 }
218
219 try {
220 $state = \Signls_Sdk_Bridge_1_1_7::state( self::PRODUCT );
221 $sdk_status = isset( $state['consent_status'] ) && \is_string( $state['consent_status'] ) ? $state['consent_status'] : 'unset';
222 if ( self::enabled() && 'enabled' !== $sdk_status ) {
223 return \Signls_Sdk_Bridge_1_1_7::enable( self::PRODUCT, $source, self::NOTICE_VERSION );
224 }
225 if ( ! self::enabled() && 'disabled' !== $sdk_status ) {
226 return \Signls_Sdk_Bridge_1_1_7::disable( self::PRODUCT, $source, self::NOTICE_VERSION );
227 }
228 return true;
229 } catch ( \Throwable $error ) {
230 unset( $error );
231 return false;
232 }
233 }
234
235 private static function record_lifecycle_event( string $event, bool $active ): void {
236 $lifecycle = \get_option( self::LIFECYCLE_OPTION, array() );
237 $lifecycle = \is_array( $lifecycle ) ? $lifecycle : array();
238 $items = isset( $lifecycle[ $event ] ) && \is_array( $lifecycle[ $event ] ) ? $lifecycle[ $event ] : array();
239 if ( 'activations' === $event ) {
240 $lifecycle['is_reactivation'] = ! empty( $items );
241 }
242 $items[] = \time();
243 $lifecycle[ $event ] = \array_slice( $items, -256 );
244 $lifecycle['is_active'] = $active;
245 \update_option( self::LIFECYCLE_OPTION, $lifecycle, false );
246 }
247
248 private static function record_upgrade(): bool {
249 $lifecycle = \get_option( self::LIFECYCLE_OPTION, array() );
250 $lifecycle = \is_array( $lifecycle ) ? $lifecycle : array();
251 $previous = isset( $lifecycle['version'] ) && \is_scalar( $lifecycle['version'] ) ? (string) $lifecycle['version'] : '';
252 $current = \defined( 'BLOCKS_VERSION' ) ? (string) BLOCKS_VERSION : '';
253 if ( '' === $current || '' === $previous || $current === $previous ) {
254 if ( '' !== $current && '' === $previous ) {
255 $lifecycle['version'] = $current;
256 \update_option( self::LIFECYCLE_OPTION, $lifecycle, false );
257 }
258 return false;
259 }
260 $upgrades = isset( $lifecycle['upgrades'] ) && \is_array( $lifecycle['upgrades'] ) ? $lifecycle['upgrades'] : array();
261 $upgrades[] = array(
262 'from' => $previous,
263 'to' => $current,
264 'timestamp' => \time(),
265 );
266 $lifecycle['upgrades'] = \array_slice( $upgrades, -256 );
267 $lifecycle['version'] = $current;
268 \update_option( self::LIFECYCLE_OPTION, $lifecycle, false );
269 return true;
270 }
271 }
272