PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
desktop-mode / includes / feedback / deactivation.php

deactivation.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.10, at includes/feedback/deactivation.php

249 lines 9.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — deactivation feedback: the screen hook, the payload
4 * and the forwarder.
5 *
6 * Three surfaces show the dialog and all three call the same REST
7 * route: the classic `plugins.php` (the primary surface — the sites
8 * we most need to hear from never opened OpenStation), the same page
9 * inside a chromeless window, and the native Plugins app. The first
10 * two get the bundle from `admin_enqueue_scripts` below; the app
11 * lazy-loads it from the config block `apps/plugins/plugins.os.php`
12 * ships through {@see openstation_deactivation_feedback_client_config()}.
13 *
14 * @package OpenStation
15 */
16
17 defined( 'ABSPATH' ) || exit;
18
19 /** The reasons the dialog offers, as the slugs the intake stores. */
20 const OPENSTATION_FEEDBACK_REASONS = array( 'changed_too_much', 'missing_features', 'too_buggy', 'other' );
21
22 /** Where a submission came from. */
23 const OPENSTATION_FEEDBACK_CONTEXTS = array( 'classic', 'chromeless', 'app' );
24
25 /** Longest free-text `details` forwarded, in characters. */
26 const OPENSTATION_FEEDBACK_DETAILS_MAX = 1000;
27
28 /**
29 * The static half of what the dialog needs, shared by the screen hook
30 * and the Plugins app config.
31 *
32 * @param string $context One of {@see OPENSTATION_FEEDBACK_CONTEXTS}.
33 * @return array{ plugin:string, restUrl:string, restNonce:string, context:string }
34 */
35 function openstation_deactivation_feedback_client_config( $context ) {
36 if ( ! in_array( $context, OPENSTATION_FEEDBACK_CONTEXTS, true ) ) {
37 $context = 'classic';
38 }
39 return array(
40 'plugin' => plugin_basename( OPENSTATION_FILE ),
41 'restUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/feedback/deactivation' ) ),
42 'restNonce' => wp_create_nonce( 'wp_rest' ),
43 'context' => $context,
44 );
45 }
46
47 /**
48 * What the native Plugins app needs to lazy-load the dialog, or
49 * `null` when the feature is off. Rides the app's per-viewer config
50 * (`apps/plugins/plugins.os.php`).
51 *
52 * The bundle goes through the shell's `loadVendorScript`, which
53 * appends a raw `<script src>` and never prints the handle, so the
54 * translations `wp_set_script_translations()` attached are harvested
55 * here the way every other lazy bundle's are.
56 *
57 * @return array{ script:array{ url:string, translations:string }, styleUrl:string, restUrl:string }|null
58 */
59 function openstation_deactivation_feedback_app_config() {
60 if ( ! openstation_deactivation_feedback_enabled() ) {
61 return null;
62 }
63 $script = function_exists( 'openstation_resolve_script_payload' )
64 ? openstation_resolve_script_payload( 'os-deactivation-feedback' )
65 : array();
66 $url = ! empty( $script['url'] )
67 ? (string) $script['url']
68 : OPENSTATION_URL . 'assets/js/deactivation-feedback' . openstation_asset_suffix() . '.js';
69 return array(
70 'script' => array(
71 'url' => esc_url_raw( $url ),
72 'translations' => isset( $script['translations'] ) ? (string) $script['translations'] : '',
73 ),
74 'styleUrl' => esc_url_raw( OPENSTATION_URL . 'assets/css/deactivation-feedback.css' ),
75 'restUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/feedback/deactivation' ) ),
76 );
77 }
78
79 /**
80 * Enqueue the dialog bundle on the Plugins screen — classic admin,
81 * chromeless window and network admin alike — for anyone who can
82 * deactivate a plugin.
83 *
84 * The bundle intercepts the Deactivate link on OpenStation's own row.
85 * It is not in the chromeless trim list (`includes/render/chromeless-trim.php`
86 * drops the admin-bar family only), so it survives inside a window.
87 *
88 * @param string $hook_suffix Current admin page.
89 */
90 function openstation_feedback_enqueue_deactivation_dialog( $hook_suffix ) {
91 if ( 'plugins.php' !== $hook_suffix || ! current_user_can( 'activate_plugins' ) ) {
92 return;
93 }
94 if ( ! openstation_deactivation_feedback_enabled() ) {
95 return;
96 }
97 $context = function_exists( 'openstation_is_chromeless_request' ) && openstation_is_chromeless_request()
98 ? 'chromeless'
99 : 'classic';
100 wp_enqueue_style( 'os-deactivation-feedback' );
101 wp_enqueue_script( 'os-deactivation-feedback' );
102 wp_add_inline_script(
103 'os-deactivation-feedback',
104 'window.openStationDeactivationFeedbackConfig = ' . wp_json_encode( openstation_deactivation_feedback_client_config( $context ) ) . ';',
105 'before'
106 );
107 }
108 add_action( 'admin_enqueue_scripts', 'openstation_feedback_enqueue_deactivation_dialog' );
109
110 /**
111 * The real moment a first-run stamp records, in epoch seconds, or
112 * `null` when it is absent or its age is unknown.
113 *
114 * `includes/first-run/stamps.php` writes `{ at, via }`: `via` is
115 * `activation` when the stamp was written at the real moment and
116 * `backfill` when it was reconstructed later for an install that
117 * predates it. A backfilled `at` is the moment we noticed, not the
118 * moment it happened, so it is reported as unknown rather than as a
119 * number wrong by an arbitrary amount.
120 *
121 * @param array{at:int,via:string}|null $stamp A normalised stamp.
122 * @return int|null
123 */
124 function openstation_feedback_stamp_moment( $stamp ) {
125 if ( null === $stamp || 'activation' !== $stamp['via'] || $stamp['at'] <= 0 ) {
126 return null;
127 }
128 return (int) $stamp['at'];
129 }
130
131 /**
132 * Whole days between two moments, floored at zero, or `null` when
133 * either is unknown.
134 *
135 * @param int|null $from Earlier moment, epoch seconds.
136 * @param int|null $to Later moment, epoch seconds.
137 * @return int|null
138 */
139 function openstation_feedback_days_between( $from, $to ) {
140 if ( null === $from || null === $to ) {
141 return null;
142 }
143 return max( 0, (int) floor( ( $to - $from ) / DAY_IN_SECONDS ) );
144 }
145
146 /**
147 * Build the anonymous payload for one submission.
148 *
149 * Deliberately no site id, no home URL hash, no user data. The random
150 * per-submission id exists only so the intake can ignore a retry.
151 * Every field is listed in `readme.txt` under "External services";
152 * add one here and add it there in the same change.
153 *
154 * @param string[] $reasons Any of {@see OPENSTATION_FEEDBACK_REASONS}; the
155 * dialog lets the admin tick several.
156 * @param string $details Free text, optional.
157 * @param string $context One of {@see OPENSTATION_FEEDBACK_CONTEXTS}.
158 * @return array
159 */
160 function openstation_deactivation_feedback_payload( $reasons, $details = '', $context = 'classic' ) {
161 // Known slugs only, deduplicated, in the dialog's own order.
162 $reasons = array_values(
163 array_intersect( OPENSTATION_FEEDBACK_REASONS, array_map( 'strval', (array) $reasons ) )
164 );
165 if ( empty( $reasons ) ) {
166 $reasons = array( 'other' );
167 }
168 if ( ! in_array( $context, OPENSTATION_FEEDBACK_CONTEXTS, true ) ) {
169 $context = 'classic';
170 }
171 $details = sanitize_textarea_field( (string) $details );
172 if ( mb_strlen( $details ) > OPENSTATION_FEEDBACK_DETAILS_MAX ) {
173 $details = mb_substr( $details, 0, OPENSTATION_FEEDBACK_DETAILS_MAX );
174 }
175
176 $enabled_users = function_exists( 'openstation_users_with_prior_desktop_use' )
177 ? openstation_users_with_prior_desktop_use()
178 : array();
179
180 $php = explode( '.', PHP_VERSION );
181
182 $installed_at = openstation_feedback_stamp_moment( openstation_get_install_stamp() );
183 $first_enabled_at = openstation_feedback_stamp_moment( openstation_get_first_enabled_stamp() );
184
185 // Site-activated plugins, plus the network-activated ones on a
186 // multisite: `active_plugins` alone would under-count a network.
187 // Deduplicated, because network activation does not remove a
188 // plugin from a site's own list.
189 $active_plugins = count(
190 array_unique(
191 array_merge(
192 (array) get_option( 'active_plugins', array() ),
193 is_multisite() ? array_keys( (array) get_site_option( 'active_sitewide_plugins', array() ) ) : array()
194 )
195 )
196 );
197
198 return array(
199 'id' => wp_generate_uuid4(),
200 'reasons' => $reasons,
201 'details' => $details,
202 'plugin_version' => OPENSTATION_VERSION,
203 'wp_version' => get_bloginfo( 'version' ),
204 'php_version' => $php[0] . '.' . ( isset( $php[1] ) ? $php[1] : '0' ),
205 'locale' => get_locale(),
206 'multisite' => is_multisite(),
207 'install_age_days' => openstation_feedback_days_between( $installed_at, time() ),
208 'ever_enabled' => count( $enabled_users ) > 0,
209 'enabled_user_count' => count( $enabled_users ),
210 'first_enable_delay_days' => openstation_feedback_days_between( $installed_at, $first_enabled_at ),
211 'deactivator_enabled' => openstation_is_enabled(),
212 'active_plugins' => $active_plugins,
213 'context' => $context,
214 );
215 }
216
217 /**
218 * Forward one payload to the intake. Synchronous, short and
219 * best-effort: the plugin is about to be deactivated, so a cron job
220 * would never run, and the admin should wait three seconds at most.
221 *
222 * @param array $payload The filtered payload.
223 * @return bool True on a 2xx answer.
224 */
225 function openstation_deactivation_feedback_forward( array $payload ) {
226 /**
227 * Filters the intake URL. Hosts that run their own intake (an
228 * internal one, say) point this at it; it receives the JSON
229 * payload by POST.
230 *
231 * @param string $endpoint Default {@see OPENSTATION_FEEDBACK_ENDPOINT}.
232 */
233 $endpoint = (string) apply_filters( 'openstation_deactivation_feedback_endpoint', OPENSTATION_FEEDBACK_ENDPOINT );
234 if ( '' === $endpoint ) {
235 return false;
236 }
237 $response = wp_remote_post(
238 $endpoint,
239 array(
240 'timeout' => 3,
241 'redirection' => 0,
242 'user-agent' => 'WP OpenStation feedback/' . OPENSTATION_VERSION,
243 'headers' => array( 'Content-Type' => 'application/json' ),
244 'body' => wp_json_encode( $payload ),
245 )
246 );
247 return ! is_wp_error( $response ) && 2 === (int) floor( wp_remote_retrieve_response_code( $response ) / 100 );
248 }
249