Shared folders, heartbeat widget, and heartbeat-pipeline hardening#217
Conversation
- Introduced comprehensive PHPUnit tests for the folder-sharing feature, covering schema migration, share lifecycle, capability gating, REST routes, and conflict handling. - Added tests for invite, accept, deny, revoke actions, ensuring proper functionality and user permissions. - Enhanced window system by importing necessary UI components to prevent race conditions during window creation.
…zy loading - Introduced `heartbeat.php` to register the heartbeat widget and its assets. - Created `index.ts` for the widget's functionality, utilizing PIXI.js for rendering. - Added CSS styles for the heartbeat widget in `styles.css`. - Ensured lazy loading of JavaScript and CSS assets to optimize performance. - Implemented a context menu for toggling the visibility of the heart animation.
- Implement tombstone management to ensure that revived placements do not carry lingering tombstones from previous removals, preventing UI inconsistencies. - Update the drag manager to handle synthesized clicks after drag operations, preventing unintended window minimization. - Improve the desktop shortcuts synchronization logic to ensure that server icons hidden from the desktop can be restored without requiring a page refresh. - Add comprehensive unit tests to cover new functionality and regressions related to shared folders and file placements. - Refactor heartbeat handling to ensure new files added to shared folders are correctly surfaced to recipients without requiring a refresh.
Resolves a conflict in src/desktop-files/rest.ts between trunk's "throw on null body" hardening (0b7ff8b) and this branch's "throw with first-120-chars detail on a JSON parse failure" hardening. The resolution keeps both — throws on any null body, and includes the parse-error detail + body head when text was non-empty. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
✅ WordPress Plugin Check Report
📊 ReportAll checks passed! No errors or warnings found. 🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check |
|
@claude can you review this PR? |
|
Claude finished @epeicher's task in 4m 24s —— View job PR Review: Shared folders, heartbeat widget, and heartbeat-pipeline hardening
This is a substantial, well-structured PR. The capability model, schema design, and security posture are all sound. Here is a detailed review organized by severity. Architecture & DesignThe two-table approach ( The The Issues FoundMedium1. Schema version mismatch in inline comments ( The constant is // Schema v8 (since 0.18.0): per-principal grants.Should be 2. $actor_id = isset( $row['user_id'] ) ? (int) $row['user_id'] : ( isset( $row['owner_id'] ) ? (int) $row['owner_id'] : 0 );There is no Low3. 'capability' => array( 'type' => 'string', 'default' => 'read' ),
// and
'capability' => array( 'type' => 'string', 'required' => true ),The validation is handled inside 4. The function signature at line 107 accepts a 5th 5. const this as unknown as { title: string | null } ).title ?? '';
6. Ancestor-walk query count under cold cache (
7. // Replace existing snapshot (server may have updated cap).
s.state.pending = s.state.pending.map( ( p ) => ( p.id === inv.id ? inv : p ) );
Security Observations (all clear)
Positive Observations
Overall: ready to merge with minor fixes. Items 1 (schema version comment) and 3 (enum declaration) are the cleanest to address before merging. The rest can be filed as follow-up issues. |
- Added `updated_by` field to track the user who last modified placements in the database. - Updated database insertion and update logic to accommodate the new `updated_by` field. - Improved invite handling in the shares store to prevent unnecessary notifications for unchanged invites. - Refactored modal title retrieval to ensure consistency with HTMLElement properties. - Adjusted widget liberation logic to preserve current rendered sizes instead of default sizes. - Expanded PHPUnit tests to cover new functionality, including folder deletion cascades and share revocation actions. - Implemented idempotency checks for schema installation to prevent errors on repeated calls. - Enhanced test coverage for shared folder behaviors and user capabilities.
|
@claude could you do another review? Can you focus on security, database and plugin checks? |
|
Claude finished @AllTerrainDeveloper's task in 4m 51s —— View job Security, database, and plugin check review — focused pass on behalf of @AllTerrainDeveloper.
Corrections to the previous reviewBefore the new findings: two items from the prior review were already correct in the code. Issue #1 (schema comment "v8") — The schema file at line 129 currently reads Issue #3 (missing SecurityMedium — Revoked share still visible via legacy
|
| Severity | Area | Issue |
|---|---|---|
| Medium | Security | Revoked share re-granted via legacy share_meta fallback (sharing.php:175) |
| Medium | Database | purge_folder leaves orphan rows in folder_shares / share_user_decisions (trash.php:999) |
| Low | Security | 409 actor PII visible to read-only recipients (rest.php:578) |
| Low | Security | Double-prepare() in heartbeat query (heartbeat.php:151) |
| Low | Database | dedupe_placements lacks per-operation idempotency guard (schema.php:254) |
| Low | Database | ALTER TABLE TOCTOU on concurrent workers (all ensure_* helpers) |
| Low | Plugin | Heartbeat widget CSS eagerly enqueued for all users (heartbeat.php:99) |
The two Medium items should be resolved before merge. The share visibility bypass is the most important — it silently undermines the revocation UX the entire sharing feature is built around.
- Added a per-user opt-out for the folder-sharing feature in OS settings, defaulting to enabled. - Updated the database schema to handle folder-sharing data, including purging related tables for site admins. - Enhanced the heartbeat system to respect the folder-sharing toggle, preventing invites from being sent to users who have opted out. - Implemented UI changes to reflect the new sharing settings, including a toggle in the OS settings and a confirmation dialog for purging data. - Updated tests to cover new functionality, ensuring that the sharing toggle works as expected and that purging data behaves correctly.
|
@claude could you do another review? Can you focus on security, database and plugin checks? |
|
Claude finished @AllTerrainDeveloper's task in 6m 39s —— View job Security, database & plugin-check review (third pass)
Corrections to the previous two reviewsAll six medium-and-low items raised in the prior reviews are addressed in the current code. Before the new findings: Prior Medium — Revoked share re-granted via // Non-owner viewer: the shares table is the single source
// of truth. `share_meta` on the folders row is diagnostic
// only — it is never consulted for visibility. (Earlier
// drafts had a fallback that silently re-granted access
// to revoked recipients; reviewer caught the
// revocation-bypass and we dropped the fallback before
// the feature shipped.)
$cap = desktop_mode_folder_share_user_capability( (int) $folder['id'], (int) $user_id );
$can = 'none' !== $cap;✅ Fixed before the feature shipped. The code comment explicitly acknowledges the reviewer's note. Prior Medium — // Cascade-revoke every share + per-user decision for the folder
// BEFORE deleting the folder row.
$share_ids = (array) $wpdb->get_col( ... );
if ( ! empty( $share_ids ) ) {
$wpdb->query( $wpdb->prepare( "DELETE FROM {$tables['decisions']} WHERE share_id IN ($placeholders)", ... ) );
$wpdb->query( $wpdb->prepare( "DELETE FROM {$tables['shares']} WHERE id IN ($placeholders)", ... ) );
}✅ Fixed. Prior Low — 409 conflict toast exposes actor PII to read-only recipients ( Prior Low — Double- Prior Low — Prior Low — Schema version comment saying "v8" ( New Findings — SecurityLow —
|
| Severity | Area | Item | Status |
|---|---|---|---|
| Low | Security | user_login exposed in user-search response |
New |
| Low | Security | DROP TABLE filter values not sanitized in purge endpoint |
New |
| Low | API design | URL {folder_id} ignored in share PATCH/DELETE/accept/deny |
New |
share_meta fallback |
✅ Fixed | ||
purge_folder orphan rows in shares + decisions |
✅ Fixed | ||
| ✅ Fixed | |||
prepare() in heartbeat query |
✅ Fixed | ||
dedupe_placements idempotency guard |
✅ Fixed | ||
| ✅ Fixed | |||
| Low | Plugin | Heartbeat widget CSS eagerly enqueued for all users | Remains (cosmetic) |
The two previously-identified Medium issues are both fixed. The new findings are all Low. The code is well-secured and ready to merge.
epeicher
left a comment
There was a problem hiding this comment.
Thanks for adding this great functionality @AllTerrainDeveloper, I love it! I think it can be very useful to share lists of items to review for example.
I have tested it by using different profiles on different browsers and it works perfectly, thanks for addressing all the comments.
LGTM! ![]()

Shared.folders.mov
Shared folders + Heartbeat widget
Two new features for Desktop Mode.
Shared folders
Owners can share any folder with specific users or whole roles, at
read or read+write capability. Recipients accept, deny, or leave;
their decisions are private per-user (no "first to click decides
for everyone in the role").
Capability model. Owner → write. Direct user invite or accepted
role invite → grant on the shares table. Sub-folders inherit the
parent's grant via the ancestor walker.
share_mode='all'stillworks for site-wide folders and defaults to read.
Shared namespace. Every recipient sees the same icons inside a
shared folder; only
(x, y)is per-user. Files the viewer can'tcan_read(private posts, drafts, …) ship withaccessGated: trueand render with a lock overlay + permission-explainer preview pane.
REST surface.
GET/folders/{id}/sharesPOST/folders/{id}/sharesPATCH/folders/{id}/shares/{shareId}DELETE/folders/{id}/shares/{shareId}POST/folders/{id}/shares/{shareId}/acceptPOST/folders/{id}/shares/{shareId}/denyPOST/folders/{id}/leaveGET/files/users/searchWrites optionally accept
If-Match: <updatedAtMs>and return astructured 409 with
reason/actor/currenton conflict.UI.
(
<wpd-modal>,<wpd-user-search>,<wpd-role-picker>,<wpd-segmented>for read / read+write per recipient, "Apply tosub-folders" checkbox).
folder" tile action, shared folder windows append "· Shared" to
the title.
canTrashflag — read-only recipients don't see "Move to Trash" or "Move
folder to Trash" and the recycle-bin drop target refuses their
drags.
Schema. v9 adds
wp_desktop_mode_folder_shares(polymorphic viatarget_typefor future non-folder sharing) andwp_desktop_mode_share_user_decisions(per-user state forrole-principal). Migration is idempotent.
Hooks (Experimental).
desktop_mode_files_share_{invited,accepted,denied,revoked,capability_changed,leave}.desktop_mode_files_share_{eligible_roles,can_manage,user_query_args},desktop_mode_folder_share_{accept_default_parent,user_capability}.desktop-mode/folder-share-*.Heartbeat widget
A Pixi-rendered heart that beats in sync with the WordPress
Heartbeat. Ships as its own lazy Vite bundle
(
widget-heartbeat[.min].{js,css}, ~8.7 KB JS) — PIXI loads ondemand through
wp.desktop.loadModules(['pixijs']).heartbeat-tick, with a halo glow and a brief horizontal squishduring contraction.
through the heart polygon).
re-mounts via a
createSharedStoreslot.frame and an 88 px compact frame.
OS Setting. New row in OS Settings → Features: WordPress
Heartbeat rate (15 s / 30 s / 45 s / 60 s default). Maps onto
wp.heartbeat.interval('standard' | 'slow')live and persists viathe
heartbeat_settingsfilter.Tests
@group desktop-mode-files: 125 / 360 assertions.surfaces.
npm run lint,tsc --noEmit,npm run buildclean.