PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.8
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 0.8.6 All 33 releases
desktop-mode / includes / media-query.php

media-query.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.8, at includes/media-query.php

249 lines 9.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — Media dimension filtering for REST.
4 *
5 * Adds two opt-in query parameters to `/wp/v2/media`:
6 *
7 * - `openstation_min_width` — only return images at least this many pixels wide.
8 * - `openstation_min_height` — only return images at least this many pixels tall.
9 *
10 * The OS Settings wallpaper picker uses these to keep a site with thousands
11 * of small product images from burying the handful of desktop-worthy HD
12 * shots under an infinite-scroll slog. The client still applies the same
13 * filter locally as a belt-and-suspenders safeguard — if the server-side
14 * filter ever ships mis-stamped meta, the UI still hides too-small images.
15 *
16 * Why not `meta_query` on `_wp_attachment_metadata` directly? That key is
17 * stored serialized, so SQL can't compare its `width`/`height` members
18 * reliably. We stamp two flat numeric meta keys on every new attachment
19 * (and opportunistically backfill existing ones) so `WP_Meta_Query` can
20 * use a normal NUMERIC `>=` comparison.
21 *
22 * @package OpenStation
23 */
24
25 defined( 'ABSPATH' ) || exit;
26
27 /**
28 * Numeric post-meta keys stamped on every image attachment.
29 *
30 * The VALUE keeps its pre-rebrand spelling on purpose: it is a
31 * persisted or externally-visible identifier, so renaming it would
32 * orphan data already written by live installs (or break a live
33 * URL). The mismatch between this constant's name and its value is
34 * deliberate — it is NOT a half-finished rename.
35 */
36 const OPENSTATION_META_WIDTH = '_desktop_mode_width';
37 /**
38 * The VALUE keeps its pre-rebrand spelling on purpose: it is a
39 * persisted or externally-visible identifier, so renaming it would
40 * orphan data already written by live installs (or break a live
41 * URL). The mismatch between this constant's name and its value is
42 * deliberate — it is NOT a half-finished rename.
43 */
44 const OPENSTATION_META_HEIGHT = '_desktop_mode_height';
45
46 /**
47 * Option key flipped to `1` once every image has been backfilled.
48 *
49 * The VALUE keeps its pre-rebrand spelling on purpose: it is a
50 * persisted or externally-visible identifier, so renaming it would
51 * orphan data already written by live installs (or break a live
52 * URL). The mismatch between this constant's name and its value is
53 * deliberate — it is NOT a half-finished rename.
54 */
55 const OPENSTATION_BACKFILL_DONE_OPTION = 'desktop_mode_media_dims_backfilled';
56
57 /** How many legacy attachments to backfill per filtered REST request. */
58 const OPENSTATION_MEDIA_BACKFILL_BATCH = 50;
59
60 /**
61 * Stamp numeric dimension meta whenever an attachment's metadata is
62 * generated or updated. Hooked on both `wp_generate_attachment_metadata`
63 * (new uploads) and `wp_update_attachment_metadata` (regenerated
64 * thumbnails, image editor saves) so the stamp stays in sync with
65 * whatever Core thinks the canonical dimensions are.
66 *
67 * @param array $metadata Attachment metadata.
68 * @param int $attachment_id Attachment post ID.
69 * @return array The metadata, unchanged — we only read from it.
70 */
71 function openstation_stamp_media_dimensions( $metadata, $attachment_id ) {
72 if ( ! is_array( $metadata ) ) {
73 return $metadata;
74 }
75
76 $width = isset( $metadata['width'] ) ? (int) $metadata['width'] : 0;
77 $height = isset( $metadata['height'] ) ? (int) $metadata['height'] : 0;
78
79 // Stamp zero for images we can't measure (SVGs, broken files) so the
80 // backfill sweep knows we've already inspected this row and doesn't
81 // re-check it on every subsequent filtered request.
82 update_post_meta( $attachment_id, OPENSTATION_META_WIDTH, max( 0, $width ) );
83 update_post_meta( $attachment_id, OPENSTATION_META_HEIGHT, max( 0, $height ) );
84
85 return $metadata;
86 }
87 add_filter( 'wp_generate_attachment_metadata', 'openstation_stamp_media_dimensions', 10, 2 );
88 add_filter( 'wp_update_attachment_metadata', 'openstation_stamp_media_dimensions', 10, 2 );
89
90 /**
91 * Register the `openstation_min_width` / `openstation_min_height` query parameters on
92 * the media collection so Core sanitizes them before they reach our
93 * query filter. Without this, the params would still work but would
94 * show up as unknown to any REST consumer introspecting the schema.
95 *
96 * @param array $params Existing collection params.
97 * @return array
98 */
99 function openstation_register_media_query_params( $params ) {
100 $params['openstation_min_width'] = array(
101 'description' => __( 'Only return images at least this many pixels wide.', 'desktop-mode' ),
102 'type' => 'integer',
103 'minimum' => 1,
104 );
105 $params['openstation_min_height'] = array(
106 'description' => __( 'Only return images at least this many pixels tall.', 'desktop-mode' ),
107 'type' => 'integer',
108 'minimum' => 1,
109 );
110 return $params;
111 }
112 add_filter( 'rest_attachment_collection_params', 'openstation_register_media_query_params' );
113
114 /**
115 * Inject meta_query clauses on the attachment REST query when either
116 * dimension filter is present. Triggers a bounded backfill first so
117 * legacy attachments (uploaded before this filter existed) start
118 * participating without a one-shot CLI command.
119 *
120 * @param array $args WP_Query args built by the REST controller.
121 * @param WP_REST_Request $request The REST request.
122 * @return array The query args, possibly with extra meta_query entries.
123 */
124 function openstation_filter_media_by_dimensions( $args, $request ) {
125 $min_width = absint( $request->get_param( 'openstation_min_width' ) );
126 $min_height = absint( $request->get_param( 'openstation_min_height' ) );
127
128 if ( ! $min_width && ! $min_height ) {
129 return $args;
130 }
131
132 // Chip away at any remaining unstamped attachments before the query
133 // runs, so a site upgrading into this feature gets useful results on
134 // roughly the first few picker opens rather than after a CLI run.
135 // Logged-in users only — anonymous REST readers can still use the
136 // dimension filters, but must never trigger database writes (or the
137 // NOT EXISTS sweep query) on a public, unauthenticated endpoint.
138 if ( is_user_logged_in() ) {
139 openstation_backfill_media_dimensions( OPENSTATION_MEDIA_BACKFILL_BATCH );
140 }
141
142 $meta_query = isset( $args['meta_query'] ) && is_array( $args['meta_query'] )
143 ? $args['meta_query']
144 : array();
145
146 $clauses = array();
147 if ( $min_width ) {
148 $clauses[] = array(
149 'key' => OPENSTATION_META_WIDTH,
150 'value' => $min_width,
151 'compare' => '>=',
152 'type' => 'NUMERIC',
153 );
154 }
155 if ( $min_height ) {
156 $clauses[] = array(
157 'key' => OPENSTATION_META_HEIGHT,
158 'value' => $min_height,
159 'compare' => '>=',
160 'type' => 'NUMERIC',
161 );
162 }
163
164 if ( count( $clauses ) > 1 ) {
165 $clauses['relation'] = 'AND';
166 }
167
168 // Compose with any existing meta_query so we don't stomp other
169 // filters (Core's own, or anything plugins have layered on).
170 if ( empty( $meta_query ) ) {
171 $args['meta_query'] = $clauses; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- NUMERIC >= dimension filter on indexed meta keys; canonical WP pattern for picker filtering.
172 } else {
173 $args['meta_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- composed AND clause merging caller meta_query with our dimension filter.
174 'relation' => 'AND',
175 $meta_query,
176 $clauses,
177 );
178 }
179
180 return $args;
181 }
182 add_filter( 'rest_attachment_query', 'openstation_filter_media_by_dimensions', 10, 2 );
183
184 /**
185 * Stamp dimension meta on up to $batch image attachments that don't
186 * have it yet. Returns early once a site has been fully backfilled so
187 * filtered REST requests don't keep paying for an empty sweep query.
188 *
189 * Ordering by `ID DESC` so newest attachments get stamped first — the
190 * user is most likely to be looking for something they uploaded
191 * recently, so we prioritize the tail they're staring at.
192 *
193 * @param int $batch Maximum attachments to stamp this pass.
194 * @return int Number of attachments stamped (0 when nothing remained).
195 */
196 function openstation_backfill_media_dimensions( $batch ) {
197 if ( get_option( OPENSTATION_BACKFILL_DONE_OPTION ) ) {
198 return 0;
199 }
200
201 $ids = get_posts(
202 array(
203 'post_type' => 'attachment',
204 'post_status' => 'inherit',
205 'post_mime_type' => 'image',
206 'posts_per_page' => (int) $batch,
207 'orderby' => 'ID',
208 'order' => 'DESC',
209 'fields' => 'ids',
210 'no_found_rows' => true,
211 'update_post_term_cache' => false,
212 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- one-shot backfill targeting the small subset of attachments that lack our dimension stamp; runs at most $batch rows per filtered request and stops once the site option flips.
213 'meta_query' => array(
214 array(
215 'key' => OPENSTATION_META_WIDTH,
216 'compare' => 'NOT EXISTS',
217 ),
218 ),
219 )
220 );
221
222 if ( empty( $ids ) ) {
223 // Flip the flag so future filtered requests skip this query.
224 // If a plugin ever adds new images via a back door that bypasses
225 // `wp_generate_attachment_metadata`, those can be picked up by
226 // manually deleting this option.
227 update_option( OPENSTATION_BACKFILL_DONE_OPTION, 1, false );
228 return 0;
229 }
230
231 foreach ( $ids as $id ) {
232 // `wp_get_attachment_metadata()` hits the object cache if it was
233 // primed by the enclosing query; otherwise it's a single meta
234 // lookup per id. We're bounded at $batch per request so the
235 // per-request overhead stays predictable.
236 $metadata = wp_get_attachment_metadata( $id );
237
238 $width = is_array( $metadata ) && isset( $metadata['width'] ) ? (int) $metadata['width'] : 0;
239 $height = is_array( $metadata ) && isset( $metadata['height'] ) ? (int) $metadata['height'] : 0;
240
241 // Always stamp, even when zero — that's how the sweep query
242 // knows to skip this row on the next pass.
243 update_post_meta( $id, OPENSTATION_META_WIDTH, max( 0, $width ) );
244 update_post_meta( $id, OPENSTATION_META_HEIGHT, max( 0, $height ) );
245 }
246
247 return count( $ids );
248 }
249