PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.3
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.3
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 / media-query.php

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

235 lines 8.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode — Media dimension filtering for REST.
4 *
5 * Adds two opt-in query parameters to `/wp/v2/media`:
6 *
7 * - `desktop_mode_min_width` — only return images at least this many pixels wide.
8 * - `desktop_mode_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 WPDesktopMode
23 * @since 0.5.0
24 */
25
26 defined( 'ABSPATH' ) || exit;
27
28 /** Numeric post-meta keys stamped on every image attachment. */
29 const DESKTOP_MODE_META_WIDTH = '_desktop_mode_width';
30 const DESKTOP_MODE_META_HEIGHT = '_desktop_mode_height';
31
32 /** Option key flipped to `1` once every image has been backfilled. */
33 const DESKTOP_MODE_BACKFILL_DONE_OPTION = 'desktop_mode_media_dims_backfilled';
34
35 /** How many legacy attachments to backfill per filtered REST request. */
36 const DESKTOP_MODE_MEDIA_BACKFILL_BATCH = 50;
37
38 /**
39 * Stamp numeric dimension meta whenever an attachment's metadata is
40 * generated or updated. Hooked on both `wp_generate_attachment_metadata`
41 * (new uploads) and `wp_update_attachment_metadata` (regenerated
42 * thumbnails, image editor saves) so the stamp stays in sync with
43 * whatever Core thinks the canonical dimensions are.
44 *
45 * @since 0.5.0
46 *
47 * @param array $metadata Attachment metadata.
48 * @param int $attachment_id Attachment post ID.
49 * @return array The metadata, unchanged — we only read from it.
50 */
51 function desktop_mode_stamp_media_dimensions( $metadata, $attachment_id ) {
52 if ( ! is_array( $metadata ) ) {
53 return $metadata;
54 }
55
56 $width = isset( $metadata['width'] ) ? (int) $metadata['width'] : 0;
57 $height = isset( $metadata['height'] ) ? (int) $metadata['height'] : 0;
58
59 // Stamp zero for images we can't measure (SVGs, broken files) so the
60 // backfill sweep knows we've already inspected this row and doesn't
61 // re-check it on every subsequent filtered request.
62 update_post_meta( $attachment_id, DESKTOP_MODE_META_WIDTH, max( 0, $width ) );
63 update_post_meta( $attachment_id, DESKTOP_MODE_META_HEIGHT, max( 0, $height ) );
64
65 return $metadata;
66 }
67 add_filter( 'wp_generate_attachment_metadata', 'desktop_mode_stamp_media_dimensions', 10, 2 );
68 add_filter( 'wp_update_attachment_metadata', 'desktop_mode_stamp_media_dimensions', 10, 2 );
69
70 /**
71 * Register the `desktop_mode_min_width` / `desktop_mode_min_height` query parameters on
72 * the media collection so Core sanitizes them before they reach our
73 * query filter. Without this, the params would still work but would
74 * show up as unknown to any REST consumer introspecting the schema.
75 *
76 * @since 0.5.0
77 *
78 * @param array $params Existing collection params.
79 * @return array
80 */
81 function desktop_mode_register_media_query_params( $params ) {
82 $params['desktop_mode_min_width'] = array(
83 'description' => __( 'Only return images at least this many pixels wide.', 'desktop-mode' ),
84 'type' => 'integer',
85 'minimum' => 1,
86 );
87 $params['desktop_mode_min_height'] = array(
88 'description' => __( 'Only return images at least this many pixels tall.', 'desktop-mode' ),
89 'type' => 'integer',
90 'minimum' => 1,
91 );
92 return $params;
93 }
94 add_filter( 'rest_attachment_collection_params', 'desktop_mode_register_media_query_params' );
95
96 /**
97 * Inject meta_query clauses on the attachment REST query when either
98 * dimension filter is present. Triggers a bounded backfill first so
99 * legacy attachments (uploaded before this filter existed) start
100 * participating without a one-shot CLI command.
101 *
102 * @since 0.5.0
103 *
104 * @param array $args WP_Query args built by the REST controller.
105 * @param WP_REST_Request $request The REST request.
106 * @return array The query args, possibly with extra meta_query entries.
107 */
108 function desktop_mode_filter_media_by_dimensions( $args, $request ) {
109 $min_width = absint( $request->get_param( 'desktop_mode_min_width' ) );
110 $min_height = absint( $request->get_param( 'desktop_mode_min_height' ) );
111
112 if ( ! $min_width && ! $min_height ) {
113 return $args;
114 }
115
116 // Chip away at any remaining unstamped attachments before the query
117 // runs, so a site upgrading into this feature gets useful results on
118 // roughly the first few picker opens rather than after a CLI run.
119 // Logged-in users only — anonymous REST readers can still use the
120 // dimension filters, but must never trigger database writes (or the
121 // NOT EXISTS sweep query) on a public, unauthenticated endpoint.
122 if ( is_user_logged_in() ) {
123 desktop_mode_backfill_media_dimensions( DESKTOP_MODE_MEDIA_BACKFILL_BATCH );
124 }
125
126 $meta_query = isset( $args['meta_query'] ) && is_array( $args['meta_query'] )
127 ? $args['meta_query']
128 : array();
129
130 $clauses = array();
131 if ( $min_width ) {
132 $clauses[] = array(
133 'key' => DESKTOP_MODE_META_WIDTH,
134 'value' => $min_width,
135 'compare' => '>=',
136 'type' => 'NUMERIC',
137 );
138 }
139 if ( $min_height ) {
140 $clauses[] = array(
141 'key' => DESKTOP_MODE_META_HEIGHT,
142 'value' => $min_height,
143 'compare' => '>=',
144 'type' => 'NUMERIC',
145 );
146 }
147
148 if ( count( $clauses ) > 1 ) {
149 $clauses['relation'] = 'AND';
150 }
151
152 // Compose with any existing meta_query so we don't stomp other
153 // filters (Core's own, or anything plugins have layered on).
154 if ( empty( $meta_query ) ) {
155 $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.
156 } else {
157 $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.
158 'relation' => 'AND',
159 $meta_query,
160 $clauses,
161 );
162 }
163
164 return $args;
165 }
166 add_filter( 'rest_attachment_query', 'desktop_mode_filter_media_by_dimensions', 10, 2 );
167
168 /**
169 * Stamp dimension meta on up to $batch image attachments that don't
170 * have it yet. Returns early once a site has been fully backfilled so
171 * filtered REST requests don't keep paying for an empty sweep query.
172 *
173 * Ordering by `ID DESC` so newest attachments get stamped first — the
174 * user is most likely to be looking for something they uploaded
175 * recently, so we prioritize the tail they're staring at.
176 *
177 * @since 0.5.0
178 *
179 * @param int $batch Maximum attachments to stamp this pass.
180 * @return int Number of attachments stamped (0 when nothing remained).
181 */
182 function desktop_mode_backfill_media_dimensions( $batch ) {
183 if ( get_option( DESKTOP_MODE_BACKFILL_DONE_OPTION ) ) {
184 return 0;
185 }
186
187 $ids = get_posts(
188 array(
189 'post_type' => 'attachment',
190 'post_status' => 'inherit',
191 'post_mime_type' => 'image',
192 'posts_per_page' => (int) $batch,
193 'orderby' => 'ID',
194 'order' => 'DESC',
195 'fields' => 'ids',
196 'no_found_rows' => true,
197 'update_post_term_cache' => false,
198 // 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.
199 'meta_query' => array(
200 array(
201 'key' => DESKTOP_MODE_META_WIDTH,
202 'compare' => 'NOT EXISTS',
203 ),
204 ),
205 )
206 );
207
208 if ( empty( $ids ) ) {
209 // Flip the flag so future filtered requests skip this query.
210 // If a plugin ever adds new images via a back door that bypasses
211 // `wp_generate_attachment_metadata`, those can be picked up by
212 // manually deleting this option.
213 update_option( DESKTOP_MODE_BACKFILL_DONE_OPTION, 1, false );
214 return 0;
215 }
216
217 foreach ( $ids as $id ) {
218 // `wp_get_attachment_metadata()` hits the object cache if it was
219 // primed by the enclosing query; otherwise it's a single meta
220 // lookup per id. We're bounded at $batch per request so the
221 // per-request overhead stays predictable.
222 $metadata = wp_get_attachment_metadata( $id );
223
224 $width = is_array( $metadata ) && isset( $metadata['width'] ) ? (int) $metadata['width'] : 0;
225 $height = is_array( $metadata ) && isset( $metadata['height'] ) ? (int) $metadata['height'] : 0;
226
227 // Always stamp, even when zero — that's how the sweep query
228 // knows to skip this row on the next pass.
229 update_post_meta( $id, DESKTOP_MODE_META_WIDTH, max( 0, $width ) );
230 update_post_meta( $id, DESKTOP_MODE_META_HEIGHT, max( 0, $height ) );
231 }
232
233 return count( $ids );
234 }
235