PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.8.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.8.8
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 / desktop-files / sharing.php

sharing.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.8.8, at includes/desktop-files/sharing.php

181 lines 6.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode — Folder sharing visibility logic.
4 *
5 * Computes which folders a viewer can see based on each folder's
6 * `share_mode` / `share_meta` columns:
7 *
8 * - `private` — owner only.
9 * - `users` — owner + ids in `share_meta.users`.
10 * - `roles` — owner + users with any role in `share_meta.roles`.
11 * - `all` — every desktop-mode user on the site.
12 *
13 * Hooked at priority 5 on `desktop_mode_files_visible_folders`
14 * so plugins layering custom share modes (registered via
15 * `desktop_mode_files_share_modes`) can run later in the chain
16 * without competing for the early slot.
17 *
18 * @package WPDesktopMode
19 * @since 0.9.0
20 */
21
22 defined( 'ABSPATH' ) || exit;
23
24 /**
25 * Filter callback that augments the owner-only list with folders
26 * the viewer can see by virtue of a non-private share mode.
27 *
28 * @since 0.9.0
29 *
30 * @param array $owned Owner-only folders (default from the store).
31 * @param int $user_id Viewer.
32 * @return array
33 */
34 function desktop_mode_files_compute_visible_folders( $owned, $user_id ) {
35 global $wpdb;
36 $user_id = (int) $user_id;
37 if ( $user_id <= 0 ) {
38 return is_array( $owned ) ? $owned : array();
39 }
40
41 $tables = desktop_mode_files_table_names();
42 $user = get_userdata( $user_id );
43 $roles = $user ? array_values( (array) $user->roles ) : array();
44
45 // Source 1 — `share_mode='all'`. Pull straight from the folders
46 // table; the shares table never carries 'all' rows.
47 $all_rows = $wpdb->get_results(
48 $wpdb->prepare(
49 "SELECT * FROM {$tables['folders']}
50 WHERE owner_id <> %d
51 AND share_mode = 'all'
52 AND trashed_at_ms IS NULL",
53 $user_id
54 ),
55 ARRAY_A
56 );
57
58 // Source 2 — accepted user-principal shares. State lives on the
59 // shares row: once the recipient clicks Accept we flip
60 // `state='accepted'` directly.
61 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
62 $user_share_rows = $wpdb->get_results(
63 $wpdb->prepare(
64 "SELECT DISTINCT f.* FROM {$tables['folders']} f
65 INNER JOIN {$tables['shares']} s ON s.folder_id = f.id
66 WHERE f.owner_id <> %d
67 AND f.trashed_at_ms IS NULL
68 AND s.state = 'accepted'
69 AND s.principal_type = 'user'
70 AND s.principal_ref = %s",
71 $user_id,
72 (string) $user_id
73 ),
74 ARRAY_A
75 );
76
77 // Source 2b — role-principal shares the viewer has individually
78 // accepted via the per-user decisions table. The shares row
79 // itself intentionally stays `state='pending'` for role-principal
80 // invites (we don't flip a role share to 'accepted' on behalf of
81 // every member of the role — that would be a "first to click
82 // decides for all" bug). The per-user acceptance lives in the
83 // decisions table, mirroring the resolution logic in
84 // `desktop_mode_folder_share_user_capability`.
85 //
86 // Without this join the role recipient could see the folder via
87 // REST `list_placements` (which routes through
88 // `_user_capability`, which DOES consult decisions) but their
89 // heartbeat would miss live updates because the heartbeat
90 // short-circuits on `compute_visible_folders` — leaving new
91 // files the owner added invisible until F5.
92 $role_share_rows = array();
93 if ( ! empty( $roles ) ) {
94 $placeholders = implode( ',', array_fill( 0, count( $roles ), '%s' ) );
95 $role_args = array_merge( array( $user_id, $user_id ), array_map( 'strval', $roles ) );
96 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
97 $role_share_rows = $wpdb->get_results(
98 $wpdb->prepare(
99 "SELECT DISTINCT f.* FROM {$tables['folders']} f
100 INNER JOIN {$tables['shares']} s ON s.folder_id = f.id
101 INNER JOIN {$tables['decisions']} d
102 ON d.share_id = s.id
103 AND d.user_id = %d
104 AND d.state = 'accepted'
105 WHERE f.owner_id <> %d
106 AND f.trashed_at_ms IS NULL
107 AND s.principal_type = 'role'
108 AND s.principal_ref IN ($placeholders)",
109 $role_args
110 ),
111 ARRAY_A
112 );
113 }
114
115 $share_rows = array_merge( (array) $user_share_rows, (array) $role_share_rows );
116
117 $visible = is_array( $owned ) ? $owned : array();
118 $seen_ids = array();
119 foreach ( $visible as $row ) {
120 $seen_ids[ (int) $row['id'] ] = true;
121 }
122 foreach ( array_merge( (array) $all_rows, (array) $share_rows ) as $raw ) {
123 $row = desktop_mode_files_normalize_folder_row( $raw );
124 $id = (int) $row['id'];
125 if ( isset( $seen_ids[ $id ] ) ) {
126 continue;
127 }
128 if ( desktop_mode_files_user_can_see_folder( $row, $user_id, $roles ) ) {
129 $visible[] = $row;
130 $seen_ids[ $id ] = true;
131 }
132 }
133 return $visible;
134 }
135 add_filter( 'desktop_mode_files_visible_folders', 'desktop_mode_files_compute_visible_folders', 5, 2 );
136
137 /**
138 * Whether the viewer's identity satisfies a folder's share rules.
139 *
140 * @since 0.9.0
141 *
142 * @param array $folder Normalized folder row.
143 * @param int $user_id Viewer.
144 * @param string[] $user_roles Viewer's roles.
145 * @return bool
146 */
147 function desktop_mode_files_user_can_see_folder( $folder, $user_id, $user_roles ) {
148 $mode = (string) $folder['share_mode'];
149
150 // Owner always sees the folder.
151 if ( (int) $folder['owner_id'] === (int) $user_id ) {
152 $can = true;
153 } elseif ( 'all' === $mode ) {
154 $can = true;
155 } else {
156 // Non-owner viewer: the shares table is the single source
157 // of truth. `share_meta` on the folders row is diagnostic
158 // only — it is never consulted for visibility. (Earlier
159 // drafts had a fallback that silently re-granted access
160 // to revoked recipients; reviewer caught the
161 // revocation-bypass and we dropped the fallback before
162 // the feature shipped.)
163 $cap = desktop_mode_folder_share_user_capability( (int) $folder['id'], (int) $user_id );
164 $can = 'none' !== $cap;
165 }
166
167 /**
168 * Filter the per-folder visibility decision. Plugins layering
169 * custom share modes (e.g. 'team', 'workspace') can compute
170 * `$can` here.
171 *
172 * @since 0.9.0
173 *
174 * @param bool $can Default decision.
175 * @param array $folder Folder row.
176 * @param int $user_id Viewer.
177 * @param string[] $roles Viewer's roles.
178 */
179 return (bool) apply_filters( 'desktop_mode_files_user_can_see_folder', $can, $folder, $user_id, $user_roles );
180 }
181