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 / featured.php

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

228 lines 7.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugins app — the "OpenStation plugins" (Featured) tab's source.
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. A curated slug list (wp.org has no usable
8 * `requires_plugins` filter, so the seed is maintained by hand and
9 * filterable) topped up at runtime by scanning the popular feed for
10 * rows that declare the `desktop-mode` dependency, served over
11 * admin-ajax (`plugins_api()` is admin-only) and cached for an hour.
12 *
13 * @package OpenStation
14 */
15
16 // Direct access, unless a standalone host is booting on bare PHP.
17 if ( ! defined( 'ABSPATH' ) ) {
18 defined( 'OPENSTATION_STANDALONE' ) || exit;
19 }
20
21 /**
22 * Curated list of slugs that lead the Featured tab.
23 *
24 * Hand-picked because wp.org's `plugins_api` does not surface a real
25 * "filter by `requires_plugins`" query — passing `requires_plugins` to
26 * `query_plugins` is silently ignored and returns the unfiltered repo.
27 * Until the directory grows a usable filter, we maintain the seed list
28 * here and let downstream plugins amend it via the filter below.
29 *
30 * Slug-only — the AJAX handler hydrates each entry through
31 * `plugins_api( 'plugin_information' )` so the card has up-to-date
32 * icons, descriptions, and install counts without us caching them.
33 *
34 * @return string[] List of wp.org plugin slugs.
35 */
36 function openstation_plugins_window_featured_slugs() {
37 $slugs = array(
38 // The author of this plugin forgot to declare OpenStation as a
39 // dependency — surfacing it here makes sure openstation users
40 // discover it anyway. Once the `requires_plugins` query lands on
41 // wp.org we can remove the manual seed.
42 'odd-outlandish-desktop-decorator',
43 'allterrain-forms',
44 'allterrain-photo-editor',
45 'allterrain-maia',
46 );
47
48 /**
49 * Filter the curated list of featured-plugin slugs.
50 *
51 * Plugin authors can prepend (or remove) entries to recommend their
52 * own Desktop-Mode-aware add-ons. Order is preserved — the first
53 * slug renders first in the gallery.
54 *
55 * @param string[] $slugs Plugin slugs.
56 */
57 $slugs = (array) apply_filters( 'openstation_plugins_featured_slugs', $slugs );
58 $slugs = array_values(
59 array_unique(
60 array_filter(
61 array_map(
62 static function ( $s ) {
63 return sanitize_key( (string) $s );
64 },
65 $slugs
66 )
67 )
68 )
69 );
70 return $slugs;
71 }
72
73 /**
74 * `wp_ajax_openstation_plugins_featured` — return the Featured tab's
75 * curated + auto-discovered list of plugins that integrate with Desktop
76 * Mode.
77 *
78 * Composition:
79 * 1. Curated slugs from `openstation_plugins_window_featured_slugs()`,
80 * hydrated via `plugins_api( 'plugin_information' )` so the card
81 * payload is always fresh.
82 * 2. Auto-discovered slugs from `plugins_api( 'query_plugins' )` whose
83 * `requires_plugins` array contains `openstation`. wp.org has no
84 * server-side filter for this today, so we run a broad query and
85 * filter server-side. Deduped against the curated set.
86 *
87 * Body params: (none)
88 *
89 * Cached for 1h. Failures cached for 15m so a flaky wp.org doesn't
90 * hammer the API on every tab open.
91 */
92 function openstation_plugins_window_ajax_featured() {
93 $guard = openstation_plugins_window_ajax_guard( 'install_plugins' );
94 if ( is_wp_error( $guard ) ) {
95 openstation_plugins_window_ajax_error( $guard );
96 return;
97 }
98
99 openstation_plugins_window_load_plugins_api();
100
101 $cache_key = 'dm_pwfeatured_v1';
102 $cached = get_transient( $cache_key );
103 if ( false !== $cached && is_array( $cached ) ) {
104 wp_send_json_success( $cached );
105 return;
106 }
107
108 $plugins = array();
109 $seen_slugs = array();
110 $fields = array(
111 'icons' => true,
112 'banners' => false,
113 'short_description' => true,
114 'description' => false,
115 'sections' => false,
116 'screenshots' => false,
117 'rating' => true,
118 'ratings' => false,
119 'num_ratings' => true,
120 'active_installs' => true,
121 'last_updated' => true,
122 'tested' => true,
123 'requires' => true,
124 'requires_php' => true,
125 'requires_plugins' => true,
126 'homepage' => true,
127 'compatibility' => false,
128 'group' => false,
129 'contributors' => false,
130 'donate_link' => false,
131 );
132
133 // ─── 1. Curated slugs ─────────────────────────────────────────────
134 $curated = openstation_plugins_window_featured_slugs();
135 foreach ( $curated as $slug ) {
136 if ( isset( $seen_slugs[ $slug ] ) ) {
137 continue;
138 }
139 $info = plugins_api(
140 'plugin_information',
141 array(
142 'slug' => $slug,
143 'fields' => $fields,
144 )
145 );
146 if ( is_wp_error( $info ) || ! is_object( $info ) ) {
147 // Skip — a curated slug that 404s shouldn't tank the whole
148 // tab.
149 continue;
150 }
151 $row = (array) $info;
152 $row['featured'] = true;
153 $plugins[] = $row;
154 $seen_slugs[ $slug ] = true;
155 }
156
157 // ─── 2. Auto-discover via `requires_plugins` ──────────────────────
158 // Best-effort scan: pull the top of the directory and keep rows that
159 // declare openstation as a dependency. The wp.org `query_plugins`
160 // API ignores `requires_plugins` as a filter, so we have to fetch +
161 // sift locally. Scope is intentionally small (100 most-popular rows)
162 // to keep the request bounded; as the ecosystem grows we'll widen
163 // or replace with a real dependency query when wp.org ships one.
164 $discovered = plugins_api(
165 'query_plugins',
166 array(
167 'browse' => 'popular',
168 'page' => 1,
169 'per_page' => 100,
170 'fields' => $fields,
171 )
172 );
173 if ( ! is_wp_error( $discovered ) && isset( $discovered->plugins ) && is_array( $discovered->plugins ) ) {
174 foreach ( $discovered->plugins as $candidate ) {
175 $candidate = (array) $candidate;
176 $slug = isset( $candidate['slug'] ) ? sanitize_key( (string) $candidate['slug'] ) : '';
177 if ( '' === $slug || isset( $seen_slugs[ $slug ] ) ) {
178 continue;
179 }
180 $requires = isset( $candidate['requires_plugins'] ) ? (array) $candidate['requires_plugins'] : array();
181 // The wp.org directory slug, not the brand — `requires_plugins`
182 // rows resolve against our plugin folder name.
183 if ( ! in_array( 'desktop-mode', $requires, true ) ) {
184 continue;
185 }
186 $candidate['featured'] = false;
187 $plugins[] = $candidate;
188 $seen_slugs[ $slug ] = true;
189 }
190 }
191
192 // `count( $plugins ) - count( $curated )` can underflow when a
193 // curated slug fails hydration (slug typo, plugin temporarily
194 // delisted from wp.org, plugins_api returning WP_Error). The JS
195 // only uses `discovered` for informational headers, so a negative
196 // number wouldn't crash anything, but it does read as a bug.
197 // Clamp at zero so the count remains a defensible "non-curated rows
198 // in the payload."
199 $payload = array(
200 'plugins' => array_values( $plugins ),
201 'info' => array(
202 'curated' => count( $curated ),
203 'discovered' => max( 0, count( $plugins ) - count( $curated ) ),
204 'results' => count( $plugins ),
205 ),
206 );
207
208 /**
209 * Filter the Featured tab payload before it's cached + sent.
210 *
211 * Use this to inject server-side curated rows (e.g. premium /
212 * private plugins not on wp.org), or to enforce a hard cap on the
213 * response.
214 *
215 * @param array $payload `{ plugins: [...], info: {...} }`.
216 * @param array $curated Curated slug list.
217 */
218 $payload = (array) apply_filters(
219 'openstation_plugins_featured_response',
220 $payload,
221 $curated
222 );
223
224 set_transient( $cache_key, $payload, HOUR_IN_SECONDS );
225 wp_send_json_success( $payload );
226 }
227 add_action( 'wp_ajax_openstation_plugins_featured', 'openstation_plugins_window_ajax_featured' );
228