| @@ -409,8 +409,44 @@ | ||
| 409 | 409 | } |
| 410 | 410 | add_filter( 'openstation_shell_config', 'openstation_files_inject_boot_placements', 20 ); |
| 411 | 411 | |
| 412 | 412 | /** |
| 413 | + * Inline the viewer's visible folder rows into the boot-time shell | |
| 414 | + * config, the same way the root placements are. | |
| 415 | + * | |
| 416 | + * The client keeps a folders map alongside its placements, and | |
| 417 | + * anything that needs to know a folder's OWNER reads it there — | |
| 418 | + * notably the "Share folder" title-bar button, which is owner-only | |
| 419 | + * and has nothing but a window id to work from. Nothing on the | |
| 420 | + * normal boot path filled that map: placement hydration populates | |
| 421 | + * placements, and `listFolders()` only ran after a create, a rename | |
| 422 | + * or an untrash. So a plain reload left every folder ownerless, and | |
| 423 | + * the owner of a folder lost the one control that manages its | |
| 424 | + * sharing until something happened to repopulate the map. | |
| 425 | + * | |
| 426 | + * Mirrors GET /folders exactly (same visibility resolution — owned | |
| 427 | + * folders plus accepted shares plus `share_mode='all'` — same | |
| 428 | + * shape), and the client consumes it one-shot, so any later | |
| 429 | + * re-hydration still goes through REST for fresh state. | |
| 430 | + * | |
| 431 | + * @param array $config Shell config. | |
| 432 | + * @return array | |
| 433 | + */ | |
| 434 | +function openstation_files_inject_boot_folders( $config ) { | |
| 435 | + $user_id = get_current_user_id(); | |
| 436 | + if ( $user_id <= 0 ) { | |
| 437 | + return $config; | |
| 438 | + } | |
| 439 | + $out = array(); | |
| 440 | + foreach ( openstation_files_get_visible_folders( $user_id ) as $row ) { | |
| 441 | + $out[] = openstation_files_shape_folder( $row ); | |
| 442 | + } | |
| 443 | + $config['filesBootFolders'] = $out; | |
| 444 | + return $config; | |
| 445 | +} | |
| 446 | +add_filter( 'openstation_shell_config', 'openstation_files_inject_boot_folders', 20 ); | |
| 447 | + | |
| 448 | +/** | |
| 413 | 449 | * GET /placements |
| 414 | 450 | */ |
| 415 | 451 | function openstation_files_rest_list_placements( WP_REST_Request $req ) { |
| 416 | 452 | $user_id = get_current_user_id(); |
| @@ -733,28 +769,13 @@ | ||
| 733 | 769 | 'shareMode' => (string) $row['share_mode'], |
| 734 | 770 | 'shareMeta' => isset( $row['share_meta'] ) ? $row['share_meta'] : null, |
| 735 | 771 | 'updatedAtMs' => (int) $row['updated_at_ms'], |
| 736 | 772 | ); |
| 737 | - if ( function_exists( 'openstation_files_get_folder_shares' ) ) { | |
| 738 | - $shares = openstation_files_get_folder_shares( (int) $row['id'] ); | |
| 739 | - $accepted_count = 0; | |
| 740 | - $has_all = 'all' === (string) $row['share_mode']; | |
| 741 | - foreach ( $shares as $s ) { | |
| 742 | - if ( 'accepted' === $s['state'] ) { | |
| 743 | - ++$accepted_count; | |
| 744 | - } | |
| 745 | - } | |
| 746 | - // `shared` is viewer-agnostic — recipients need it for the | |
| 747 | - // shared-folder badge. The recipient COUNT is owner-internal | |
| 748 | - // (the dedicated shares endpoint gates the full roster on | |
| 749 | - // `share_can_manage`), so only managers get the real number; | |
| 750 | - // every other viewer sees `0`, keeping the wire shape stable. | |
| 751 | - $can_manage = function_exists( 'openstation_files_share_can_manage' ) | |
| 752 | - && openstation_files_share_can_manage( (int) $row['id'], get_current_user_id() ); | |
| 753 | - $shape['shareSummary'] = array( | |
| 754 | - 'shared' => $has_all || $accepted_count > 0, | |
| 755 | - 'recipientCount' => $can_manage ? $accepted_count + ( $has_all ? 1 : 0 ) : 0, | |
| 756 | - ); | |
| 773 | + // Same summary `OpenStation_Folder_File::serialize()` puts on a | |
| 774 | + // folder PLACEMENT, so a tile paints identically whichever | |
| 775 | + // response it came from. | |
| 776 | + if ( function_exists( 'openstation_files_folder_share_summary' ) ) { | |
| 777 | + $shape['shareSummary'] = openstation_files_folder_share_summary( $row ); | |
| 757 | 778 | } |
| 758 | 779 | return $shape; |
| 759 | 780 | } |
| 760 | 781 | |
| @@ -834,9 +855,9 @@ | ||
| 834 | 855 | } |
| 835 | 856 | } |
| 836 | 857 | $actor_payload = array( |
| 837 | 858 | 'id' => $viewer_can_see ? $actor_id : 0, |
| 838 | - 'name' => $viewer_can_see && $actor ? $actor->display_name : '', | |
| 859 | + 'name' => $viewer_can_see && $actor ? openstation_plain_text_title( $actor->display_name ) : '', | |
| 839 | 860 | 'avatar' => $viewer_can_see && $actor ? get_avatar_url( $actor->ID, array( 'size' => 32 ) ) : '', |
| 840 | 861 | ); |
| 841 | 862 | |
| 842 | 863 | return new WP_Error( |
| @@ -880,9 +901,9 @@ | ||
| 880 | 901 | ); |
| 881 | 902 | if ( 'user' === $row['principal_type'] ) { |
| 882 | 903 | $uid = (int) $row['principal_ref']; |
| 883 | 904 | $user = $uid > 0 ? get_userdata( $uid ) : null; |
| 884 | - $shape['displayName'] = $user ? $user->display_name : ''; | |
| 905 | + $shape['displayName'] = $user ? openstation_plain_text_title( $user->display_name ) : ''; | |
| 885 | 906 | $shape['avatarUrl'] = $user ? get_avatar_url( $uid, array( 'size' => 48 ) ) : ''; |
| 886 | 907 | } else { |
| 887 | 908 | $roles = wp_roles(); |
| 888 | 909 | $info = $roles && isset( $roles->roles[ $row['principal_ref'] ] ) ? $roles->roles[ $row['principal_ref'] ] : null; |
| @@ -1162,18 +1183,31 @@ | ||
| 1162 | 1183 | // in the dropdown on it. |
| 1163 | 1184 | $exclude[] = (int) get_current_user_id(); |
| 1164 | 1185 | $exclude = array_values( array_unique( array_filter( $exclude ) ) ); |
| 1165 | 1186 | |
| 1187 | + // Eligibility lives in the query, not only in the loop below: | |
| 1188 | + // with `number` applied first, a page of 20 Subscribers (or | |
| 1189 | + // agents) sorting ahead of the editors came back empty after | |
| 1190 | + // the per-row check, and the picker showed nobody at all. The | |
| 1191 | + // loop still re-checks every row, because the filter below can | |
| 1192 | + // rewrite these args. | |
| 1166 | 1193 | $args = array( |
| 1167 | - 'number' => 20, | |
| 1168 | - 'orderby' => 'display_name', | |
| 1169 | - 'order' => 'ASC', | |
| 1170 | - 'exclude' => $exclude, | |
| 1194 | + 'number' => 20, | |
| 1195 | + 'orderby' => 'display_name', | |
| 1196 | + 'order' => 'ASC', | |
| 1197 | + 'exclude' => $exclude, | |
| 1198 | + 'capability' => 'edit_posts', | |
| 1199 | + 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- agents are excluded in SQL so they cannot use up the page. | |
| 1200 | + array( | |
| 1201 | + 'key' => OPENSTATION_AGENT_USER_MARKER_META, | |
| 1202 | + 'compare' => 'NOT EXISTS', | |
| 1203 | + ), | |
| 1204 | + ), | |
| 1171 | 1205 | // `fields => 'all'` returns full WP_User objects so the |
| 1172 | 1206 | // capability check below resolves role caps correctly. A |
| 1173 | 1207 | // stdClass with stripped fields breaks `user_can()` on |
| 1174 | 1208 | // some WordPress versions and silently drops every row. |
| 1175 | - 'fields' => 'all', | |
| 1209 | + 'fields' => 'all', | |
| 1176 | 1210 | ); |
| 1177 | 1211 | if ( '' !== $q ) { |
| 1178 | 1212 | $args['search'] = '*' . $q . '*'; |
| 1179 | 1213 | $args['search_columns'] = array( 'user_login', 'user_email', 'display_name', 'user_nicename' ); |
| @@ -1190,9 +1224,9 @@ | ||
| 1190 | 1224 | $query = new WP_User_Query( $args ); |
| 1191 | 1225 | $users = $query->get_results(); |
| 1192 | 1226 | $out = array(); |
| 1193 | 1227 | foreach ( (array) $users as $user ) { |
| 1194 | - if ( ! user_can( $user, 'edit_posts' ) ) { | |
| 1228 | + if ( ! openstation_files_share_user_is_eligible( $user ) ) { | |
| 1195 | 1229 | continue; |
| 1196 | 1230 | } |
| 1197 | 1231 | // Disambiguation handle uses `user_nicename` (the public |
| 1198 | 1232 | // URL slug) instead of `user_login` — the login is the auth |
| @@ -1200,9 +1234,9 @@ | ||
| 1200 | 1234 | // broader than needed for a share picker. Matches the |
| 1201 | 1235 | // `slug` field WP's own `/wp/v2/users` endpoint surfaces. |
| 1202 | 1236 | $out[] = array( |
| 1203 | 1237 | 'id' => (int) $user->ID, |
| 1204 | - 'name' => (string) $user->display_name, | |
| 1238 | + 'name' => openstation_plain_text_title( $user->display_name ), | |
| 1205 | 1239 | 'slug' => (string) $user->user_nicename, |
| 1206 | 1240 | 'avatarUrl' => get_avatar_url( $user->ID, array( 'size' => 48 ) ), |
| 1207 | 1241 | ); |
| 1208 | 1242 | } |