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 / my-wordpress / lock.php

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

321 lines 10.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode — My WordPress: post-lock REST field.
4 *
5 * Surfaces "is this post currently being edited by someone else?"
6 * on every post / page / opt-in CPT REST response so the My WordPress
7 * file-explorer can show a lock icon + the locking user's name on
8 * the tile label without an extra round-trip.
9 *
10 * Core stores the lock as `_edit_lock` post meta with the shape
11 * `<timestamp>:<user_id>`. `wp_check_post_lock()` is the canonical
12 * read — it parses the meta, applies the `wp_check_post_lock_window`
13 * filter (default 150 s), and returns the locking user id or `false`.
14 * We expose the same intelligence as a structured field, gated on
15 * `edit_post` so users who can't edit the post never see who else is
16 * editing it.
17 *
18 * The field name is `desktop_mode_lock`; shape:
19 *
20 * - `null` — not locked, OR the requester lacks edit caps.
21 * - `{ userId, userName, userAvatarUrl, time }` — locked by another
22 * user. `time` is the ISO-8601 timestamp of the lock heartbeat.
23 *
24 * @package WPDesktopMode
25 * @since 0.8.0
26 */
27
28 defined( 'ABSPATH' ) || exit;
29
30 /**
31 * Compute the lock payload for a post.
32 *
33 * Returns `null` when:
34 * - The post isn't locked.
35 * - The current user is the lock holder (no point flagging yourself).
36 * - The current user can't edit the post (don't leak who's editing).
37 *
38 * @since 0.8.0
39 *
40 * @param int $post_id Post id.
41 * @return array{userId:int,userName:string,userAvatarUrl:string,time:string}|null
42 */
43 function desktop_mode_my_wordpress_post_lock_payload( $post_id ) {
44 $post_id = (int) $post_id;
45 if ( $post_id <= 0 ) {
46 return null;
47 }
48
49 if ( ! current_user_can( 'edit_post', $post_id ) ) {
50 return null;
51 }
52
53 require_once ABSPATH . 'wp-admin/includes/post.php';
54 $lock_user_id = wp_check_post_lock( $post_id );
55 if ( ! $lock_user_id ) {
56 return null;
57 }
58
59 $user = get_userdata( (int) $lock_user_id );
60 if ( ! $user ) {
61 return null;
62 }
63
64 // Read the raw meta to surface the heartbeat timestamp — useful
65 // in tooltips ("locked 8 seconds ago").
66 $raw = (string) get_post_meta( $post_id, '_edit_lock', true );
67 $timestamp = 0;
68 if ( '' !== $raw && false !== strpos( $raw, ':' ) ) {
69 list( $timestamp ) = explode( ':', $raw );
70 $timestamp = (int) $timestamp;
71 }
72
73 $avatar = get_avatar_url( $user->ID, array( 'size' => 48 ) );
74
75 return array(
76 'userId' => (int) $user->ID,
77 'userName' => (string) $user->display_name,
78 'userAvatarUrl' => is_string( $avatar ) ? $avatar : '',
79 'time' => $timestamp > 0 ? gmdate( 'c', $timestamp ) : '',
80 );
81 }
82
83 /**
84 * Compute the contributor list for a post. Returns an array of
85 * structured user shapes (one per user) so the JS side can paint
86 * tiles directly without an extra `/wp/v2/users/<id>` round-trip
87 * per row.
88 *
89 * Sources, merged in order:
90 * 1. Co-Authors Plus, when installed — `get_coauthors()` returns
91 * user objects (or guest authors with a different shape).
92 * 2. Revision authors — everyone who has saved the post leaves a
93 * revision row stamped with their user id.
94 * 3. The `_edit_last` post meta — who saved the post most
95 * recently; the only signal on installs with revisions
96 * disabled.
97 * 4. Anything plugins return from the
98 * `desktop_mode_my_wordpress_post_contributors` filter, which
99 * receives the post id + the running user-id list. Filter
100 * contract is plain int[] for ergonomics; we expand each id
101 * into the structured shape afterwards.
102 *
103 * Gated on `edit_post`, same as the lock payload above — returns an
104 * empty array for users who can't edit the post, so revision-author
105 * identities never leak to read-only viewers.
106 *
107 * The post's `post_author` is intentionally NOT included here —
108 * it's already surfaced by the canonical "Author" sub-folder.
109 * Contributors is the *additional* people surface.
110 *
111 * @since 0.8.0
112 *
113 * @param int $post_id Post id.
114 * @return array<int,array{userId:int,userName:string,userAvatarUrl:string}>
115 */
116 function desktop_mode_my_wordpress_post_contributors_payload( $post_id ) {
117 $post_id = (int) $post_id;
118 if ( $post_id <= 0 ) {
119 return array();
120 }
121
122 if ( ! current_user_can( 'edit_post', $post_id ) ) {
123 return array();
124 }
125
126 $post = get_post( $post_id );
127 if ( ! $post ) {
128 return array();
129 }
130
131 $primary_author_id = (int) $post->post_author;
132 $ids = array();
133
134 // Co-Authors Plus, when active. `get_coauthors()` returns a list
135 // that can mix `WP_User`s with guest-author objects (which have
136 // no `ID` and aren't WP users). We only collect real users; CAP
137 // guest authors are out of scope today (their avatar/edit URL
138 // shape is plugin-specific and would force an extra abstraction
139 // layer that doesn't pay for itself in Phase 1).
140 if ( function_exists( 'get_coauthors' ) ) {
141 $coauthors = get_coauthors( $post_id );
142 foreach ( (array) $coauthors as $user ) {
143 if ( $user instanceof WP_User ) {
144 $ids[] = (int) $user->ID;
145 } elseif ( is_object( $user ) && isset( $user->ID ) ) {
146 $ids[] = (int) $user->ID;
147 }
148 }
149 }
150
151 // Revision authors — every user who has hit Save / Update on
152 // this post leaves a revision row, and core stamps each
153 // revision's `post_author` with the editing user. Walking the
154 // revision list is therefore the canonical "who has edited this
155 // post" answer without any plugin or extra meta. We dedupe
156 // against the primary author below so the post owner doesn't
157 // double-count.
158 $revision_ids = wp_get_post_revisions(
159 $post_id,
160 array(
161 'fields' => 'ids',
162 // `posts_per_page = -1` so a long history doesn't truncate.
163 // The list is naturally bounded by core's revision retention
164 // filter (`wp_revisions_to_keep`), typically `5` to `unlimited`.
165 'numberposts' => -1,
166 )
167 );
168 foreach ( (array) $revision_ids as $rev_id ) {
169 $rev = get_post( $rev_id );
170 if ( $rev ) {
171 $ids[] = (int) $rev->post_author;
172 }
173 }
174
175 // `_edit_last` is core's "who saved this post most recently"
176 // post meta, set by `wp_update_post()`. On installs where
177 // revisions are disabled (or pruned aggressively) this is the
178 // only signal that a non-author user ever touched the row.
179 $edit_last = (int) get_post_meta( $post_id, '_edit_last', true );
180 if ( $edit_last > 0 ) {
181 $ids[] = $edit_last;
182 }
183
184 /**
185 * Filter the list of contributor user ids for a post.
186 *
187 * Plugins that track contributors via custom meta, a taxonomy,
188 * a join table, or any other mechanism wire their source in
189 * here. Each id should resolve to a `WP_User`; non-resolving
190 * ids are silently dropped.
191 *
192 * Examples:
193 *
194 * ```php
195 * // ACF user-list field "post_contributors":
196 * add_filter( 'desktop_mode_my_wordpress_post_contributors',
197 * function ( $ids, $post_id ) {
198 * $extra = (array) get_field( 'post_contributors', $post_id );
199 * foreach ( $extra as $u ) {
200 * if ( $u instanceof WP_User ) {
201 * $ids[] = $u->ID;
202 * } elseif ( is_numeric( $u ) ) {
203 * $ids[] = (int) $u;
204 * }
205 * }
206 * return $ids;
207 * }, 10, 2 );
208 * ```
209 *
210 * @since 0.8.0
211 *
212 * @param int[] $ids Contributor user ids gathered so far
213 * (from Co-Authors Plus, etc.).
214 * @param int $post_id Post id.
215 */
216 $ids = (array) apply_filters( 'desktop_mode_my_wordpress_post_contributors', $ids, $post_id );
217
218 // De-duplicate, drop the primary author so the Contributors
219 // sub-folder only carries *additional* people, drop empty/0,
220 // and resolve to user records.
221 $out = array();
222 $seen = array();
223 foreach ( $ids as $id ) {
224 $id = (int) $id;
225 if ( $id <= 0 ) {
226 continue;
227 }
228 if ( $id === $primary_author_id ) {
229 continue;
230 }
231 if ( isset( $seen[ $id ] ) ) {
232 continue;
233 }
234 $seen[ $id ] = true;
235 $user = get_userdata( $id );
236 if ( ! $user ) {
237 continue;
238 }
239 $avatar = get_avatar_url( $user->ID, array( 'size' => 96 ) );
240 $out[] = array(
241 'userId' => (int) $user->ID,
242 'userName' => (string) $user->display_name,
243 'userAvatarUrl' => is_string( $avatar ) ? $avatar : '',
244 );
245 }
246 return $out;
247 }
248
249 /**
250 * Register the REST fields on every public post type that runs
251 * through the standard `/wp/v2/<type>` endpoint. Posts and pages
252 * cover the Phase 1 surface; CPTs come along for free.
253 *
254 * Two fields:
255 * - `desktop_mode_lock` — active edit-lock holder.
256 * - `desktop_mode_contributors` — additional contributor users
257 * beyond the primary author.
258 *
259 * @since 0.8.0
260 */
261 function desktop_mode_my_wordpress_register_lock_field() {
262 $types = get_post_types(
263 array(
264 'show_in_rest' => true,
265 'public' => true,
266 ),
267 'names'
268 );
269
270 foreach ( $types as $type ) {
271 register_rest_field(
272 $type,
273 'desktop_mode_lock',
274 array(
275 'get_callback' => static function ( $post ) {
276 $post_id = isset( $post['id'] ) ? (int) $post['id'] : 0;
277 return desktop_mode_my_wordpress_post_lock_payload( $post_id );
278 },
279 'schema' => array(
280 'description' => __( 'Active edit-lock holder, or null when the post is not locked.', 'desktop-mode' ),
281 'type' => array( 'object', 'null' ),
282 'context' => array( 'view', 'edit' ),
283 'readonly' => true,
284 'properties' => array(
285 'userId' => array( 'type' => 'integer' ),
286 'userName' => array( 'type' => 'string' ),
287 'userAvatarUrl' => array( 'type' => 'string' ),
288 'time' => array( 'type' => 'string' ),
289 ),
290 ),
291 )
292 );
293
294 register_rest_field(
295 $type,
296 'desktop_mode_contributors',
297 array(
298 'get_callback' => static function ( $post ) {
299 $post_id = isset( $post['id'] ) ? (int) $post['id'] : 0;
300 return desktop_mode_my_wordpress_post_contributors_payload( $post_id );
301 },
302 'schema' => array(
303 'description' => __( 'Additional contributor users beyond the primary author. Sourced from Co-Authors Plus when present, revision authors, the `_edit_last` meta, plus anything plugins return via `desktop_mode_my_wordpress_post_contributors`. Empty for requesters who cannot edit the post.', 'desktop-mode' ),
304 'type' => 'array',
305 'context' => array( 'view', 'edit' ),
306 'readonly' => true,
307 'items' => array(
308 'type' => 'object',
309 'properties' => array(
310 'userId' => array( 'type' => 'integer' ),
311 'userName' => array( 'type' => 'string' ),
312 'userAvatarUrl' => array( 'type' => 'string' ),
313 ),
314 ),
315 ),
316 )
317 );
318 }
319 }
320 add_action( 'rest_api_init', 'desktop_mode_my_wordpress_register_lock_field' );
321