PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.12
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.12
1.1.12 1.1.11 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 All 36 releases
desktop-mode / apps / plugins / parts / updates.php

updates.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.12, at apps/plugins/parts/updates.php

340 lines 12.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugins app — what the `update_plugins` transient says.
4 *
5 * Part of the `desktop-mode-plugins` app: required by `plugins.os.php`,
6 * plain `.php` on purpose — only `*.os.php` files are app entries to
7 * the framework loader. The transient is primed at most once per
8 * request (Core's own 12h throttle, or the Refresh button's forced
9 * check) and read into one per-request snapshot; the three REST
10 * fields derived from it — the pending update, the directory slug,
11 * the auto-update state — read that snapshot, as does the dock badge
12 * count. Every callback uses only `wp-includes/` functions.
13 *
14 * @package OpenStation
15 */
16
17 // Direct access, unless a standalone host is booting on bare PHP.
18 if ( ! defined( 'ABSPATH' ) ) {
19 defined( 'OPENSTATION_STANDALONE' ) || exit;
20 }
21
22 /**
23 * Lazily prime the `update_plugins` site transient so REST callers see
24 * the same "updates available" picture as the classic Plugins screen.
25 *
26 * Core only refreshes the transient on `load-plugins.php`,
27 * `load-update-core.php`, and the twice-daily cron — REST is not on
28 * that list, so a fresh page load of the Plugins window can see an
29 * empty or stale transient even when the dock badge (computed off
30 * `$menu`, which Core builds against `wp_get_update_data()`) reports
31 * pending updates. We mirror Core's own throttle
32 * (`_maybe_update_plugins()` — 12h since last check) so a hot REST hit
33 * is a transient read, not an HTTPS round trip to api.wordpress.org.
34 *
35 * @param bool $force When true, delete the transient and force a fresh
36 * wp.org check regardless of the 12h throttle.
37 */
38 function openstation_plugins_window_maybe_refresh_update_transient( $force = false ) {
39 /**
40 * Short-circuit the lazy refresh of the `update_plugins` transient.
41 *
42 * Return `false` to skip the refresh — useful for hosts that run
43 * their own update orchestration (managed WordPress, internal
44 * mirrors) and don't want every REST hit to the plugins endpoint
45 * to potentially trigger a wp.org check. The filter also gates the
46 * Refresh button's forced check so hosts that block wp.org calls
47 * outright keep that posture even when the user asks.
48 *
49 * @param bool $refresh Whether to call `wp_update_plugins()`.
50 * @param bool $force Whether the caller asked to bypass the throttle.
51 */
52 if ( ! apply_filters( 'openstation_plugins_window_refresh_updates', true, $force ) ) {
53 return;
54 }
55
56 if ( ! function_exists( 'wp_update_plugins' ) ) {
57 // `wp-includes/update.php` is normally autoloaded on every
58 // request; guard anyway so an unusual bootstrap (mu-plugin
59 // CLI harness, stripped-down REST runtime) doesn't fatal.
60 return;
61 }
62
63 if ( $force ) {
64 // A user-initiated refresh bypasses the throttle: delete the
65 // transient (and the `plugins` cache group), then repopulate it
66 // with a fresh wp.org snapshot. Without the second step the
67 // field callbacks read `false` for the rest of the request and
68 // every row reports "no updates".
69 if ( function_exists( 'wp_clean_plugins_cache' ) ) {
70 wp_clean_plugins_cache( true );
71 } else {
72 delete_site_transient( 'update_plugins' );
73 }
74 wp_update_plugins();
75 return;
76 }
77
78 $current = get_site_transient( 'update_plugins' );
79 if (
80 is_object( $current ) &&
81 isset( $current->last_checked ) &&
82 12 * HOUR_IN_SECONDS > ( time() - (int) $current->last_checked )
83 ) {
84 // Inside Core's standard refresh window — trust the cached
85 // snapshot, identical to `_maybe_update_plugins()`'s posture.
86 return;
87 }
88
89 wp_update_plugins();
90 }
91
92 /**
93 * Prime the `update_plugins` transient at most once per request — the
94 * Refresh action's forced check counts, so the `data()` read that
95 * follows it never asks wp.org twice.
96 *
97 * @param bool $force Bypass Core's throttle (the Refresh button).
98 * @return void
99 */
100 function openstation_plugins_window_prime_updates_once( $force = false ) {
101 static $primed = false;
102 if ( $primed && ! $force ) {
103 return;
104 }
105 $primed = true;
106 openstation_plugins_window_maybe_refresh_update_transient( $force );
107 }
108
109 /**
110 * The `update_plugins` transient, primed at most once per request:
111 * every row's fields and the badge count read it through here.
112 *
113 * Deliberately NOT memoised on top of Core. `get_site_transient()` is
114 * already an in-memory read after the first call of a request, and a
115 * memo of our own would have to be invalidated whenever anything else
116 * rewrites the transient — an upgrader finishing, a forced refresh, a
117 * test priming a fixture. The obvious listeners for that are the
118 * transient's own set/delete hooks, and Plugin Check reads any mention
119 * of those two names as a self-hosted plugin updater
120 * (`plugin_updater_detected`), which a wp.org-hosted plugin may not
121 * ship. Reading Core's cache every time costs nothing and cannot go
122 * stale.
123 *
124 * @return object|null The transient object, or null when it is cold.
125 */
126 function openstation_plugins_window_updates() {
127 openstation_plugins_window_prime_updates_once();
128 $updates = get_site_transient( 'update_plugins' );
129 return is_object( $updates ) ? $updates : null;
130 }
131
132
133 /**
134 * `openstation_update_available` callback.
135 *
136 * @param array $row Core REST plugin row.
137 * @return array{available:bool,new_version:string|null,package:string,slug:string}
138 */
139 function openstation_plugins_window_field_update_available( $row ) {
140 $none = array(
141 'available' => false,
142 'new_version' => null,
143 'package' => '',
144 'slug' => '',
145 );
146 $plugin_file = openstation_plugins_window_row_plugin_file( $row );
147 if ( '' === $plugin_file ) {
148 return $none;
149 }
150
151 $updates = openstation_plugins_window_updates();
152 if ( null === $updates || empty( $updates->response ) || ! is_array( $updates->response ) ) {
153 return $none;
154 }
155 if ( ! isset( $updates->response[ $plugin_file ] ) ) {
156 return $none;
157 }
158
159 $entry = $updates->response[ $plugin_file ];
160 return array(
161 'available' => true,
162 'new_version' => is_object( $entry ) && isset( $entry->new_version ) ? (string) $entry->new_version : null,
163 // The download URL Core's upgrader fetches. Empty for plugins
164 // without a wp.org package (premium / private hosts) — Core
165 // renders "Automatic update is unavailable" there rather than
166 // "Update now", and so does the client.
167 'package' => is_object( $entry ) && ! empty( $entry->package ) ? (string) $entry->package : '',
168 // What `wp_ajax_update_plugin` echoes back in its envelope.
169 'slug' => is_object( $entry ) && ! empty( $entry->slug ) ? (string) $entry->slug : '',
170 );
171 }
172
173 /**
174 * Count plugin updates visible to the Plugins window — updates in the
175 * `update_plugins` transient whose key is an installed plugin file.
176 *
177 * Core's `wp_get_update_data()` reports `count( $response )` verbatim,
178 * which is what `wp-admin/menu.php` embeds in the Plugins menu title
179 * (the source the dock builder captures). That raw count drifts above
180 * the in-window "Update available" filter when the transient holds
181 * orphan entries — files no longer on disk, or rows an `Update URI`
182 * host keyed on a file `get_plugins()` doesn't return. The window
183 * shows a row as updatable iff `response[ $plugin_file ]` is set —
184 * exactly the intersection computed here, so the two surfaces agree.
185 *
186 * @return int Number of installed plugins with a pending update.
187 */
188 function openstation_plugins_window_count_visible_updates() {
189 $updates = get_site_transient( 'update_plugins' );
190 if ( ! is_object( $updates ) || empty( $updates->response ) || ! is_array( $updates->response ) ) {
191 return 0;
192 }
193
194 // `get_plugins()` lives in `wp-admin/includes/plugin.php`. Loaded by
195 // default on every admin request (where `$menu` is built), but
196 // required explicitly so REST, cron and WP-CLI callers can use this
197 // helper without depending on the admin runtime.
198 if ( ! function_exists( 'get_plugins' ) ) {
199 require_once ABSPATH . 'wp-admin/includes/plugin.php';
200 }
201 $installed = get_plugins();
202
203 $count = 0;
204 foreach ( array_keys( $updates->response ) as $plugin_file ) {
205 if ( isset( $installed[ $plugin_file ] ) ) {
206 ++$count;
207 }
208 }
209 return $count;
210 }
211
212 /**
213 * What wp.org last said about one plugin — `slug`, `icons`, versions.
214 *
215 * Both halves have to be read: a plugin is filed under `response`
216 * when an update is pending and `no_update` otherwise, with the same
217 * directory metadata in each. Reading only `response` misses every
218 * up-to-date plugin.
219 *
220 * @param string $plugin_file Plugin file (e.g. `"akismet/akismet.php"`).
221 * @return array|null Null when wp.org doesn't know this plugin, or the
222 * transient is cold.
223 */
224 function openstation_plugins_window_update_entry( $plugin_file ) {
225 if ( '' === $plugin_file ) {
226 return null;
227 }
228 $updates = openstation_plugins_window_updates();
229 if ( null === $updates ) {
230 return null;
231 }
232 if ( isset( $updates->response[ $plugin_file ] ) ) {
233 return (array) $updates->response[ $plugin_file ];
234 }
235 if ( isset( $updates->no_update[ $plugin_file ] ) ) {
236 return (array) $updates->no_update[ $plugin_file ];
237 }
238 return null;
239 }
240
241 /**
242 * `openstation_wporg_slug` callback — is this plugin listed on the
243 * WordPress.org directory, and under which slug? Mirrors Core (see
244 * `WP_Plugins_List_Table::prepare_items()`).
245 *
246 * @param array $row Core REST plugin row.
247 * @return string|null Directory slug, or null when the plugin isn't listed.
248 */
249 function openstation_plugins_window_field_wporg_slug( $row ) {
250 $entry = openstation_plugins_window_update_entry(
251 openstation_plugins_window_row_plugin_file( $row )
252 );
253 if ( null === $entry || empty( $entry['slug'] ) ) {
254 return null;
255 }
256 $slug = sanitize_key( (string) $entry['slug'] );
257 return '' !== $slug ? $slug : null;
258 }
259
260 /**
261 * `openstation_auto_update` callback.
262 *
263 * Mirrors the per-row state Core derives in
264 * `WP_Plugins_List_Table::prepare_items()` for its "Automatic Updates"
265 * column:
266 *
267 * - `enabled` bool — the file is in the `auto_update_plugins`
268 * site option, or a filter forced it on.
269 * - `forced` bool|null — `true`/`false` when the `auto_update_plugin`
270 * filter pinned the state, `null` when the
271 * user is free to toggle.
272 * - `supported` bool — the `update_plugins` transient knows the
273 * plugin (`response` or `no_update`). Core
274 * hides the toggle otherwise — premium /
275 * private plugins never check in.
276 *
277 * The global `wp_is_auto_update_enabled_for_type( 'plugin' )` gate is
278 * on the window config instead — see
279 * `openstation_plugins_window_auto_updates_enabled()`.
280 *
281 * @param array $row Core REST plugin row.
282 * @return array{enabled:bool,forced:bool|null,supported:bool}
283 */
284 function openstation_plugins_window_field_auto_update( $row ) {
285 $plugin_file = openstation_plugins_window_row_plugin_file( $row );
286 if ( '' === $plugin_file ) {
287 return array(
288 'enabled' => false,
289 'forced' => null,
290 'supported' => false,
291 );
292 }
293
294 $auto_updates = (array) get_site_option( 'auto_update_plugins', array() );
295 $enabled = in_array( $plugin_file, $auto_updates, true );
296 $supported = null !== openstation_plugins_window_update_entry( $plugin_file );
297
298 // The payload Core's filter expects (its `$filter_payload`).
299 // `wp_is_auto_update_forced_for_item()` is admin-only, so the filter
300 // runs directly — it is a single `apply_filters()` underneath.
301 // `wp_parse_args( $row, $defaults )` lets `$row` win, and Core's REST
302 // row spells `plugin` without `.php` while every `auto_update_plugin`
303 // callback reads `$item->plugin` as the FULL filename — the
304 // normalised file is layered on last so it always wins.
305 $filter_payload = wp_parse_args(
306 $row,
307 array(
308 'id' => $plugin_file,
309 'slug' => isset( $row['textdomain'] ) ? (string) $row['textdomain'] : '',
310 'plugin' => $plugin_file,
311 'new_version' => '',
312 'url' => '',
313 'package' => '',
314 'icons' => array(),
315 'banners' => array(),
316 'banners_rtl' => array(),
317 'tested' => '',
318 'requires_php' => '',
319 'compatibility' => new stdClass(),
320 )
321 );
322 $filter_payload['plugin'] = $plugin_file;
323 $filter_payload['id'] = $plugin_file;
324 $filter_payload = (object) $filter_payload;
325 /** This filter is documented in wp-admin/includes/class-wp-automatic-updater.php */
326 $forced = apply_filters( 'auto_update_plugin', null, $filter_payload ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Core's filter; the effective auto-update state has to come from the same source Core reads.
327 if ( null !== $forced ) {
328 $forced = (bool) $forced;
329 // A forced state is the effective state regardless of the
330 // option — matches Core's `single_row_columns()`.
331 $enabled = $forced;
332 }
333
334 return array(
335 'enabled' => (bool) $enabled,
336 'forced' => $forced,
337 'supported' => $supported,
338 );
339 }
340