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
← All changes | includes/my-wordpress/media-usage.php +121 -115 0.9.21.1.10 View file →
@@ -1,7 +1,7 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — My WordPress: per-attachment "used in" endpoint.
3 + * OpenStation — My WordPress: per-attachment "used in" endpoint.
4 4 *
5 5 * `GET /desktop-mode/v1/media-usage/<id>` returns the list of public
6 6 * post-type entries that reference a given attachment, either as
7 7 * featured image (`_thumbnail_id` meta) or as an embed inside
@@ -21,14 +21,15 @@
21 21 *
22 22 * Rows are filtered per-row with `current_user_can( 'read_post' )`,
23 23 * so subscribers never see drafts/private posts they can't read.
24 24 *
25 - * Results are cached in a transient keyed on the attachment id +
26 - * the viewer's effective capability scope. Cache is busted whenever
27 - * any post is saved or deleted.
25 + * Only the viewer-independent reference scan (post id => usedAs map)
26 + * is cached in a transient keyed on the attachment id; the per-row
27 + * `read_post` gate runs on every request, so a cached scan can never
28 + * leak rows across viewers with different capabilities. Cache is
29 + * busted whenever any post is saved or deleted.
28 30 *
29 - * @package WPDesktopMode
30 - * @since 0.21.0
31 + * @package OpenStation
31 32 */
32 33
33 34 defined( 'ABSPATH' ) || exit;
34 35
@@ -33,18 +34,16 @@
33 34 defined( 'ABSPATH' ) || exit;
34 35
35 36 /**
36 37 * Register the route.
37 - *
38 - * @since 0.21.0
39 38 */
40 -function desktop_mode_my_wordpress_register_media_usage_route() {
39 +function openstation_my_wordpress_register_media_usage_route() {
41 40 register_rest_route(
42 41 'desktop-mode/v1',
43 42 '/media-usage/(?P<id>\d+)',
44 43 array(
45 44 'methods' => WP_REST_Server::READABLE,
46 - 'callback' => 'desktop_mode_my_wordpress_media_usage_callback',
45 + 'callback' => 'openstation_my_wordpress_media_usage_callback',
47 46 'permission_callback' => static function ( $request ) {
48 47 $id = (int) $request->get_param( 'id' );
49 48 if ( $id <= 0 ) {
50 49 return false;
@@ -64,9 +63,9 @@
64 63 ),
65 64 )
66 65 );
67 66 }
68 -add_action( 'rest_api_init', 'desktop_mode_my_wordpress_register_media_usage_route' );
67 +add_action( 'rest_api_init', 'openstation_my_wordpress_register_media_usage_route' );
69 68
70 69 /**
71 70 * Cache TTL (seconds). Filterable so sites that bulk-import media
72 71 * can shorten the window, or sites with stable libraries can
@@ -71,23 +70,19 @@
71 70 * Cache TTL (seconds). Filterable so sites that bulk-import media
72 71 * can shorten the window, or sites with stable libraries can
73 72 * lengthen it.
74 73 *
75 - * @since 0.21.0
76 - *
77 74 * @param int $attachment_id Attachment id.
78 75 * @return int
79 76 */
80 -function desktop_mode_my_wordpress_media_usage_ttl( $attachment_id ) {
77 +function openstation_my_wordpress_media_usage_ttl( $attachment_id ) {
81 78 /**
82 79 * Filter the media-usage transient TTL.
83 80 *
84 - * @since 0.21.0
85 - *
86 81 * @param int $seconds Default 300 (5 minutes).
87 82 * @param int $attachment_id Attachment id the cache key is for.
88 83 */
89 - return (int) apply_filters( 'desktop_mode_my_wordpress_media_usage_cache_ttl', 300, $attachment_id );
84 + return (int) apply_filters( 'openstation_my_wordpress_media_usage_cache_ttl', 300, $attachment_id );
90 85 }
91 86
92 87 /**
93 88 * Capability buckets the cache namespaces over. A second-tier
@@ -94,13 +89,11 @@
94 89 * change to the gating logic must update this list — the busters
95 90 * iterate over the same array, so the writer and the buster can
96 91 * never go out of sync.
97 92 *
98 - * @since 0.21.0
99 - *
100 93 * @return string[]
101 94 */
102 -function desktop_mode_my_wordpress_media_usage_cache_buckets() {
95 +function openstation_my_wordpress_media_usage_cache_buckets() {
103 96 return array( 'edit', 'read' );
104 97 }
105 98
106 99 /**
@@ -106,31 +99,29 @@
106 99 /**
107 100 * Bucket key for the current user — the writer's view of which
108 101 * cache slot to read/write.
109 102 *
110 - * @since 0.21.0
111 - *
112 103 * @return string
113 104 */
114 -function desktop_mode_my_wordpress_media_usage_current_bucket() {
105 +function openstation_my_wordpress_media_usage_current_bucket() {
115 106 return current_user_can( 'edit_others_posts' ) ? 'edit' : 'read';
116 107 }
117 108
118 109 /**
119 110 * Build the transient key — namespaces the cache by attachment id
120 - * AND a coarse capability bucket so admins and viewers never share
121 - * a hit.
111 + * AND a coarse capability bucket. The cached value is the
112 + * viewer-independent reference map (per-row capability gating runs
113 + * after the cache read), so the bucket is key hygiene rather than a
114 + * security boundary.
122 115 *
123 - * @since 0.21.0
124 - *
125 116 * @param int $attachment_id Attachment id.
126 117 * @param string $bucket Optional bucket override. Defaults to
127 118 * the current user's bucket.
128 119 * @return string
129 120 */
130 -function desktop_mode_my_wordpress_media_usage_cache_key( $attachment_id, $bucket = null ) {
121 +function openstation_my_wordpress_media_usage_cache_key( $attachment_id, $bucket = null ) {
131 122 if ( null === $bucket ) {
132 - $bucket = desktop_mode_my_wordpress_media_usage_current_bucket();
123 + $bucket = openstation_my_wordpress_media_usage_current_bucket();
133 124 }
134 125 return 'dm_media_usage_' . (int) $attachment_id . '_' . $bucket . '_v1';
135 126 }
136 127
@@ -136,71 +127,70 @@
136 127
137 128 /**
138 129 * Endpoint callback. See file docblock for payload shape.
139 130 *
140 - * @since 0.21.0
141 - *
142 131 * @param WP_REST_Request $request REST request.
143 132 * @return array|WP_Error
144 133 */
145 -function desktop_mode_my_wordpress_media_usage_callback( $request ) {
134 +function openstation_my_wordpress_media_usage_callback( $request ) {
146 135 $attachment_id = (int) $request->get_param( 'id' );
147 136 $attachment = get_post( $attachment_id );
148 137 if ( ! $attachment || 'attachment' !== $attachment->post_type ) {
149 138 return new WP_Error(
150 - 'desktop_mode_media_not_found',
139 + 'openstation_media_not_found',
151 140 __( 'Attachment not found.', 'desktop-mode' ),
152 141 array( 'status' => 404 )
153 142 );
154 143 }
155 144
156 - $cache_key = desktop_mode_my_wordpress_media_usage_cache_key( $attachment_id );
157 - $cached = get_transient( $cache_key );
158 - if ( is_array( $cached ) ) {
159 - /*
160 - * Cache stores the PRE-filter payload. We re-run the filter
161 - * on every hit so plugin extensions (ACF image meta, page-
162 - * builder galleries, etc.) stay live even while the heavy
163 - * SQL portion of the payload is cached. Plugin output is
164 - * cheaper to recompute than the LIKE-scan; the base payload
165 - * only refreshes on the cache-bust events (save_post,
166 - * deleted_post, delete_attachment).
167 - */
168 - /** This filter is documented in includes/my-wordpress/media-usage.php */
169 - return apply_filters( 'desktop_mode_my_wordpress_media_usage', $cached, $attachment_id );
145 + $cache_key = openstation_my_wordpress_media_usage_cache_key( $attachment_id );
146 + $rows_by_post = get_transient( $cache_key );
147 + if ( ! is_array( $rows_by_post ) ) {
148 + $rows_by_post = openstation_my_wordpress_media_usage_collect( $attachment );
149 + set_transient(
150 + $cache_key,
151 + $rows_by_post,
152 + openstation_my_wordpress_media_usage_ttl( $attachment_id )
153 + );
170 154 }
171 155
172 - $payload = desktop_mode_my_wordpress_media_usage_build( $attachment );
156 + /*
157 + * The transient stores ONLY the viewer-independent reference map
158 + * (post id => usedAs). Both the per-row `read_post` gate and the
159 + * extension filter run on every request: the gate so a cache hit
160 + * written during one viewer's request can never leak unreadable
161 + * rows to another viewer, the filter so plugin extensions (ACF
162 + * image meta, page-builder galleries, etc.) stay live while the
163 + * heavy LIKE-scan portion stays cached. The base map only
164 + * refreshes on the cache-bust events (save_post,
165 + * before_delete_post, delete_attachment).
166 + */
167 + $payload = openstation_my_wordpress_media_usage_build( $attachment, $rows_by_post );
173 168
174 - set_transient(
175 - $cache_key,
176 - $payload,
177 - desktop_mode_my_wordpress_media_usage_ttl( $attachment_id )
178 - );
179 -
180 169 /**
181 170 * Filter the media-usage payload before returning to the bundle.
182 171 * Plugins (ACF, page builders, Yoast image meta) can append rows
183 172 * to `usedIn` describing their own attachment references.
184 173 *
185 - * @since 0.21.0
186 - *
187 174 * @param array $payload Default payload.
188 175 * @param int $attachment_id Subject attachment id.
189 176 */
190 - return apply_filters( 'desktop_mode_my_wordpress_media_usage', $payload, $attachment_id );
177 + return apply_filters( 'openstation_my_wordpress_media_usage', $payload, $attachment_id );
191 178 }
192 179
193 180 /**
194 - * Build the un-filtered payload. Separated from the callback so the
195 - * cache bypass can short-circuit before any DB work.
181 + * Collect the viewer-independent reference map for an attachment:
182 + * post id => 'featured'|'content'. This is the heavy SQL portion of
183 + * the payload and the ONLY part that gets transient-cached — the
184 + * per-row `read_post` gate lives in
185 + * `openstation_my_wordpress_media_usage_build()` and runs on every
186 + * request, so a cached map can never leak rows across viewers with
187 + * different capabilities.
196 188 *
197 - * @since 0.21.0
198 - *
199 189 * @param WP_Post $attachment Attachment post.
200 - * @return array
190 + * @return array<int,string> Map of post id => usedAs kind.
201 191 */
202 -function desktop_mode_my_wordpress_media_usage_build( $attachment ) {
192 +function openstation_my_wordpress_media_usage_collect( $attachment ) {
203 193 global $wpdb;
204 194
205 195 $attachment_id = (int) $attachment->ID;
206 196 $file_url = (string) wp_get_attachment_url( $attachment_id );
@@ -205,31 +195,14 @@
205 195 $attachment_id = (int) $attachment->ID;
206 196 $file_url = (string) wp_get_attachment_url( $attachment_id );
207 197 $file_basename = '' !== $file_url ? wp_basename( $file_url ) : '';
208 198
209 - $author = get_userdata( (int) $attachment->post_author );
210 - $media_info = array(
211 - 'id' => $attachment_id,
212 - 'title' => (string) get_the_title( $attachment_id ),
213 - 'mime' => (string) $attachment->post_mime_type,
214 - 'sourceUrl' => $file_url,
215 - 'filename' => $file_basename,
216 - 'date' => mysql2date( 'c', $attachment->post_date_gmt, false ),
217 - 'author' => array(
218 - 'id' => (int) $attachment->post_author,
219 - 'name' => $author ? (string) $author->display_name : '',
220 - ),
221 - );
222 -
223 199 $public_types = array_values( get_post_types( array( 'public' => true ), 'names' ) );
224 200 // Filter out `attachment` from the search — attachments don't
225 201 // reference other attachments in a meaningful way for this view.
226 202 $public_types = array_values( array_diff( $public_types, array( 'attachment' ) ) );
227 203 if ( empty( $public_types ) ) {
228 - return array(
229 - 'media' => $media_info,
230 - 'usedIn' => array(),
231 - );
204 + return array();
232 205 }
233 206
234 207 // `usedAs` priority: featured > content > meta. We collect every
235 208 // hit per post id, then collapse to the highest-priority kind for
@@ -263,10 +236,10 @@
263 236 $basename_variants[] = $m[1] . $m[2];
264 237 }
265 238 $basename_variants = array_values( array_unique( $basename_variants ) );
266 239
267 - $class_pattern = '%wp-image-' . $attachment_id . '%';
268 - $url_patterns = array();
240 + $class_pattern = '%wp-image-' . $attachment_id . '%';
241 + $url_patterns = array();
269 242 foreach ( $basename_variants as $variant ) {
270 243 $url_patterns[] = '%' . $wpdb->esc_like( $variant ) . '%';
271 244 }
272 245
@@ -330,8 +303,51 @@
330 303 $rows_by_post[ $pid ] = 'content';
331 304 }
332 305 }
333 306
307 + return $rows_by_post;
308 +}
309 +
310 +/**
311 + * Build the payload for the CURRENT viewer. The reference map can be
312 + * passed in (typically straight from the transient); when omitted
313 + * it's collected fresh. Row building applies
314 + * `current_user_can( 'read_post' )` per row on every call — never
315 + * cache this function's output, it is viewer-specific.
316 + *
317 + * @param WP_Post $attachment Attachment post.
318 + * @param array<int,string>|null $rows_by_post Optional precollected map of
319 + * post id => usedAs kind.
320 + * @return array
321 + */
322 +function openstation_my_wordpress_media_usage_build( $attachment, $rows_by_post = null ) {
323 + $attachment_id = (int) $attachment->ID;
324 + $file_url = (string) wp_get_attachment_url( $attachment_id );
325 + $file_basename = '' !== $file_url ? wp_basename( $file_url ) : '';
326 +
327 + $author = get_userdata( (int) $attachment->post_author );
328 + $media_info = array(
329 + 'id' => $attachment_id,
330 + 'title' => (string) get_the_title( $attachment_id ),
331 + 'mime' => (string) $attachment->post_mime_type,
332 + 'sourceUrl' => $file_url,
333 + 'filename' => $file_basename,
334 + 'date' => mysql2date( 'c', $attachment->post_date_gmt, false ),
335 + 'author' => array(
336 + 'id' => (int) $attachment->post_author,
337 + 'name' => $author ? (string) $author->display_name : '',
338 + ),
339 + );
340 +
341 + if ( ! is_array( $rows_by_post ) ) {
342 + $rows_by_post = openstation_my_wordpress_media_usage_collect( $attachment );
343 + }
344 +
345 + $public_types = array_values( get_post_types( array( 'public' => true ), 'names' ) );
346 + // Filter out `attachment` from the search — attachments don't
347 + // reference other attachments in a meaningful way for this view.
348 + $public_types = array_values( array_diff( $public_types, array( 'attachment' ) ) );
349 +
334 350 // --- Build the row payload, per-row capability gated ----------------
335 351 $type_objects = array();
336 352 foreach ( $public_types as $type ) {
337 353 $type_objects[ $type ] = get_post_type_object( $type );
@@ -389,13 +405,11 @@
389 405 * buster can union pre + post sets — otherwise removing a
390 406 * `wp-image-N` block from a post would leave the cache for
391 407 * attachment N stale until the TTL expires.
392 408 *
393 - * @since 0.21.0
394 - *
395 409 * @var array<int,array<int,true>>
396 410 */
397 -$GLOBALS['desktop_mode_media_usage_pre_save_refs'] = array();
411 +$GLOBALS['openstation_media_usage_pre_save_refs'] = array();
398 412
399 413 /**
400 414 * Extract attachment ids referenced by a post — featured image plus
401 415 * everything resolvable from `post_content`. Defers to the canonical
@@ -402,26 +416,24 @@
402 416 * resolver in `attached-media.php` so this buster catches the same
403 417 * cases the REST field does (block-class scan, classic `[caption]`
404 418 * shortcodes, `data-id` / `data-attachment-id`, and raw `<img src>`
405 419 * URL resolution including `-scaled.jpg` ↔ original swaps), plus
406 - * any ids appended via the `desktop_mode_my_wordpress_attached_media`
420 + * any ids appended via the `openstation_my_wordpress_attached_media`
407 421 * filter (ACF, page builders, post-meta galleries). Without this
408 422 * delegation, editing a post to add or remove a raw URL embed
409 423 * wouldn't bust the affected attachment's media-usage cache until
410 424 * the TTL expired.
411 425 *
412 - * @since 0.21.0
413 - *
414 426 * @param int|WP_Post $post Post id or object.
415 427 * @return array<int,true> Set of attachment ids keyed for dedup.
416 428 */
417 -function desktop_mode_my_wordpress_media_usage_extract_refs( $post ) {
429 +function openstation_my_wordpress_media_usage_extract_refs( $post ) {
418 430 $ids = array();
419 431 $obj = is_object( $post ) ? $post : get_post( (int) $post );
420 432 if ( ! $obj || ! isset( $obj->ID ) ) {
421 433 return $ids;
422 434 }
423 - if ( ! function_exists( 'desktop_mode_my_wordpress_post_attached_media' ) ) {
435 + if ( ! function_exists( 'openstation_my_wordpress_post_attached_media' ) ) {
424 436 // Defensive: bootstrap order should always load
425 437 // attached-media.php before this can be called from a save
426 438 // hook, but fall back to the legacy minimal scan just in
427 439 // case so the buster never silently no-ops.
@@ -436,9 +448,9 @@
436 448 }
437 449 }
438 450 return $ids;
439 451 }
440 - foreach ( desktop_mode_my_wordpress_post_attached_media( (int) $obj->ID ) as $id ) {
452 + foreach ( openstation_my_wordpress_post_attached_media( (int) $obj->ID ) as $id ) {
441 453 $id = (int) $id;
442 454 if ( $id > 0 ) {
443 455 $ids[ $id ] = true;
444 456 }
@@ -451,21 +463,19 @@
451 463 * Fired by `pre_post_update`, which runs before the DB row mutates,
452 464 * so `get_post` here returns the OLD content. We stash the ref set
453 465 * in a per-request global and read it back in the `save_post` hook.
454 466 *
455 - * @since 0.21.0
456 - *
457 467 * @param int $post_id Post id about to be updated.
458 468 */
459 -function desktop_mode_my_wordpress_media_usage_snapshot_pre_save( $post_id ) {
469 +function openstation_my_wordpress_media_usage_snapshot_pre_save( $post_id ) {
460 470 $post_id = (int) $post_id;
461 471 if ( $post_id <= 0 ) {
462 472 return;
463 473 }
464 - $GLOBALS['desktop_mode_media_usage_pre_save_refs'][ $post_id ] =
465 - desktop_mode_my_wordpress_media_usage_extract_refs( $post_id );
474 + $GLOBALS['openstation_media_usage_pre_save_refs'][ $post_id ] =
475 + openstation_my_wordpress_media_usage_extract_refs( $post_id );
466 476 }
467 -add_action( 'pre_post_update', 'desktop_mode_my_wordpress_media_usage_snapshot_pre_save' );
477 +add_action( 'pre_post_update', 'openstation_my_wordpress_media_usage_snapshot_pre_save' );
468 478
469 479 /**
470 480 * Bust the transient when a post changes. The cache key is
471 481 * per-attachment, so we don't know which entries reference what —
@@ -479,34 +489,32 @@
479 489 *
480 490 * Bounded by the actual count of `wp-image-N` matches in either
481 491 * version of the content + the post's `_thumbnail_id`.
482 492 *
483 - * @since 0.21.0
484 - *
485 493 * @param int $post_id Post id that was just modified.
486 494 */
487 -function desktop_mode_my_wordpress_media_usage_bust_for_post( $post_id ) {
495 +function openstation_my_wordpress_media_usage_bust_for_post( $post_id ) {
488 496 $post_id = (int) $post_id;
489 497 if ( $post_id <= 0 ) {
490 498 return;
491 499 }
492 500
493 - $ids = desktop_mode_my_wordpress_media_usage_extract_refs( $post_id );
501 + $ids = openstation_my_wordpress_media_usage_extract_refs( $post_id );
494 502
495 - if ( isset( $GLOBALS['desktop_mode_media_usage_pre_save_refs'][ $post_id ] ) ) {
496 - $ids += $GLOBALS['desktop_mode_media_usage_pre_save_refs'][ $post_id ];
497 - unset( $GLOBALS['desktop_mode_media_usage_pre_save_refs'][ $post_id ] );
503 + if ( isset( $GLOBALS['openstation_media_usage_pre_save_refs'][ $post_id ] ) ) {
504 + $ids += $GLOBALS['openstation_media_usage_pre_save_refs'][ $post_id ];
505 + unset( $GLOBALS['openstation_media_usage_pre_save_refs'][ $post_id ] );
498 506 }
499 507
500 508 foreach ( array_keys( $ids ) as $attachment_id ) {
501 - foreach ( desktop_mode_my_wordpress_media_usage_cache_buckets() as $bucket ) {
509 + foreach ( openstation_my_wordpress_media_usage_cache_buckets() as $bucket ) {
502 510 delete_transient(
503 - desktop_mode_my_wordpress_media_usage_cache_key( (int) $attachment_id, $bucket )
511 + openstation_my_wordpress_media_usage_cache_key( (int) $attachment_id, $bucket )
504 512 );
505 513 }
506 514 }
507 515 }
508 -add_action( 'save_post', 'desktop_mode_my_wordpress_media_usage_bust_for_post' );
516 +add_action( 'save_post', 'openstation_my_wordpress_media_usage_bust_for_post' );
509 517 // `before_delete_post`, NOT `deleted_post`. By the time `deleted_post`
510 518 // fires, `delete_all_meta_for_post` has already wiped `_thumbnail_id`
511 519 // and the row itself is gone — `extract_refs()` would return an empty
512 520 // set, so the cache for any referenced attachment would survive until
@@ -512,9 +520,9 @@
512 520 // set, so the cache for any referenced attachment would survive until
513 521 // its 5-minute TTL. `before_delete_post` fires while the post + meta
514 522 // are still readable. Signature matches (we only consume the first
515 523 // arg, the post id).
516 -add_action( 'before_delete_post', 'desktop_mode_my_wordpress_media_usage_bust_for_post' );
524 +add_action( 'before_delete_post', 'openstation_my_wordpress_media_usage_bust_for_post' );
517 525 // New posts skip `pre_post_update` but still go through `save_post`,
518 526 // so the buster works as-is — the pre-snapshot is just empty.
519 527
520 528 /**
@@ -519,20 +527,18 @@
519 527
520 528 /**
521 529 * Bust the transient when the attachment itself is deleted.
522 530 *
523 - * @since 0.21.0
524 - *
525 531 * @param int $post_id Attachment id.
526 532 */
527 -function desktop_mode_my_wordpress_media_usage_bust_for_attachment( $post_id ) {
533 +function openstation_my_wordpress_media_usage_bust_for_attachment( $post_id ) {
528 534 $post_id = (int) $post_id;
529 535 if ( $post_id <= 0 ) {
530 536 return;
531 537 }
532 - foreach ( desktop_mode_my_wordpress_media_usage_cache_buckets() as $bucket ) {
538 + foreach ( openstation_my_wordpress_media_usage_cache_buckets() as $bucket ) {
533 539 delete_transient(
534 - desktop_mode_my_wordpress_media_usage_cache_key( $post_id, $bucket )
540 + openstation_my_wordpress_media_usage_cache_key( $post_id, $bucket )
535 541 );
536 542 }
537 543 }
538 -add_action( 'delete_attachment', 'desktop_mode_my_wordpress_media_usage_bust_for_attachment' );
544 +add_action( 'delete_attachment', 'openstation_my_wordpress_media_usage_bust_for_attachment' );