Google Calendar/Meet — Pro slots + OAuth scope (free side)#625
Google Calendar/Meet — Pro slots + OAuth scope (free side)#625anik-fahmid wants to merge 60 commits into
Conversation
Free feature, always on. Lets each user connect their own Google account and attach Drive files to tasks by reference (drive.file scope). - OAuth2 connect/disconnect via WP HTTP API (no SDK); per-user tokens encrypted at rest (AES-256-CBC, key from WP salts), auto-refresh. - Admin sets site-level OAuth credentials + Picker keys (Client ID/Secret, API key, App ID). Settings_Page_Access gated. - Google Picker (client-side) for browsing under least-privilege drive.file; setOrigin + z-index/pointer-events handling for in-admin embedding. - Task detail "Google Drive" section: attach/list/detach file references. - DB tables wp_pm_google_tokens, wp_pm_google_drive_files (idempotent install + last_used_at self-heal). - 60-day stale-token purge via daily cron. - uninstall.php removes Google Workspace tables/options/cron only. - Graceful reconnect when site salts rotate (undecryptable tokens purged). - Browser guidance for third-party-cookie blocking (Chrome/Brave/Safari). Files: src/Google_Workspace/* (Loader, Google_Client, Google_Service, Models, Controllers), routes/google-workspace.php, React store slice + components/google-workspace/*, registrations in index.jsx, sidebar nav, TaskDetailSheet picker-aware outside-close guard, Create_Table + start.php bootstrap, uninstall.php.
- Move site-level OAuth credentials into Settings → Google Workspace tab (new GoogleWorkspaceSettingsTab; admin-only via the Settings page gate). - Google Workspace page is now per-user account connection only (connect/disconnect/status) + a features overview (Drive available; Calendar/Meet coming soon) so future features slot in cleanly. - One account connection powers all Google Workspace features. - Sidebar nav uses the Google Drive logo instead of the disk icon. No backend changes; reuses existing settings/status/auth endpoints.
…Google Drive (free) / Google Workspace (pro)
…hidden everywhere when off)
…ttings, full color in task/connection
…ace settings tab to top
…tion when role can't attach
…ly when allowed) + enforce detach permission
…hout Drive access (admin + frontend)
…cussion/project) + shared GoogleDriveAttach component
… cleanup on comment/task delete)
…hor or manager (UI + API)
…nt button = + with mono Drive icon
… + discussion orphan cleanup
…n (staged, attached on create)
…lined hover-reveal comment button
…idden + ungenerated variant)
…edit/delete (order: drive, edit, delete); chips render below via showAdd=false
…n; discussion/file headers show icon + count only
…edit/delete is the only comment surface
…Calendar & Meet
- G Workspace page: Calendar/Meet rows are now Slots (google.workspace.feature.{calendar,meet}); free shows a clickable Pro upgrade teaser, Pro fills them.
- Settings tab: Calendar sync + Meet groups shown as locked Pro teasers (google.workspace.settings.{calendar,meet} slots) behind the upgrade modal.
- Drive remains the free feature; Calendar/Meet reserved for Pro.
- Google_Client: add CALENDAR_SCOPE; get_auth_url() accepts a scope override.
- OAuth_Controller: auth-url honors with_calendar (requests calendar.events alongside drive.file via include_granted_scopes); status returns calendar_connected.
- Google_Service: user_has_scope()/user_has_calendar(); Loader localizes calendar_connected.
- Slice getAuthUrl({withCalendar}); expose GW thunks (fetchStatus/getAuthUrl/disconnect) on window.PM for Pro.
…ce page - Calendar is now a card section on the G Workspace page (slot google.workspace.feature.calendar), Pro fills it with sync settings + connect; free shows a Pro cover card. - Drop the Calendar/Meet teasers from the admin settings tab (creds-only again).
…; sidebar label 'Google Workspace' (free+pro) - Sidebar nav label unified to 'Google Workspace' for free and pro. - Workspace page now shows Connected services cards: Google Drive (free, status) + Calendar/Meet slots (Pro connect cards, free covers). - Calendar config (enable + sync options) moved back to Settings -> Google Workspace (admin) via the settings.calendar slot.
- Drive card gets a per-user on/off (user meta pm_gws_drive_on); user_can_use_drive respects it; status/localize expose drive_user_on; new POST google-workspace/my-prefs; saveDrivePref thunk.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (10)
💤 Files with no reviewable changes (1)
🚧 Files skipped from review as they are similar to previous changes (7)
WalkthroughAdds Google Workspace OAuth, Drive attachment, settings, and UI wiring across PHP services, REST endpoints, Redux state, and React screens. It also introduces Google link decoration, activity metadata, and attachment support in task, discussion, and comment flows. ChangesGoogle Workspace Integration
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (6)
src/Google_Workspace/Google_Service.php (1)
265-285: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winConsider chunked iteration for stale token purge.
Google_Token::all()loads every token row into memory. On sites with many users, this could cause memory pressure during the daily cron run. A chunked approach avoids loading the entire table at once.♻️ Proposed chunked iteration
public static function purge_stale( $days = 60 ) { $cutoff = Carbon::now()->subDays( $days ); $purged = 0; - $tokens = Google_Token::all(); - foreach ( $tokens as $token ) { + Google_Token::chunk( 100, function ( $tokens ) use ( $cutoff, &$purged ) { + foreach ( $tokens as $token ) { $marker = $token->last_used_at ?: $token->updated_at ?: $token->created_at; if ( ! $marker || ! Carbon::parse( $marker )->lessThan( $cutoff ) ) { continue; } $refresh = self::decrypt( $token->refresh_token ); if ( $refresh ) { self::client()->revoke( $refresh ); } $token->delete(); $purged++; - } + } + } ); return $purged; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Google_Workspace/Google_Service.php` around lines 265 - 285, The purge_stale method loads all Google_Token records into memory at once using Google_Token::all(), which can cause memory pressure on sites with many users. Replace the all() call with a chunked iteration approach that processes tokens in smaller batches instead of loading the entire table at once. This will allow the daily cron run to process stale tokens without memory issues while maintaining the same purge logic.views/assets/src/components/google-workspace/GoogleDriveAttach.jsx (1)
17-17: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winRoute notifications through the shared
useToasthook.Using the wrapper keeps toast behavior and integration consistent across screens.
As per coding guidelines, "Use custom
useToasthook for showing notifications".Also applies to: 82-82
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@views/assets/src/components/google-workspace/GoogleDriveAttach.jsx` at line 17, The GoogleDriveAttach component is directly importing toast from 'sonner' instead of using the custom useToast hook. Replace the direct import of toast from 'sonner' with an import of the useToast hook, then replace all direct toast calls (including the one around line 82) with the useToast hook invocation to maintain consistency with the codebase's notification patterns and ensure all toast behavior is properly integrated through the shared wrapper.Source: Coding guidelines
views/assets/src/components/google-workspace/DrivePickerModal.jsx (1)
13-13: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winUse the shared
useToasthook instead of importingsonnerdirectly.This keeps notification behavior consistent with the rest of the app and project conventions.
As per coding guidelines, "Use custom
useToasthook for showing notifications".Suggested refactor
-import { toast } from 'sonner' +import { useToast } from '`@hooks/useToast`' export default function DrivePickerModal({ projectId, attachableType, attachableId, attachedIds = [], onClose, onPicked }) { const dispatch = useAppDispatch() + const toast = useToast() const launchedRef = useRef(false)Also applies to: 60-60, 96-97, 143-143
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@views/assets/src/components/google-workspace/DrivePickerModal.jsx` at line 13, Replace the direct import of toast from 'sonner' with the custom useToast hook to maintain consistency with project conventions. In the DrivePickerModal component, replace the sonner import statement with an import of the useToast hook, then call this hook inside the component to get the toast function, and update all toast method calls throughout the file (including the instances at lines 60, 96-97, and 143) to use the toast function returned by the hook instead of the imported one.Source: Coding guidelines
views/assets/src/components/google-workspace/GoogleWorkspacePage.jsx (2)
16-18: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winUse
useConfirmfor the disconnect confirmation flow.The custom
AlertDialoghere should be routed through the shared confirm hook to stay consistent with the project’s confirmation contract.As per coding guidelines, "Use custom
useConfirmhook for confirmation dialogs."Also applies to: 204-225
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@views/assets/src/components/google-workspace/GoogleWorkspacePage.jsx` around lines 16 - 18, Replace the direct usage of AlertDialog components (AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction) with the shared useConfirm hook for the disconnect confirmation flow in the GoogleWorkspacePage component. Remove the AlertDialog import statement and instead import and use the useConfirm hook to handle the confirmation dialog, ensuring consistency with the project's confirmation contract and coding guidelines.Source: Coding guidelines
22-23: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winUse the shared toast abstraction for notifications.
Line 22 and the toast calls in this file bypass the project’s
useToasthook.As per coding guidelines, "Use custom
useToasthook for showing notifications."Also applies to: 93-93, 106-113
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@views/assets/src/components/google-workspace/GoogleWorkspacePage.jsx` around lines 22 - 23, Remove the direct import of toast from 'sonner' on line 22 and replace it with an import of the custom useToast hook from the project's hooks. Then, update all toast function calls throughout the GoogleWorkspacePage component (including those at lines 93 and 106-113) to use the custom useToast hook instead of calling toast directly from sonner, ensuring consistency with the project's notification abstraction pattern.Source: Coding guidelines
views/assets/src/components/admin-settings/SettingsPage.jsx (1)
48-48: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winUse an alias import for the new tab component.
Line 48 introduces a relative import, but this project’s React guidelines require alias-based paths.
Suggested change
-const GoogleWorkspaceSettingsTab = lazy(() => import('./tabs/GoogleWorkspaceSettingsTab')) +const GoogleWorkspaceSettingsTab = lazy(() => import('`@components/admin-settings/tabs/GoogleWorkspaceSettingsTab`'))As per coding guidelines, "Use route aliases in webpack:
@→src/,@components,@store,@hooks,@lib."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@views/assets/src/components/admin-settings/SettingsPage.jsx` at line 48, The import statement for GoogleWorkspaceSettingsTab uses a relative path (`./tabs/GoogleWorkspaceSettingsTab`) instead of an alias-based import as required by the project's React guidelines. Replace the relative import path in the lazy import for GoogleWorkspaceSettingsTab with the appropriate alias path using `@components` (or the correct alias mapping for where this tab component is located) to follow the project's webpack alias conventions that map `@` to `src/` and provide scoped aliases like `@components`, `@store`, `@hooks`, and `@lib`.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@routes/google-workspace.php`:
- Around line 12-62: All 15 routes in this file are missing input validation and
sanitization. For each route definition (including those calling
Settings_Controller@get, Settings_Controller@save, OAuth_Controller methods,
Drive_Controller methods, etc.), add both ->validator() and ->sanitizer() method
chains after the ->permission() call. This applies to all route types—GET, POST,
and DELETE—ensuring proper input handling and security across the entire Google
Workspace API surface.
In
`@views/assets/src/components/admin-settings/tabs/GoogleWorkspaceSettingsTab.jsx`:
- Around line 40-43: Replace the clickable div element (the one with onClick={()
=> setOpen(true)} and the border-gray-200 styling) with a shadcn/ui button
component instead. Transfer the className styling and onClick handler to the
button component. Ensure the button maintains the same visual appearance and
hover behavior as the current div, and verify that keyboard users can now
trigger the interaction using Enter or Space keys.
In `@views/assets/src/components/google-workspace/GoogleDriveAttach.jsx`:
- Around line 156-158: The remove action buttons in GoogleDriveAttach.jsx are
missing an explicit type attribute, causing them to default to type="submit" and
inadvertently submit enclosing forms when users click to remove attachments. Add
type="button" to both button elements - the one in the diff around line 156 that
calls onDetach(file.id) and the other one around line 196 (also mentioned in
"Also applies to") to ensure these buttons function as regular buttons without
form submission behavior.
In `@views/assets/src/components/layout/AppSidebar.jsx`:
- Around line 250-254: The AppSidebar component adds a new Google Workspace
navigation item with key 'google-workspace' and route '/google-workspace', but
the activeKey logic does not include handling for this new route. Find where
activeKey is computed or determined in the AppSidebar component and add a
condition to map the '/google-workspace' route to the 'google-workspace' key so
that the navigation item appears active when the user is viewing that route.
Ensure the activeKey logic includes the same route-to-key mapping pattern used
for other navigation items in the component.
In `@views/assets/src/components/projects/DiscussionsPage/index.jsx`:
- Line 74: The stagedDrive state initialized in the component is being reset
after successful discussion creation at line 138, but not when the form is
canceled or closed, allowing previously selected Drive files to persist in the
next draft. Find all cancel and close handler paths in the component and add
code to clear stagedDrive by calling setStagedDrive([]) in each of those paths,
similar to what is done after successful creation. The comment indicates this
issue also applies to another state variable at line 247, so apply the same fix
pattern there as well.
In `@views/assets/src/store/googleWorkspaceSlice.js`:
- Line 189: In the saveSettings.fulfilled case reducer, the drive_comments_on
status field is being assigned a.payload.drive_comments directly without
normalizing it to a boolean. This causes strict equality checks to fail when the
backend returns numeric values (0 or 1) instead of actual booleans. Convert
a.payload.drive_comments to a boolean value before assigning it to
s.status.drive_comments_on using a boolean coercion method like double negation
or the Boolean constructor to ensure reliable boolean comparisons throughout the
component.
---
Nitpick comments:
In `@src/Google_Workspace/Google_Service.php`:
- Around line 265-285: The purge_stale method loads all Google_Token records
into memory at once using Google_Token::all(), which can cause memory pressure
on sites with many users. Replace the all() call with a chunked iteration
approach that processes tokens in smaller batches instead of loading the entire
table at once. This will allow the daily cron run to process stale tokens
without memory issues while maintaining the same purge logic.
In `@views/assets/src/components/admin-settings/SettingsPage.jsx`:
- Line 48: The import statement for GoogleWorkspaceSettingsTab uses a relative
path (`./tabs/GoogleWorkspaceSettingsTab`) instead of an alias-based import as
required by the project's React guidelines. Replace the relative import path in
the lazy import for GoogleWorkspaceSettingsTab with the appropriate alias path
using `@components` (or the correct alias mapping for where this tab component
is located) to follow the project's webpack alias conventions that map `@` to
`src/` and provide scoped aliases like `@components`, `@store`, `@hooks`, and
`@lib`.
In `@views/assets/src/components/google-workspace/DrivePickerModal.jsx`:
- Line 13: Replace the direct import of toast from 'sonner' with the custom
useToast hook to maintain consistency with project conventions. In the
DrivePickerModal component, replace the sonner import statement with an import
of the useToast hook, then call this hook inside the component to get the toast
function, and update all toast method calls throughout the file (including the
instances at lines 60, 96-97, and 143) to use the toast function returned by the
hook instead of the imported one.
In `@views/assets/src/components/google-workspace/GoogleDriveAttach.jsx`:
- Line 17: The GoogleDriveAttach component is directly importing toast from
'sonner' instead of using the custom useToast hook. Replace the direct import of
toast from 'sonner' with an import of the useToast hook, then replace all direct
toast calls (including the one around line 82) with the useToast hook invocation
to maintain consistency with the codebase's notification patterns and ensure all
toast behavior is properly integrated through the shared wrapper.
In `@views/assets/src/components/google-workspace/GoogleWorkspacePage.jsx`:
- Around line 16-18: Replace the direct usage of AlertDialog components
(AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle,
AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction)
with the shared useConfirm hook for the disconnect confirmation flow in the
GoogleWorkspacePage component. Remove the AlertDialog import statement and
instead import and use the useConfirm hook to handle the confirmation dialog,
ensuring consistency with the project's confirmation contract and coding
guidelines.
- Around line 22-23: Remove the direct import of toast from 'sonner' on line 22
and replace it with an import of the custom useToast hook from the project's
hooks. Then, update all toast function calls throughout the GoogleWorkspacePage
component (including those at lines 93 and 106-113) to use the custom useToast
hook instead of calling toast directly from sonner, ensuring consistency with
the project's notification abstraction pattern.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 88ab8c16-d1b9-4f82-a5cd-7861ae7a3350
📒 Files selected for processing (28)
bootstrap/start.phpdb/Create_Table.phproutes/google-workspace.phpsrc/Google_Workspace/Controllers/Drive_Controller.phpsrc/Google_Workspace/Controllers/OAuth_Controller.phpsrc/Google_Workspace/Controllers/Settings_Controller.phpsrc/Google_Workspace/Google_Client.phpsrc/Google_Workspace/Google_Service.phpsrc/Google_Workspace/Loader.phpsrc/Google_Workspace/Models/Google_Drive_File.phpsrc/Google_Workspace/Models/Google_Token.phpuninstall.phpviews/assets/src/components/admin-settings/SettingsPage.jsxviews/assets/src/components/admin-settings/tabs/GoogleWorkspaceSettingsTab.jsxviews/assets/src/components/google-workspace/DrivePickerModal.jsxviews/assets/src/components/google-workspace/GoogleDriveAttach.jsxviews/assets/src/components/google-workspace/GoogleDriveCommentButton.jsxviews/assets/src/components/google-workspace/GoogleDriveStage.jsxviews/assets/src/components/google-workspace/GoogleDriveTaskSection.jsxviews/assets/src/components/google-workspace/GoogleIcons.jsxviews/assets/src/components/google-workspace/GoogleWorkspacePage.jsxviews/assets/src/components/layout/AppSidebar.jsxviews/assets/src/components/projects/DiscussionsPage/DiscussionDetailPage.jsxviews/assets/src/components/projects/DiscussionsPage/index.jsxviews/assets/src/components/tasks/TaskDetailSheet/index.jsxviews/assets/src/index.jsxviews/assets/src/store/googleWorkspaceSlice.jsviews/assets/src/store/index.js
| $wedevs_pm_router->get( 'google-workspace/settings', $gw_base . 'Settings_Controller@get' ) | ||
| ->permission( [ $gw_admin ] ); | ||
|
|
||
| $wedevs_pm_router->post( 'google-workspace/settings', $gw_base . 'Settings_Controller@save' ) | ||
| ->permission( [ $gw_admin ] ); | ||
|
|
||
| // ── Per-user connection ────────────────────────────────────────────── | ||
| $wedevs_pm_router->get( 'google-workspace/status', $gw_base . 'OAuth_Controller@status' ) | ||
| ->permission( [ $gw_auth ] ); | ||
|
|
||
| $wedevs_pm_router->get( 'google-workspace/auth-url', $gw_base . 'OAuth_Controller@auth_url' ) | ||
| ->permission( [ $gw_auth ] ); | ||
|
|
||
| $wedevs_pm_router->post( 'google-workspace/disconnect', $gw_base . 'OAuth_Controller@disconnect' ) | ||
| ->permission( [ $gw_auth ] ); | ||
|
|
||
| $wedevs_pm_router->post( 'google-workspace/my-prefs', $gw_base . 'OAuth_Controller@save_my_prefs' ) | ||
| ->permission( [ $gw_auth ] ); | ||
|
|
||
| // ── Drive Picker config (vends caller's own access token + Picker keys) ── | ||
| $wedevs_pm_router->get( 'google-workspace/drive/picker-config', $gw_base . 'Drive_Controller@picker_config' ) | ||
| ->permission( [ $gw_auth ] ); | ||
|
|
||
| // ── Per-project Drive role access (manager configures; members query) ── | ||
| $gw_manager = 'WeDevs\PM\Core\Permissions\Project_Manage_Capability'; | ||
|
|
||
| $wedevs_pm_router->get( 'projects/{project_id}/google-workspace/access', $gw_base . 'Drive_Controller@get_access' ) | ||
| ->permission( [ $gw_manager ] ); | ||
|
|
||
| $wedevs_pm_router->post( 'projects/{project_id}/google-workspace/access', $gw_base . 'Drive_Controller@save_access' ) | ||
| ->permission( [ $gw_manager ] ); | ||
|
|
||
| $wedevs_pm_router->get( 'projects/{project_id}/google-workspace/can-use', $gw_base . 'Drive_Controller@can_use' ) | ||
| ->permission( [ $gw_access ] ); | ||
|
|
||
| // ── Drive attachments (polymorphic: task, comment, discussion, project) ── | ||
| $wedevs_pm_router->get( 'projects/{project_id}/google-drive', $gw_base . 'Drive_Controller@index' ) | ||
| ->permission( [ $gw_access ] ); | ||
|
|
||
| $wedevs_pm_router->post( 'projects/{project_id}/google-drive', $gw_base . 'Drive_Controller@attach' ) | ||
| ->permission( [ $gw_access ] ); | ||
|
|
||
| $wedevs_pm_router->delete( 'projects/{project_id}/google-drive/{id}', $gw_base . 'Drive_Controller@destroy' ) | ||
| ->permission( [ $gw_access ] ); | ||
|
|
||
| // Legacy task-scoped routes (kept for back-compat). | ||
| $wedevs_pm_router->get( 'projects/{project_id}/tasks/{task_id}/google-drive', $gw_base . 'Drive_Controller@index' ) | ||
| ->permission( [ $gw_access ] ); | ||
|
|
||
| $wedevs_pm_router->post( 'projects/{project_id}/tasks/{task_id}/google-drive', $gw_base . 'Drive_Controller@attach' ) | ||
| ->permission( [ $gw_access ] ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if existing route files use validator/sanitizer methods
rg -n '\->validator\(' routes/ --type php | head -20
rg -n '\->sanitizer\(' routes/ --type php | head -20Repository: weDevsOfficial/wp-project-manager
Length of output: 3407
🏁 Script executed:
cat -n routes/google-workspace.phpRepository: weDevsOfficial/wp-project-manager
Length of output: 3442
Missing ->validator() and ->sanitizer() on all endpoints.
Every REST endpoint must include both validator and sanitizer methods. All 15 routes in this file are missing these—including POST operations (settings, disconnect, my-prefs, access, attach) and DELETE (destroy), which especially require input validation.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@routes/google-workspace.php` around lines 12 - 62, All 15 routes in this file
are missing input validation and sanitization. For each route definition
(including those calling Settings_Controller@get, Settings_Controller@save,
OAuth_Controller methods, Drive_Controller methods, etc.), add both
->validator() and ->sanitizer() method chains after the ->permission() call.
This applies to all route types—GET, POST, and DELETE—ensuring proper input
handling and security across the entire Google Workspace API surface.
Source: Coding guidelines
| <div | ||
| className="mb-4 flex items-center justify-between rounded-lg border border-gray-200 bg-white p-4 cursor-pointer hover:border-gray-300" | ||
| onClick={() => setOpen(true)} | ||
| > |
There was a problem hiding this comment.
Make the locked card trigger keyboard-accessible.
Line 40 uses a clickable div; keyboard users can’t reliably trigger it. Use a semantic button (or shadcn button wrapper) for this interaction.
Suggested change
- <div
+ <button
+ type="button"
className="mb-4 flex items-center justify-between rounded-lg border border-gray-200 bg-white p-4 cursor-pointer hover:border-gray-300"
onClick={() => setOpen(true)}
>
@@
- </div>
+ </button>As per coding guidelines, "Use shadcn/ui components (Radix primitives) for UI elements."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div | |
| className="mb-4 flex items-center justify-between rounded-lg border border-gray-200 bg-white p-4 cursor-pointer hover:border-gray-300" | |
| onClick={() => setOpen(true)} | |
| > | |
| <button | |
| type="button" | |
| className="mb-4 flex items-center justify-between rounded-lg border border-gray-200 bg-white p-4 cursor-pointer hover:border-gray-300" | |
| onClick={() => setOpen(true)} | |
| > |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@views/assets/src/components/admin-settings/tabs/GoogleWorkspaceSettingsTab.jsx`
around lines 40 - 43, Replace the clickable div element (the one with
onClick={() => setOpen(true)} and the border-gray-200 styling) with a shadcn/ui
button component instead. Transfer the className styling and onClick handler to
the button component. Ensure the button maintains the same visual appearance and
hover behavior as the current div, and verify that keyboard users can now
trigger the interaction using Enter or Space keys.
Source: Coding guidelines
| <button onClick={() => onDetach(file.id)} className="text-gray-400 hover:text-red-600" title={__('Remove', 'wedevs-project-manager')}> | ||
| <X className="h-3 w-3" /> | ||
| </button> |
There was a problem hiding this comment.
Set explicit type="button" on remove actions.
Line 156 and Line 196 currently use default button type, which can submit enclosing forms when users try to remove an attachment.
Suggested fix
- <button onClick={() => onDetach(file.id)} className="text-gray-400 hover:text-red-600" title={__('Remove', 'wedevs-project-manager')}>
+ <button type="button" onClick={() => onDetach(file.id)} className="text-gray-400 hover:text-red-600" title={__('Remove', 'wedevs-project-manager')}>
<X className="h-3 w-3" />
</button>
...
- <button onClick={() => onDetach(file.id)} className="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-red-600" title={__('Remove', 'wedevs-project-manager')}>
+ <button type="button" onClick={() => onDetach(file.id)} className="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-red-600" title={__('Remove', 'wedevs-project-manager')}>
<Trash2 className="h-3.5 w-3.5" />
</button>Also applies to: 196-198
🧰 Tools
🪛 ast-grep (0.44.0)
[warning] 156-156: A list component should have a key to prevent re-rendering
Context:
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.
(list-component-needs-key)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@views/assets/src/components/google-workspace/GoogleDriveAttach.jsx` around
lines 156 - 158, The remove action buttons in GoogleDriveAttach.jsx are missing
an explicit type attribute, causing them to default to type="submit" and
inadvertently submit enclosing forms when users click to remove attachments. Add
type="button" to both button elements - the one in the diff around line 156 that
calls onDetach(file.id) and the other one around line 196 (also mentioned in
"Also applies to") to ensure these buttons function as regular buttons without
form submission behavior.
| .addCase(fetchSettings.rejected, (s) => { s.settingsLoading = false }) | ||
|
|
||
| .addCase(saveSettings.pending, (s) => { s.saving = true }) | ||
| .addCase(saveSettings.fulfilled, (s, a) => { s.saving = false; s.settings = a.payload; s.status.configured = a.payload.configured; s.status.picker_ready = a.payload.picker_ready; s.status.drive_enabled = a.payload.drive_enabled; s.status.drive_comments_on = a.payload.drive_comments }) |
There was a problem hiding this comment.
Normalize drive_comments_on to a boolean when saving settings.
Line 189 stores a.payload.drive_comments verbatim. If this is 0/1, strict checks like status.drive_comments_on === false will misbehave and show comment-drive UI when disabled.
Suggested fix
- .addCase(saveSettings.fulfilled, (s, a) => { s.saving = false; s.settings = a.payload; s.status.configured = a.payload.configured; s.status.picker_ready = a.payload.picker_ready; s.status.drive_enabled = a.payload.drive_enabled; s.status.drive_comments_on = a.payload.drive_comments })
+ .addCase(saveSettings.fulfilled, (s, a) => {
+ s.saving = false
+ s.settings = a.payload
+ s.status.configured = !!a.payload.configured
+ s.status.picker_ready = !!a.payload.picker_ready
+ s.status.drive_enabled = !!a.payload.drive_enabled
+ s.status.drive_comments_on = !!a.payload.drive_comments
+ })📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .addCase(saveSettings.fulfilled, (s, a) => { s.saving = false; s.settings = a.payload; s.status.configured = a.payload.configured; s.status.picker_ready = a.payload.picker_ready; s.status.drive_enabled = a.payload.drive_enabled; s.status.drive_comments_on = a.payload.drive_comments }) | |
| .addCase(saveSettings.fulfilled, (s, a) => { | |
| s.saving = false | |
| s.settings = a.payload | |
| s.status.configured = !!a.payload.configured | |
| s.status.picker_ready = !!a.payload.picker_ready | |
| s.status.drive_enabled = !!a.payload.drive_enabled | |
| s.status.drive_comments_on = !!a.payload.drive_comments | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@views/assets/src/store/googleWorkspaceSlice.js` at line 189, In the
saveSettings.fulfilled case reducer, the drive_comments_on status field is being
assigned a.payload.drive_comments directly without normalizing it to a boolean.
This causes strict equality checks to fail when the backend returns numeric
values (0 or 1) instead of actual booleans. Convert a.payload.drive_comments to
a boolean value before assigning it to s.status.drive_comments_on using a
boolean coercion method like double negation or the Boolean constructor to
ensure reliable boolean comparisons throughout the component.
…d) + meet_connected status; getAuthUrl supports withMeet
…sion comment composers
…ng (was fatal -> 500 on add comment)
… comments (was attached but not rendered)
…composers (new + edit); drop chips + per-comment attach button (CommentLinkActions)
…oji broke save on utf8mb3 comment column)
…eting to discussion create form
…tLinkActions); allowMeet flag
…ng (viewBox padding) to align with label
…me composer unchanged
…chrome marks in activity feeds
…le-type icons + Meet card); expose decorateGoogleLinks on window.PM
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (1)
views/assets/src/store/googleWorkspaceSlice.js (1)
191-191: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winNormalize boolean fields when applying
saveSettingspayload.
s.status.drive_comments_on = a.payload.drive_comments(and the adjacentconfigured/picker_ready/drive_enabledassignments) store the raw payload values. If the backend returns0/1, strict checks like=== falsewill misbehave. Coerce with!!before assigning.Suggested fix
- .addCase(saveSettings.fulfilled, (s, a) => { s.saving = false; s.settings = a.payload; s.status.configured = a.payload.configured; s.status.picker_ready = a.payload.picker_ready; s.status.drive_enabled = a.payload.drive_enabled; s.status.drive_comments_on = a.payload.drive_comments }) + .addCase(saveSettings.fulfilled, (s, a) => { + s.saving = false + s.settings = a.payload + s.status.configured = !!a.payload.configured + s.status.picker_ready = !!a.payload.picker_ready + s.status.drive_enabled = !!a.payload.drive_enabled + s.status.drive_comments_on = !!a.payload.drive_comments + })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@views/assets/src/store/googleWorkspaceSlice.js` at line 191, The saveSettings.fulfilled handler in googleWorkspaceSlice is assigning raw payload values into s.status fields, so boolean flags like configured, picker_ready, drive_enabled, and drive_comments_on can end up as 0/1 instead of true/false. Update the reducer case to coerce each of those payload properties to booleans before storing them, keeping the behavior consistent with strict boolean checks elsewhere.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/Activity/Transformers/Activity_Transformer.php`:
- Around line 155-157: In parse_meta_for_comment() within Activity_Transformer,
the early type check is causing the method to return immediately when it
receives an Activity object, so the has_drive and has_meet flags are never
added. Update the guard to handle the actual Activity instance (or its meta
payload) instead of requiring an array, and make sure the existing Google
Workspace flag logic for $activity->meta['has_drive'] and
$activity->meta['has_meet'] can run before returning the meta response.
In `@src/Comment/Observers/Comment_Observer.php`:
- Around line 76-93: The new Drive/Meet link metadata is only being added in
comment_on_task(), so other comment activity paths never receive has_drive /
has_meet. Update the shared comment activity flow in Comment_Observer so
gw_meta() is merged into the meta for all non-delete comment actions, including
discussions, milestones, projects, files, and task lists, while still skipping
deleted comments. Use the existing gw_meta() helper and the comment_on_task()
pattern to locate the right place to centralize this.
In `@src/Google_Workspace/Controllers/OAuth_Controller.php`:
- Line 27: The meet_connected value is using the calendar check, so the Meet
connection state will always mirror Calendar. Update the OAuth_Controller
response building logic to call Google_Service::user_has_meet() for
meet_connected instead of Google_Service::user_has_calendar(), keeping the
surrounding user status fields unchanged.
In `@views/assets/src/components/google-workspace/DriveCommentInsert.jsx`:
- Around line 22-26: The linkHtml function is building HTML with unescaped Drive
metadata, which can allow injected markup or attributes. Update linkHtml in
DriveCommentInsert.jsx to escape/sanitize both the href value from f.webViewLink
or f.url and the displayed label from f.name before interpolating them into the
anchor string. Keep the existing fallback to __('Drive file',
'wedevs-project-manager') and ensure any helper used for escaping is applied
consistently in this component.
In `@views/assets/src/components/projects/DiscussionsPage/index.jsx`:
- Line 247: The attachment staging flow in DiscussionsPage is broken because
CommentLinkActions only passes HTML through onInsert, so stagedDrive never gets
populated and the attachFileFor loop never runs. Update the DiscussionsPage
comment composer to also receive selected files from CommentLinkActions and wire
that data into setStagedDrive alongside setFormDesc, ensuring the existing
attachFileFor handling can persist Drive attachments.
In `@views/assets/src/lib/google-links.js`:
- Around line 64-71: The Google Meet card in decorateGoogleLinks() is being
rebuilt with innerHTML, which can reintroduce decoded markup from a.textContent
and create XSS risk. Replace the innerHTML construction for the meet card with
DOM-based creation using doc.createElement, textContent, append, and
setAttribute, and keep the existing meet link replacement behavior in the same
meet.google.com branch.
---
Duplicate comments:
In `@views/assets/src/store/googleWorkspaceSlice.js`:
- Line 191: The saveSettings.fulfilled handler in googleWorkspaceSlice is
assigning raw payload values into s.status fields, so boolean flags like
configured, picker_ready, drive_enabled, and drive_comments_on can end up as 0/1
instead of true/false. Update the reducer case to coerce each of those payload
properties to booleans before storing them, keeping the behavior consistent with
strict boolean checks elsewhere.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b04787a5-550e-43eb-a5dc-0596630aaa51
📒 Files selected for processing (22)
src/Activity/Transformers/Activity_Transformer.phpsrc/Comment/Observers/Comment_Observer.phpsrc/Google_Workspace/Controllers/Drive_Controller.phpsrc/Google_Workspace/Controllers/OAuth_Controller.phpsrc/Google_Workspace/Google_Client.phpsrc/Google_Workspace/Google_Service.phpsrc/Google_Workspace/Loader.phpviews/assets/src/components/admin-settings/tabs/GoogleWorkspaceSettingsTab.jsxviews/assets/src/components/google-workspace/CommentLinkActions.jsxviews/assets/src/components/google-workspace/DriveCommentInsert.jsxviews/assets/src/components/google-workspace/GoogleIcons.jsxviews/assets/src/components/google-workspace/GoogleWorkspacePage.jsxviews/assets/src/components/layout/AppSidebar.jsxviews/assets/src/components/projects/ActivitiesPage/constants.jsviews/assets/src/components/projects/ActivitiesPage/parts/ActivityItem.jsxviews/assets/src/components/projects/DiscussionsPage/DiscussionDetailPage.jsxviews/assets/src/components/projects/DiscussionsPage/index.jsxviews/assets/src/components/tasks/SingleTaskListPage.jsxviews/assets/src/components/tasks/TaskDetailSheet/index.jsxviews/assets/src/index.jsxviews/assets/src/lib/google-links.jsviews/assets/src/store/googleWorkspaceSlice.js
✅ Files skipped from review due to trivial changes (2)
- views/assets/src/components/tasks/SingleTaskListPage.jsx
- views/assets/src/components/google-workspace/CommentLinkActions.jsx
🚧 Files skipped from review as they are similar to previous changes (8)
- views/assets/src/components/google-workspace/GoogleIcons.jsx
- views/assets/src/components/layout/AppSidebar.jsx
- views/assets/src/index.jsx
- views/assets/src/components/google-workspace/GoogleWorkspacePage.jsx
- src/Google_Workspace/Google_Client.php
- src/Google_Workspace/Controllers/Drive_Controller.php
- src/Google_Workspace/Loader.php
- src/Google_Workspace/Google_Service.php
| // Google Workspace: flag comments that contain a Drive/Meet link. | ||
| if ( ! empty( $activity->meta['has_drive'] ) ) { $meta['has_drive'] = true; } | ||
| if ( ! empty( $activity->meta['has_meet'] ) ) { $meta['has_meet'] = true; } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
parse_meta_for_comment() returns before these flags ever run.
Line 135 checks is_array( $activity ), but this method is passed an Activity object. That means the function exits immediately and the new has_drive / has_meet fields never reach the response.
Suggested fix
- if ( ! is_array( $activity ) ) {
+ if ( ! is_array( $activity->meta ) ) {
return $meta;
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Activity/Transformers/Activity_Transformer.php` around lines 155 - 157,
In parse_meta_for_comment() within Activity_Transformer, the early type check is
causing the method to return immediately when it receives an Activity object, so
the has_drive and has_meet flags are never added. Update the guard to handle the
actual Activity instance (or its meta payload) instead of requiring an array,
and make sure the existing Google Workspace flag logic for
$activity->meta['has_drive'] and $activity->meta['has_meet'] can run before
returning the meta response.
| /** Flags whether a comment body contains a Drive / Meet link (for activity icons). */ | ||
| private function gw_meta( Comment $comment ) { | ||
| $content = (string) $comment->content; | ||
| return [ | ||
| 'has_drive' => ( strpos( $content, 'drive.google.com' ) !== false || strpos( $content, 'docs.google.com' ) !== false ), | ||
| 'has_meet' => ( strpos( $content, 'meet.google.com' ) !== false ), | ||
| ]; | ||
| } | ||
|
|
||
| private function comment_on_task( Comment $comment, Task $task, $action_type ) { | ||
| $meta = [ | ||
| 'comment_id' => $comment->id, | ||
| 'task_title' => $task->title, | ||
| ]; | ||
| // Don't mark a deleted comment with Drive/Meet icons. | ||
| if ( $action_type !== 'delete' ) { | ||
| $meta = array_merge( $meta, $this->gw_meta( $comment ) ); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
The new Drive/Meet metadata is only attached for task comments.
gw_meta() is introduced as a generic comment-link detector, but Lines 91-92 only merge it in comment_on_task(). Comment activities for discussions, milestones, projects, files, and task lists will never carry has_drive / has_meet, so the new badges/icons stay inconsistent across comment types.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Comment/Observers/Comment_Observer.php` around lines 76 - 93, The new
Drive/Meet link metadata is only being added in comment_on_task(), so other
comment activity paths never receive has_drive / has_meet. Update the shared
comment activity flow in Comment_Observer so gw_meta() is merged into the meta
for all non-delete comment actions, including discussions, milestones, projects,
files, and task lists, while still skipping deleted comments. Use the existing
gw_meta() helper and the comment_on_task() pattern to locate the right place to
centralize this.
| 'account_email' => $conn['account_email'], | ||
| 'expired' => $conn['expired'], | ||
| 'calendar_connected'=> Google_Service::user_has_calendar( get_current_user_id() ), | ||
| 'meet_connected' => Google_Service::user_has_calendar( get_current_user_id() ), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -nP 'function\s+user_has_meet' src/Google_Workspace/Google_Service.php -C2Repository: weDevsOfficial/wp-project-manager
Length of output: 404
🏁 Script executed:
#!/bin/bash
sed -n '1,120p' src/Google_Workspace/Controllers/OAuth_Controller.phpRepository: weDevsOfficial/wp-project-manager
Length of output: 3220
🏁 Script executed:
#!/bin/bash
sed -n '130,155p' src/Google_Workspace/Google_Service.phpRepository: weDevsOfficial/wp-project-manager
Length of output: 1157
meet_connected should use the Meet helper. src/Google_Workspace/Controllers/OAuth_Controller.php:27 currently calls Google_Service::user_has_calendar() here, so the Meet status will mirror Calendar. Use Google_Service::user_has_meet() instead.
Suggested change
- 'meet_connected' => Google_Service::user_has_calendar( get_current_user_id() ),
+ 'meet_connected' => Google_Service::user_has_meet( get_current_user_id() ),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 'meet_connected' => Google_Service::user_has_calendar( get_current_user_id() ), | |
| 'meet_connected' => Google_Service::user_has_meet( get_current_user_id() ), |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Google_Workspace/Controllers/OAuth_Controller.php` at line 27, The
meet_connected value is using the calendar check, so the Meet connection state
will always mirror Calendar. Update the OAuth_Controller response building logic
to call Google_Service::user_has_meet() for meet_connected instead of
Google_Service::user_has_calendar(), keeping the surrounding user status fields
unchanged.
| </SelectContent> | ||
| </Select> | ||
| <FileUploadArea files={formFiles} onFilesChange={setFormFiles} /> | ||
| <CommentLinkActions projectId={projectId} onInsert={(html) => setFormDesc(prev => (prev || '') + html)} /> |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
stagedDrive never gets populated after this swap.
CommentLinkActions only calls onInsert(html). It has no way to feed selected files into setStagedDrive, so the attachFileFor(...) loop at Lines 129-132 never runs and new discussions won't persist Drive attachments as attachment records.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@views/assets/src/components/projects/DiscussionsPage/index.jsx` at line 247,
The attachment staging flow in DiscussionsPage is broken because
CommentLinkActions only passes HTML through onInsert, so stagedDrive never gets
populated and the attachFileFor loop never runs. Update the DiscussionsPage
comment composer to also receive selected files from CommentLinkActions and wire
that data into setStagedDrive alongside setFormDesc, ensuring the existing
attachFileFor handling can persist Drive attachments.
…r_has_meet/with_meet) — Meet uses calendar.events
…endering — DOM-build Meet card with textContent, escape + http(s)-validate inserted Drive/Meet links
There was a problem hiding this comment.
🧹 Nitpick comments (1)
views/assets/src/lib/google-links.js (1)
104-108: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low valueDrive/Docs anchors keep an unvalidated
href.Unlike the Meet branch (which routes the CTA through
safeHttpUrl), the Drive/Docs branch only adds classes and an icon while leaving the originalhrefuntouched. The match relies on a substring regex onhref, so a target likejavascript:'drive.google.com'still passes Line 104 and survives decoration. The href is pre-existing input (not introduced here), so this isn't a new injection vector, but applyingsafeHttpUrlhere would make link validation consistent with the stated PR intent.🛡️ Optional consistency fix
if (/drive\.google\.com|docs\.google\.com/.test(href)) { + const safeHref = safeHttpUrl(href) + if (safeHref) a.setAttribute('href', safeHref) const kind = driveKind(href, text) a.classList.add('pm-gdrive-link', 'inline-flex', 'items-center', 'gap-1', 'align-middle') a.insertAdjacentHTML('afterbegin', iconHtml(kind)) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@views/assets/src/lib/google-links.js` around lines 104 - 108, The Drive/Docs branch in google-links.js leaves the existing anchor href unchanged after matching on /drive\.google\.com|docs\.google\.com/, so unsafe URLs can survive decoration. Update the google links handling so the Drive/Docs path also routes the target through safeHttpUrl, similar to the Meet branch, and only keep the decorated anchor when the sanitized URL is valid. Use the driveKind helper and the existing anchor mutation logic as the place to apply this consistency fix.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@views/assets/src/lib/google-links.js`:
- Around line 104-108: The Drive/Docs branch in google-links.js leaves the
existing anchor href unchanged after matching on
/drive\.google\.com|docs\.google\.com/, so unsafe URLs can survive decoration.
Update the google links handling so the Drive/Docs path also routes the target
through safeHttpUrl, similar to the Meet branch, and only keep the decorated
anchor when the sanitized URL is valid. Use the driveKind helper and the
existing anchor mutation logic as the place to apply this consistency fix.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c9ae6edc-fa73-44db-97b0-7344194740ef
📒 Files selected for processing (6)
src/Google_Workspace/Controllers/OAuth_Controller.phpsrc/Google_Workspace/Google_Client.phpsrc/Google_Workspace/Google_Service.phpviews/assets/src/components/google-workspace/DriveCommentInsert.jsxviews/assets/src/lib/google-links.jsviews/assets/src/store/googleWorkspaceSlice.js
💤 Files with no reviewable changes (4)
- src/Google_Workspace/Controllers/OAuth_Controller.php
- src/Google_Workspace/Google_Client.php
- views/assets/src/store/googleWorkspaceSlice.js
- src/Google_Workspace/Google_Service.php
🚧 Files skipped from review as they are similar to previous changes (1)
- views/assets/src/components/google-workspace/DriveCommentInsert.jsx
…; sidebar any-feature gate + reactive; active-route highlight for /google-workspace
…ard off-state; align access route perms to Project_Settings_Page_Access; localize calendar/meet master flags; drop dead Meet scope
🔒 Security fix — Google Drive cross-project IDOR (High)This branch carries the same Fix: (I can't push to your fork, so the fix is attached as a patch below — apply with diff --git a/src/Google_Workspace/Controllers/Drive_Controller.php b/src/Google_Workspace/Controllers/Drive_Controller.php
index 620464b18..360cd6db7 100644
--- a/src/Google_Workspace/Controllers/Drive_Controller.php
+++ b/src/Google_Workspace/Controllers/Drive_Controller.php
@@ -4,6 +4,10 @@ namespace WeDevs\PM\Google_Workspace\Controllers;
use WP_REST_Request;
use WeDevs\PM\Google_Workspace\Google_Service;
use WeDevs\PM\Google_Workspace\Models\Google_Drive_File;
+use WeDevs\PM\Task\Models\Task;
+use WeDevs\PM\Comment\Models\Comment;
+use WeDevs\PM\Discussion_Board\Models\Discussion_Board;
+use WeDevs\PM\File\Models\File;
use Carbon\Carbon;
if ( ! defined( 'ABSPATH' ) ) exit;
@@ -61,6 +65,42 @@ class Drive_Controller {
return [ $type, $id ];
}
+ /**
+ * Resolve the real project that owns a polymorphic attachable target.
+ * Returns 0 when the target does not exist or the type is not verifiable.
+ */
+ private function attachable_project_id( $type, $id ) {
+ $id = (int) $id;
+
+ if ( ! $id ) {
+ return 0;
+ }
+
+ switch ( $type ) {
+ case 'task':
+ $model = Task::find( $id );
+ return $model ? (int) $model->project_id : 0;
+
+ case 'comment':
+ $model = Comment::find( $id );
+ return $model ? (int) $model->project_id : 0;
+
+ case 'discussion':
+ $model = Discussion_Board::find( $id );
+ return $model ? (int) $model->project_id : 0;
+
+ case 'file':
+ $model = File::find( $id );
+ return $model ? (int) $model->project_id : 0;
+
+ case 'project':
+ return $id;
+
+ default:
+ return 0;
+ }
+ }
+
/**
* For comment attachments, restrict add/remove to the comment author or a
* project manager (mirrors who can edit the comment). Other entity types
@@ -84,14 +124,21 @@ class Drive_Controller {
}
public function index( WP_REST_Request $request ) {
+ $project_id = (int) $request->get_param( 'project_id' );
list( $type, $id ) = $this->resolve_attachable( $request );
if ( $type === '' ) {
return [ 'data' => [] ];
}
+ $owner_project_id = $this->attachable_project_id( $type, $id );
+ if ( ! $owner_project_id || $owner_project_id !== $project_id ) {
+ return new \WP_Error( 'pm_google_forbidden', __( 'This attachment target does not belong to the current project.', 'wedevs-project-manager' ), [ 'status' => 403 ] );
+ }
+
$files = Google_Drive_File::where( 'attachable_type', $type )
->where( 'attachable_id', $id )
+ ->where( 'project_id', $project_id )
->orderBy( 'created_at', 'desc' )
->get();
@@ -134,6 +181,11 @@ class Drive_Controller {
return new \WP_Error( 'pm_google_bad_request', __( 'Invalid attachment target.', 'wedevs-project-manager' ), [ 'status' => 422 ] );
}
+ $owner_project_id = $this->attachable_project_id( $type, $id );
+ if ( ! $owner_project_id || $owner_project_id !== $project_id ) {
+ return new \WP_Error( 'pm_google_forbidden', __( 'This attachment target does not belong to the current project.', 'wedevs-project-manager' ), [ 'status' => 403 ] );
+ }
+
if ( $type === 'comment' && ! Google_Service::drive_comments_enabled() ) {
return new \WP_Error( 'pm_google_forbidden', __( 'Google Drive in comments is turned off.', 'wedevs-project-manager' ), [ 'status' => 403 ] );
} |
…625) (#634) * feat(google-workspace): add Google Drive integration (OAuth + Picker) Free feature, always on. Lets each user connect their own Google account and attach Drive files to tasks by reference (drive.file scope). - OAuth2 connect/disconnect via WP HTTP API (no SDK); per-user tokens encrypted at rest (AES-256-CBC, key from WP salts), auto-refresh. - Admin sets site-level OAuth credentials + Picker keys (Client ID/Secret, API key, App ID). Settings_Page_Access gated. - Google Picker (client-side) for browsing under least-privilege drive.file; setOrigin + z-index/pointer-events handling for in-admin embedding. - Task detail "Google Drive" section: attach/list/detach file references. - DB tables wp_pm_google_tokens, wp_pm_google_drive_files (idempotent install + last_used_at self-heal). - 60-day stale-token purge via daily cron. - uninstall.php removes Google Workspace tables/options/cron only. - Graceful reconnect when site salts rotate (undecryptable tokens purged). - Browser guidance for third-party-cookie blocking (Chrome/Brave/Safari). Files: src/Google_Workspace/* (Loader, Google_Client, Google_Service, Models, Controllers), routes/google-workspace.php, React store slice + components/google-workspace/*, registrations in index.jsx, sidebar nav, TaskDetailSheet picker-aware outside-close guard, Create_Table + start.php bootstrap, uninstall.php. * refactor(google-workspace): split admin setup from per-user connection - Move site-level OAuth credentials into Settings → Google Workspace tab (new GoogleWorkspaceSettingsTab; admin-only via the Settings page gate). - Google Workspace page is now per-user account connection only (connect/disconnect/status) + a features overview (Drive available; Calendar/Meet coming soon) so future features slot in cleanly. - One account connection powers all Google Workspace features. - Sidebar nav uses the Google Drive logo instead of the disk icon. No backend changes; reuses existing settings/status/auth endpoints. * style(google-workspace): monochrome nav/settings logo; sidebar label Google Drive (free) / Google Workspace (pro) * feat(google-workspace): admin toggle to enable/disable Google Drive (hidden everywhere when off) * style(google-workspace): literal G Drive/G Workspace labels + outlined nav icons * style(google-workspace): refine monochrome Drive nav/settings glyph * style(google-workspace): adopt 2026 Drive icon — monochrome in nav/settings, full color in task/connection * style(google-workspace): outline the 2026 Drive icon in nav/settings * feat(google-workspace): per-project Drive role access + move G Workspace settings tab to top * feat(google-workspace): clearer 'View only' message in task Drive section when role can't attach * fix(google-workspace): strict role gating on frontend (show Attach only when allowed) + enforce detach permission * fix(google-workspace): no Connect prompt / hide section for roles without Drive access (admin + frontend) * refactor(google-workspace): polymorphic attachments (task/comment/discussion/project) + shared GoogleDriveAttach component * feat(google-workspace): attach Drive files to task comments (+ orphan cleanup on comment/task delete) * fix(google-workspace): comment Drive attach restricted to comment author or manager (UI + API) * style(google-workspace): section button label 'Attach'; compact comment button = + with mono Drive icon * feat(google-workspace): Drive attach in discussions (body + comments) + discussion orphan cleanup * feat(google-workspace): attach Drive files while creating a discussion (staged, attached on create) * feat(google-workspace): expose Drive components via window.PM for pro * feat(google-workspace): allow 'file' attachable type for Drive attachments * feat(google-workspace): clamp list to 2 + Show all; adder avatar; outlined hover-reveal comment button * fix(google-workspace): comment Drive button reveal via opacity (was hidden + ungenerated variant) * feat(google-workspace): comment Drive add as icon-only button beside edit/delete (order: drive, edit, delete); chips render below via showAdd=false * feat(google-workspace): show 'Google Drive' label only in task section; discussion/file headers show icon + count only * feat(google-workspace): drop comment Drive chips; icon button beside edit/delete is the only comment surface * feat(google-workspace): add Pro extension slots + upgrade covers for Calendar & Meet - G Workspace page: Calendar/Meet rows are now Slots (google.workspace.feature.{calendar,meet}); free shows a clickable Pro upgrade teaser, Pro fills them. - Settings tab: Calendar sync + Meet groups shown as locked Pro teasers (google.workspace.settings.{calendar,meet} slots) behind the upgrade modal. - Drive remains the free feature; Calendar/Meet reserved for Pro. * feat(google-workspace): incremental Calendar-scope consent infra (free) - Google_Client: add CALENDAR_SCOPE; get_auth_url() accepts a scope override. - OAuth_Controller: auth-url honors with_calendar (requests calendar.events alongside drive.file via include_granted_scopes); status returns calendar_connected. - Google_Service: user_has_scope()/user_has_calendar(); Loader localizes calendar_connected. - Slice getAuthUrl({withCalendar}); expose GW thunks (fetchStatus/getAuthUrl/disconnect) on window.PM for Pro. * feat(google-workspace): move Calendar config to the sidebar G Workspace page - Calendar is now a card section on the G Workspace page (slot google.workspace.feature.calendar), Pro fills it with sync settings + connect; free shows a Pro cover card. - Drop the Calendar/Meet teasers from the admin settings tab (creds-only again). * feat(google-workspace): Workspace page = per-feature connection cards; sidebar label 'Google Workspace' (free+pro) - Sidebar nav label unified to 'Google Workspace' for free and pro. - Workspace page now shows Connected services cards: Google Drive (free, status) + Calendar/Meet slots (Pro connect cards, free covers). - Calendar config (enable + sync options) moved back to Settings -> Google Workspace (admin) via the settings.calendar slot. * feat(google-workspace): per-user service prefs on Workspace page - Drive card gets a per-user on/off (user meta pm_gws_drive_on); user_can_use_drive respects it; status/localize expose drive_user_on; new POST google-workspace/my-prefs; saveDrivePref thunk. * feat(google-workspace): note that all features use one account; reconnecting a different account replaces it * feat(google-workspace): fire pm_google_before_disconnect (token still valid) for feature cleanup * feat(google-workspace): disconnect confirm modal (plugin AlertDialog, wider) explaining consequences * feat(google-workspace): G Workspace sidebar label; smaller settings desc + setup docs link; admin toggle to disable Drive in comments (auto-save+toast, enforced UI+API); branded Calendar/Meet icons in free teasers * fix(google-workspace): use plugin ProBadge for teasers; neutral calendar connect prompt; docs link icon-only top-right * fix(google-workspace): settings tab — G Workspace heading, shorter desc, credentials heading, icon-less feature toggles; trim account note 2nd line * fix(google-workspace): settings — Google glyph + 'Google Workspace' heading, larger credentials heading, grouped Drive enable + Drive-in-comments card with Drive logo * fix(google-workspace): settings header glyph violet (plugin accent), not multicolor Google * fix(google-workspace): settings header glyph inline + w-5 (match Pusher); remove Drive icon from Enable Google Drive toggle * feat(google-workspace): incremental Meet scope (meetings.space.created) + meet_connected status; getAuthUrl supports withMeet * fix(google-workspace): reset calendar/meet connected state on disconnect * fix(google-workspace): Drive toggle off when account disconnected * feat(google-workspace): comment.composer.action slot in task + discussion comment composers * fix(comment): guard null parent_comment/commentable in activity logging (was fatal -> 500 on add comment) * fix(google-workspace): show Drive attachments under task + discussion comments (was attached but not rendered) * feat(google-workspace): unified Drive+Meet link insertion in comment composers (new + edit); drop chips + per-comment attach button (CommentLinkActions) * fix(google-workspace): drop emoji from inserted Drive link (4-byte emoji broke save on utf8mb3 comment column) * feat(google-workspace): icon-only Drive/Meet composer buttons; add Meeting to discussion create form * feat(google-workspace): unify Drive icon in discussion create (CommentLinkActions); allowMeet flag * fix(google-workspace): normalize sidebar Drive nav glyph size/centering (viewBox padding) to align with label * revert: sidebar Drive nav glyph viewBox back to 0 0 24 24 (no resize) * fix(google-workspace): top-align sidebar Google icon with the label (no resize) * style(google-meet): update Meet brand icon (colored places); monochrome composer unchanged * feat(google-workspace): log Drive attach/detach + Meet activity, monochrome marks in activity feeds * feat(google-workspace): decorate Drive/Meet links in comment view (file-type icons + Meet card); expose decorateGoogleLinks on window.PM * copy(google-workspace): use 'team members' wording in Drive comments setting * chore(google-workspace): remove dead Meet scope chain (MEET_SCOPE/user_has_meet/with_meet) — Meet uses calendar.events * security(google-workspace): prevent injected markup in comment link rendering — DOM-build Meet card with textContent, escape + http(s)-validate inserted Drive/Meet links * fix(google-workspace): add G Workspace to admin submenu under Sprints; sidebar any-feature gate + reactive; active-route highlight for /google-workspace * fix(google-workspace): enforce Drive master toggle on usage + Drive card off-state; align access route perms to Project_Settings_Page_Access; localize calendar/meet master flags; drop dead Meet scope * fix(drive): bind Drive attachments to the route project (IDOR) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * harden(google): safe token-key fallback + gate picker_config to Drive-enabled Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(google): consolidate inline Google/Drive/Meet SVGs into GoogleIcons.jsx Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(google): hide Drive/Meet comment links from members without project access Google links pasted into comments were shown to every project member. Now the comment transformer runs content through a wedevs_pm_comment_content_visibility filter; Google_Workspace strips Drive/Docs/Meet anchors (replacing them with plain text) when the requesting user fails the existing per-project role permission Google_Service::user_can_use_drive() — managers/admins pass, co_worker/client gated by the project access map. Stripped server-side so the URL never reaches the response. No-op unless the content actually holds a google.com link. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(uninstall): preserve user data — stop dropping tables/options on delete uninstall.php dropped pm_google_tokens + pm_google_drive_files and deleted the settings options, destroying user data on plugin delete/reinstall (this is the free wp.org core plugin). Now it only clears the scheduled cleanup cron; no tables dropped, no options deleted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: anik-fahmid <fahmid.cse.cou@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds on the Drive PR. Adds free-side scaffolding for Pro Calendar/Meet: extension slots + upgrade covers on the G Workspace page and settings, incremental Calendar OAuth scope (calendar.events), per-user service prefs, disconnect cleanup hook + confirm modal. (Includes the Drive PR commits; merge after/with Drive.)
Summary by CodeRabbit