PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.8.7
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.8.7
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.8.7, at includes/my-wordpress/lock.php

307 lines 9.8 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. Anything plugins return from the
93 * `desktop_mode_my_wordpress_post_contributors` filter, which
94 * receives the post id + the running user-id list. Filter
95 * contract is plain int[] for ergonomics; we expand each id
96 * into the structured shape afterwards.
97 *
98 * The post's `post_author` is intentionally NOT included here —
99 * it's already surfaced by the canonical "Author" sub-folder.
100 * Contributors is the *additional* people surface.
101 *
102 * @since 0.8.0
103 *
104 * @param int $post_id Post id.
105 * @return array<int,array{userId:int,userName:string,userAvatarUrl:string}>
106 */
107 function desktop_mode_my_wordpress_post_contributors_payload( $post_id ) {
108 $post_id = (int) $post_id;
109 if ( $post_id <= 0 ) {
110 return array();
111 }
112 $post = get_post( $post_id );
113 if ( ! $post ) {
114 return array();
115 }
116
117 $primary_author_id = (int) $post->post_author;
118 $ids = array();
119
120 // Co-Authors Plus, when active. `get_coauthors()` returns a list
121 // that can mix `WP_User`s with guest-author objects (which have
122 // no `ID` and aren't WP users). We only collect real users; CAP
123 // guest authors are out of scope today (their avatar/edit URL
124 // shape is plugin-specific and would force an extra abstraction
125 // layer that doesn't pay for itself in Phase 1).
126 if ( function_exists( 'get_coauthors' ) ) {
127 $coauthors = get_coauthors( $post_id );
128 foreach ( (array) $coauthors as $user ) {
129 if ( $user instanceof WP_User ) {
130 $ids[] = (int) $user->ID;
131 } elseif ( is_object( $user ) && isset( $user->ID ) ) {
132 $ids[] = (int) $user->ID;
133 }
134 }
135 }
136
137 // Revision authors — every user who has hit Save / Update on
138 // this post leaves a revision row, and core stamps each
139 // revision's `post_author` with the editing user. Walking the
140 // revision list is therefore the canonical "who has edited this
141 // post" answer without any plugin or extra meta. We dedupe
142 // against the primary author below so the post owner doesn't
143 // double-count.
144 $revision_ids = wp_get_post_revisions(
145 $post_id,
146 array(
147 'fields' => 'ids',
148 // `posts_per_page = -1` so a long history doesn't truncate.
149 // The list is naturally bounded by core's revision retention
150 // filter (`wp_revisions_to_keep`), typically `5` to `unlimited`.
151 'numberposts' => -1,
152 )
153 );
154 foreach ( (array) $revision_ids as $rev_id ) {
155 $rev = get_post( $rev_id );
156 if ( $rev ) {
157 $ids[] = (int) $rev->post_author;
158 }
159 }
160
161 // `_edit_last` is core's "who saved this post most recently"
162 // post meta, set by `wp_update_post()`. On installs where
163 // revisions are disabled (or pruned aggressively) this is the
164 // only signal that a non-author user ever touched the row.
165 $edit_last = (int) get_post_meta( $post_id, '_edit_last', true );
166 if ( $edit_last > 0 ) {
167 $ids[] = $edit_last;
168 }
169
170 /**
171 * Filter the list of contributor user ids for a post.
172 *
173 * Plugins that track contributors via custom meta, a taxonomy,
174 * a join table, or any other mechanism wire their source in
175 * here. Each id should resolve to a `WP_User`; non-resolving
176 * ids are silently dropped.
177 *
178 * Examples:
179 *
180 * ```php
181 * // ACF user-list field "post_contributors":
182 * add_filter( 'desktop_mode_my_wordpress_post_contributors',
183 * function ( $ids, $post_id ) {
184 * $extra = (array) get_field( 'post_contributors', $post_id );
185 * foreach ( $extra as $u ) {
186 * if ( $u instanceof WP_User ) {
187 * $ids[] = $u->ID;
188 * } elseif ( is_numeric( $u ) ) {
189 * $ids[] = (int) $u;
190 * }
191 * }
192 * return $ids;
193 * }, 10, 2 );
194 * ```
195 *
196 * @since 0.8.0
197 *
198 * @param int[] $ids Contributor user ids gathered so far
199 * (from Co-Authors Plus, etc.).
200 * @param int $post_id Post id.
201 */
202 $ids = (array) apply_filters( 'desktop_mode_my_wordpress_post_contributors', $ids, $post_id );
203
204 // De-duplicate, drop the primary author so the Contributors
205 // sub-folder only carries *additional* people, drop empty/0,
206 // and resolve to user records.
207 $out = array();
208 $seen = array();
209 foreach ( $ids as $id ) {
210 $id = (int) $id;
211 if ( $id <= 0 ) {
212 continue;
213 }
214 if ( $id === $primary_author_id ) {
215 continue;
216 }
217 if ( isset( $seen[ $id ] ) ) {
218 continue;
219 }
220 $seen[ $id ] = true;
221 $user = get_userdata( $id );
222 if ( ! $user ) {
223 continue;
224 }
225 $avatar = get_avatar_url( $user->ID, array( 'size' => 96 ) );
226 $out[] = array(
227 'userId' => (int) $user->ID,
228 'userName' => (string) $user->display_name,
229 'userAvatarUrl' => is_string( $avatar ) ? $avatar : '',
230 );
231 }
232 return $out;
233 }
234
235 /**
236 * Register the REST fields on every public post type that runs
237 * through the standard `/wp/v2/<type>` endpoint. Posts and pages
238 * cover the Phase 1 surface; CPTs come along for free.
239 *
240 * Two fields:
241 * - `desktop_mode_lock` — active edit-lock holder.
242 * - `desktop_mode_contributors` — additional contributor users
243 * beyond the primary author.
244 *
245 * @since 0.8.0
246 */
247 function desktop_mode_my_wordpress_register_lock_field() {
248 $types = get_post_types(
249 array(
250 'show_in_rest' => true,
251 'public' => true,
252 ),
253 'names'
254 );
255
256 foreach ( $types as $type ) {
257 register_rest_field(
258 $type,
259 'desktop_mode_lock',
260 array(
261 'get_callback' => static function ( $post ) {
262 $post_id = isset( $post['id'] ) ? (int) $post['id'] : 0;
263 return desktop_mode_my_wordpress_post_lock_payload( $post_id );
264 },
265 'schema' => array(
266 'description' => __( 'Active edit-lock holder, or null when the post is not locked.', 'desktop-mode' ),
267 'type' => array( 'object', 'null' ),
268 'context' => array( 'view', 'edit' ),
269 'readonly' => true,
270 'properties' => array(
271 'userId' => array( 'type' => 'integer' ),
272 'userName' => array( 'type' => 'string' ),
273 'userAvatarUrl' => array( 'type' => 'string' ),
274 'time' => array( 'type' => 'string' ),
275 ),
276 ),
277 )
278 );
279
280 register_rest_field(
281 $type,
282 'desktop_mode_contributors',
283 array(
284 'get_callback' => static function ( $post ) {
285 $post_id = isset( $post['id'] ) ? (int) $post['id'] : 0;
286 return desktop_mode_my_wordpress_post_contributors_payload( $post_id );
287 },
288 'schema' => array(
289 'description' => __( 'Additional contributor users beyond the primary author. Sourced from Co-Authors Plus when present, plus anything plugins return via `desktop_mode_my_wordpress_post_contributors`.', 'desktop-mode' ),
290 'type' => 'array',
291 'context' => array( 'view', 'edit' ),
292 'readonly' => true,
293 'items' => array(
294 'type' => 'object',
295 'properties' => array(
296 'userId' => array( 'type' => 'integer' ),
297 'userName' => array( 'type' => 'string' ),
298 'userAvatarUrl' => array( 'type' => 'string' ),
299 ),
300 ),
301 ),
302 )
303 );
304 }
305 }
306 add_action( 'rest_api_init', 'desktop_mode_my_wordpress_register_lock_field' );
307