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

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

307 lines 10.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — 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 `openstation_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 OpenStation
25 */
26
27 defined( 'ABSPATH' ) || exit;
28
29 /**
30 * Compute the lock payload for a post.
31 *
32 * Returns `null` when:
33 * - The post isn't locked.
34 * - The current user is the lock holder (no point flagging yourself).
35 * - The current user can't edit the post (don't leak who's editing).
36 *
37 * @param int $post_id Post id.
38 * @return array{userId:int,userName:string,userAvatarUrl:string,time:string}|null
39 */
40 function openstation_my_wordpress_post_lock_payload( $post_id ) {
41 $post_id = (int) $post_id;
42 if ( $post_id <= 0 ) {
43 return null;
44 }
45
46 if ( ! current_user_can( 'edit_post', $post_id ) ) {
47 return null;
48 }
49
50 require_once ABSPATH . 'wp-admin/includes/post.php';
51 $lock_user_id = wp_check_post_lock( $post_id );
52 if ( ! $lock_user_id ) {
53 return null;
54 }
55
56 $user = get_userdata( (int) $lock_user_id );
57 if ( ! $user ) {
58 return null;
59 }
60
61 // Read the raw meta to surface the heartbeat timestamp — useful
62 // in tooltips ("locked 8 seconds ago").
63 $raw = (string) get_post_meta( $post_id, '_edit_lock', true );
64 $timestamp = 0;
65 if ( '' !== $raw && false !== strpos( $raw, ':' ) ) {
66 list( $timestamp ) = explode( ':', $raw );
67 $timestamp = (int) $timestamp;
68 }
69
70 $avatar = get_avatar_url( $user->ID, array( 'size' => 48 ) );
71
72 return array(
73 'userId' => (int) $user->ID,
74 'userName' => (string) $user->display_name,
75 'userAvatarUrl' => is_string( $avatar ) ? $avatar : '',
76 'time' => $timestamp > 0 ? gmdate( 'c', $timestamp ) : '',
77 );
78 }
79
80 /**
81 * Compute the contributor list for a post. Returns an array of
82 * structured user shapes (one per user) so the JS side can paint
83 * tiles directly without an extra `/wp/v2/users/<id>` round-trip
84 * per row.
85 *
86 * Sources, merged in order:
87 * 1. Co-Authors Plus, when installed — `get_coauthors()` returns
88 * user objects (or guest authors with a different shape).
89 * 2. Revision authors — everyone who has saved the post leaves a
90 * revision row stamped with their user id.
91 * 3. The `_edit_last` post meta — who saved the post most
92 * recently; the only signal on installs with revisions
93 * disabled.
94 * 4. Anything plugins return from the
95 * `openstation_my_wordpress_post_contributors` filter, which
96 * receives the post id + the running user-id list. Filter
97 * contract is plain int[] for ergonomics; we expand each id
98 * into the structured shape afterwards.
99 *
100 * Gated on `edit_post`, same as the lock payload above — returns an
101 * empty array for users who can't edit the post, so revision-author
102 * identities never leak to read-only viewers.
103 *
104 * The post's `post_author` is intentionally NOT included here —
105 * it's already surfaced by the canonical "Author" sub-folder.
106 * Contributors is the *additional* people surface.
107 *
108 * @param int $post_id Post id.
109 * @return array<int,array{userId:int,userName:string,userAvatarUrl:string}>
110 */
111 function openstation_my_wordpress_post_contributors_payload( $post_id ) {
112 $post_id = (int) $post_id;
113 if ( $post_id <= 0 ) {
114 return array();
115 }
116
117 if ( ! current_user_can( 'edit_post', $post_id ) ) {
118 return array();
119 }
120
121 $post = get_post( $post_id );
122 if ( ! $post ) {
123 return array();
124 }
125
126 $primary_author_id = (int) $post->post_author;
127 $ids = array();
128
129 // Co-Authors Plus, when active. `get_coauthors()` returns a list
130 // that can mix `WP_User`s with guest-author objects (which have
131 // no `ID` and aren't WP users). We only collect real users; CAP
132 // guest authors are out of scope today (their avatar/edit URL
133 // shape is plugin-specific and would force an extra abstraction
134 // layer that doesn't pay for itself in Phase 1).
135 if ( function_exists( 'get_coauthors' ) ) {
136 $coauthors = get_coauthors( $post_id );
137 foreach ( (array) $coauthors as $user ) {
138 if ( $user instanceof WP_User ) {
139 $ids[] = (int) $user->ID;
140 } elseif ( is_object( $user ) && isset( $user->ID ) ) {
141 $ids[] = (int) $user->ID;
142 }
143 }
144 }
145
146 // Revision authors — every user who has hit Save / Update on
147 // this post leaves a revision row, and core stamps each
148 // revision's `post_author` with the editing user. Walking the
149 // revision list is therefore the canonical "who has edited this
150 // post" answer without any plugin or extra meta. We dedupe
151 // against the primary author below so the post owner doesn't
152 // double-count.
153 $revision_ids = wp_get_post_revisions(
154 $post_id,
155 array(
156 'fields' => 'ids',
157 // `posts_per_page = -1` so a long history doesn't truncate.
158 // The list is naturally bounded by core's revision retention
159 // filter (`wp_revisions_to_keep`), typically `5` to `unlimited`.
160 'numberposts' => -1,
161 )
162 );
163 foreach ( (array) $revision_ids as $rev_id ) {
164 $rev = get_post( $rev_id );
165 if ( $rev ) {
166 $ids[] = (int) $rev->post_author;
167 }
168 }
169
170 // `_edit_last` is core's "who saved this post most recently"
171 // post meta, set by `wp_update_post()`. On installs where
172 // revisions are disabled (or pruned aggressively) this is the
173 // only signal that a non-author user ever touched the row.
174 $edit_last = (int) get_post_meta( $post_id, '_edit_last', true );
175 if ( $edit_last > 0 ) {
176 $ids[] = $edit_last;
177 }
178
179 /**
180 * Filter the list of contributor user ids for a post.
181 *
182 * Plugins that track contributors via custom meta, a taxonomy,
183 * a join table, or any other mechanism wire their source in
184 * here. Each id should resolve to a `WP_User`; non-resolving
185 * ids are silently dropped.
186 *
187 * Examples:
188 *
189 * ```php
190 * // ACF user-list field "post_contributors":
191 * add_filter( 'openstation_my_wordpress_post_contributors',
192 * function ( $ids, $post_id ) {
193 * $extra = (array) get_field( 'post_contributors', $post_id );
194 * foreach ( $extra as $u ) {
195 * if ( $u instanceof WP_User ) {
196 * $ids[] = $u->ID;
197 * } elseif ( is_numeric( $u ) ) {
198 * $ids[] = (int) $u;
199 * }
200 * }
201 * return $ids;
202 * }, 10, 2 );
203 * ```
204 *
205 * @param int[] $ids Contributor user ids gathered so far
206 * (from Co-Authors Plus, etc.).
207 * @param int $post_id Post id.
208 */
209 $ids = (array) apply_filters( 'openstation_my_wordpress_post_contributors', $ids, $post_id );
210
211 // De-duplicate, drop the primary author so the Contributors
212 // sub-folder only carries *additional* people, drop empty/0,
213 // and resolve to user records.
214 $out = array();
215 $seen = array();
216 foreach ( $ids as $id ) {
217 $id = (int) $id;
218 if ( $id <= 0 ) {
219 continue;
220 }
221 if ( $id === $primary_author_id ) {
222 continue;
223 }
224 if ( isset( $seen[ $id ] ) ) {
225 continue;
226 }
227 $seen[ $id ] = true;
228 $user = get_userdata( $id );
229 if ( ! $user ) {
230 continue;
231 }
232 $avatar = get_avatar_url( $user->ID, array( 'size' => 96 ) );
233 $out[] = array(
234 'userId' => (int) $user->ID,
235 'userName' => (string) $user->display_name,
236 'userAvatarUrl' => is_string( $avatar ) ? $avatar : '',
237 );
238 }
239 return $out;
240 }
241
242 /**
243 * Register the REST fields on every post type the site window can
244 * browse — public REST-exposed types plus the ones bridged under
245 * `desktop-mode/v1`. Posts and pages cover the Phase 1 surface; CPTs
246 * come along for free.
247 *
248 * Two fields:
249 * - `openstation_lock` — active edit-lock holder.
250 * - `openstation_contributors` — additional contributor users
251 * beyond the primary author.
252 */
253 function openstation_my_wordpress_register_lock_field() {
254 $types = openstation_my_wordpress_rest_field_post_types();
255
256 foreach ( $types as $type ) {
257 register_rest_field(
258 $type,
259 'openstation_lock',
260 array(
261 'get_callback' => static function ( $post ) {
262 $post_id = isset( $post['id'] ) ? (int) $post['id'] : 0;
263 return openstation_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 'openstation_contributors',
283 array(
284 'get_callback' => static function ( $post ) {
285 $post_id = isset( $post['id'] ) ? (int) $post['id'] : 0;
286 return openstation_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, revision authors, the `_edit_last` meta, plus anything plugins return via `openstation_my_wordpress_post_contributors`. Empty for requesters who cannot edit the post.', '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', 'openstation_my_wordpress_register_lock_field' );
307