Skip to content

Google Calendar/Meet + Drive (free side) — IDOR-hardened (supersedes #625)#634

Merged
arifulhoque7 merged 67 commits into
weDevsOfficial:developfrom
arifulhoque7:feature/google-calendar-secured
Jul 3, 2026
Merged

Google Calendar/Meet + Drive (free side) — IDOR-hardened (supersedes #625)#634
arifulhoque7 merged 67 commits into
weDevsOfficial:developfrom
arifulhoque7:feature/google-calendar-secured

Conversation

@arifulhoque7

@arifulhoque7 arifulhoque7 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Continuation of #625 (Google Calendar/Meet + Drive, free side) with the security blocker fixed. anik-fahmid's original commits are preserved — this only adds the fix on top.

Security fix — Drive cross-project IDOR (High · CWE-639 · OWASP A01)

Same Drive_Controller IDOR as the drive branch: (attachable_type, attachable_id) from the request was never bound to the route project_id, and index() was unscoped → cross-project read/write of Drive attachments.

Added the attachable_project_id() ownership resolver + 403 on cross-project in index()/attach() and scoped the list query by project_id. OAuth/token/crypto handling here reviewed clean. php -l clean.

Supersedes #625.

Summary by CodeRabbit

  • New Features

    • Added Google Workspace integration with a new settings page, account connection flow, and sidebar entry.
    • Enabled Drive attachments across tasks, discussions, comments, and project items.
    • Added quick actions for inserting Drive and Meet links in comments.
    • Added visual Google link previews and activity icons for Drive and Meet actions.
  • Bug Fixes

    • Improved handling of comment rendering and attachment visibility for better consistency.
    • Added safer link handling and richer status messaging for connected services.

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)
…ly when allowed) + enforce detach permission
…cussion/project) + shared GoogleDriveAttach component
…edit/delete (order: drive, edit, delete); chips render below via showAdd=false
…n; discussion/file headers show icon + count only
…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.
anik-fahmid and others added 19 commits June 23, 2026 07:54
…composers (new + edit); drop chips + per-comment attach button (CommentLinkActions)
…le-type icons + Meet card); expose decorateGoogleLinks on window.PM
…r_has_meet/with_meet) — Meet uses calendar.events
…endering — DOM-build Meet card with textContent, escape + http(s)-validate inserted Drive/Meet links
…; 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
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@arifulhoque7, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 443f46aa-b3dc-43af-b05a-9b50ade62845

📥 Commits

Reviewing files that changed from the base of the PR and between 3bdc23c and 48b4c02.

📒 Files selected for processing (16)
  • bootstrap/start.php
  • src/Comment/Transformers/Comment_Transformer.php
  • src/Google_Workspace/Controllers/Drive_Controller.php
  • src/Google_Workspace/Google_Service.php
  • src/Google_Workspace/Loader.php
  • uninstall.php
  • views/assets/src/components/admin-settings/SettingsPage.jsx
  • views/assets/src/components/admin-settings/tabs/GoogleWorkspaceSettingsTab.jsx
  • views/assets/src/components/google-workspace/DriveCommentInsert.jsx
  • views/assets/src/components/google-workspace/GoogleDriveAttach.jsx
  • views/assets/src/components/google-workspace/GoogleDriveCommentButton.jsx
  • views/assets/src/components/google-workspace/GoogleDriveStage.jsx
  • views/assets/src/components/google-workspace/GoogleIcons.jsx
  • views/assets/src/components/google-workspace/GoogleWorkspacePage.jsx
  • views/assets/src/components/layout/AppSidebar.jsx
  • views/assets/src/components/tasks/TaskDetailSheet/index.jsx

Walkthrough

This PR introduces a Google Workspace integration (Drive, with Calendar/Meet placeholders): backend OAuth2 client and token service, REST controllers, new database tables, activity/comment metadata for Drive/Meet, and a corresponding React frontend with settings, connect page, Redux state, and Drive picker/attachment components wired across tasks and discussions.

Changes

Google Workspace Integration

Layer / File(s) Summary
Bootstrap, admin menu, and routes
bootstrap/start.php, db/Create_Table.php, core/WP/Menu.php, uninstall.php, routes/google-workspace.php
Boots the Loader on startup, installs/uninstalls tables, adds a "G Workspace" admin submenu, and registers settings/OAuth/Drive REST routes.
Loader lifecycle
src/Google_Workspace/Loader.php
Wires hooks for localization, OAuth callback, scheduled cleanup, attachment deletion cleanup, and DB install/migration.
OAuth client & service
src/Google_Workspace/Google_Client.php, src/Google_Workspace/Google_Service.php
Implements raw OAuth2/Drive HTTP calls, encrypted token storage, per-project Drive access gating, and token refresh/purge logic.
REST controllers & models
src/Google_Workspace/Controllers/*.php, src/Google_Workspace/Models/*.php
Adds OAuth, Settings, and Drive REST controllers plus Google_Drive_File/Google_Token models for attaching/managing Drive files.
Activity/comment backend metadata
src/Activity/Transformers/Activity_Transformer.php, src/Comment/Observers/Comment_Observer.php
Adds Drive/Meet activity message templates, meta flags, and comment guard clauses/metadata detection.
Redux slice
views/assets/src/store/googleWorkspaceSlice.js, views/assets/src/store/index.js
Adds async thunks and state for status, settings, attachments, and project access, registered in the root store.
Settings tab, connect page, sidebar
views/assets/src/components/admin-settings/..., views/assets/src/components/google-workspace/GoogleWorkspacePage.jsx, views/assets/src/components/layout/AppSidebar.jsx, views/assets/src/components/google-workspace/GoogleIcons.jsx
Adds admin credential settings UI, a per-user connect/disconnect page, and a sidebar navigation entry.
Drive picker & attach components
views/assets/src/components/google-workspace/DrivePickerModal.jsx, GoogleDriveAttach.jsx, GoogleDriveStage.jsx, GoogleDriveTaskSection.jsx, GoogleDriveCommentButton.jsx, DriveCommentInsert.jsx, CommentLinkActions.jsx
Implements the Google Picker overlay and attach/detach UI for tasks, comments, and discussions.
Comment/activity rendering & link decoration
views/assets/src/components/projects/ActivitiesPage/*, views/assets/src/components/projects/DiscussionsPage/*, views/assets/src/components/tasks/*, views/assets/src/lib/google-links.js
Renders Drive/Meet activity icons, wires comment insert actions, and decorates rendered HTML with Google link icons.
Frontend app wiring
views/assets/src/index.jsx
Adds the /google-workspace route, task-detail slot registration, and window.PM public API extensions.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related PRs

  • weDevsOfficial/wp-project-manager#588: Extends the same views/assets/src/index.jsx routing/window.PM bridge and Redux store setup introduced in that PR to add the new Google Workspace page and reducer.

Suggested labels: Needs Dev Review, Needs Testing

Suggested reviewers: iftakharul-islam

Poem

A rabbit hopped through Drive's blue sky, 🐇
Tokens tucked in salt-safe pie,
Meet and Drive now hand in hand,
Attached with clicks across the land,
Hop, connect, and sync away—
Google Workspace joins PM today! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the Google Workspace Drive/Calendar/Meet changes and the IDOR hardening focus.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…-enabled

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (7)
views/assets/src/components/google-workspace/GoogleDriveCommentButton.jsx (1)

33-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated/inconsistent storage-access logic.

This openPicker only guards with document.requestStorageAccessFor, unlike GoogleDriveAttach.jsx's version which also falls back to hasStorageAccess/requestStorageAccess. Consider extracting a shared requestGoogleStorageAccess() helper (or hook) used by both components to avoid divergence.

🤖 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/GoogleDriveCommentButton.jsx`
around lines 33 - 43, The Google Drive picker storage-access flow is duplicated
and inconsistent between openPicker in GoogleDriveCommentButton and the attach
flow in GoogleDriveAttach, so extract the shared logic into a
requestGoogleStorageAccess helper or hook and use it from both places. Make the
shared helper handle the requestStorageAccessFor path as well as the
hasStorageAccess/requestStorageAccess fallback, then update openPicker to call
that shared symbol before setPickerOpen(true) so both components stay aligned.
views/assets/src/lib/google-links.js (1)

50-57: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Export safeHttpUrl for reuse.

This is the right guard for any Drive/Meet URL rendered as a raw href. TaskDetailSheet/index.jsx renders act.meta.file_url without this check — exporting it here would let that call site reuse the same validated helper instead of duplicating or omitting the check.

♻️ Proposed fix
-function safeHttpUrl(u) {
+export function safeHttpUrl(u) {
🤖 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 50 - 57, The safeHttpUrl
helper is currently local-only, but it should be reusable for other raw href
renderers like TaskDetailSheet/index.jsx. Export safeHttpUrl from
google-links.js so call sites can import and use the same URL validation instead
of duplicating logic or skipping the check, and keep the existing protocol guard
behavior unchanged.
views/assets/src/components/google-workspace/DriveCommentInsert.jsx (2)

14-20: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate Drive glyph — reuse DriveMonoGlyph.

This SVG is identical to DriveMonoGlyph already exported from GoogleIcons.jsx (used in TaskDetailSheet/index.jsx). The same glyph is re-declared a third time in GoogleDriveStage.jsx.

♻️ Proposed fix
-import DrivePickerModal from './DrivePickerModal'
-
-const MonoDrive = ({ className = 'h-3.5 w-3.5' }) => (
-  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" className={className} aria-hidden="true">
-    <path d="M12 17L15.2083 11.5L17.4718 7.61972L19.8 11.5L21.4109 14.1848C23.2105 17.1841 21.05 21 17.5521 21H14.3333L13.1667 19L12 17Z"/>
-    <path d="M8.79167 11.5L12 17L13.1667 19L14.3333 21H9.66667H6.44786C2.95003 21 0.789527 17.1841 2.58914 14.1848L4.2 11.5H8.79167Z"/>
-    <path d="M15.2083 11.5H8.79167H4.2L6.52817 7.61972L8.35566 4.57391C10.0064 1.82272 13.9936 1.82272 15.6443 4.57391L17.4718 7.61972L15.2083 11.5Z"/>
-  </svg>
-)
+import DrivePickerModal from './DrivePickerModal'
+import { DriveMonoGlyph as MonoDrive } from './GoogleIcons'
🤖 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/DriveCommentInsert.jsx` around
lines 14 - 20, The Drive icon is duplicated in MonoDrive instead of reusing the
shared glyph. Replace the local SVG in DriveCommentInsert with the existing
DriveMonoGlyph from GoogleIcons.jsx, and update any import/use sites so the
component references that shared icon like TaskDetailSheet/index.jsx does.
Remove the redundant inline definition to keep the Drive glyph single-sourced
and consistent across Google Workspace components.

36-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting shared Drive access/picker-open logic into a hook.

The status/canUse fetch effects and openPicker (storage-access probing) are duplicated verbatim across DriveCommentInsert.jsx, GoogleDriveStage.jsx, and GoogleDriveAttach.jsx. A shared hook (e.g. useGoogleDriveAccess(projectId)) would reduce drift risk across these three call sites.

🤖 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/DriveCommentInsert.jsx` around
lines 36 - 59, The status/canUse fetch effects and the
storage-access/picker-opening logic in DriveCommentInsert are duplicated across
multiple Google Drive components. Extract the shared behavior into a reusable
hook such as useGoogleDriveAccess(projectId) that owns the
fetchStatus/fetchCanUse effects and the openPicker/requestStorageAccessFor flow,
then update DriveCommentInsert, GoogleDriveStage, and GoogleDriveAttach to
consume it so the logic stays consistent in one place.
src/Activity/Transformers/Activity_Transformer.php (1)

155-158: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Move the Google Workspace flags out of parse_meta_for_comment()
Comment_Observer stores has_drive/has_meet on resource_type = 'task', and parse_meta_for_task() already passes meta through unchanged. This check never fires for the current Drive/Meet flow; remove it here or move it into the task branch.

🤖 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 - 158,
The Google Workspace flag handling is currently in the comment meta parsing
path, but the Drive/Meet flow stores these flags on task resources instead, so
this branch never applies. Remove the `has_drive`/`has_meet` checks from
`Activity_Transformer::parse_meta_for_comment()` and place them in the
task-specific path, likely `parse_meta_for_task()`, where the meta is already
passed through and the flags will be available for `Comment_Observer`.
core/WP/Menu.php (1)

57-67: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

phpcs:ignore doesn't cover the actual $submenu writes.

Per PHPCS semantics, a standalone phpcs:ignore comment only suppresses the comment's own line and the immediately following line. Line 57's comment therefore only covers line 58 (if (...)), not the array_merge() assignment on lines 59-63, and the else-branch write on line 65 has no suppression at all. Both are direct $submenu global overrides, same pattern flagged elsewhere in this file.

🔧 Proposed fix: move/add ignore comments next to the actual writes
                 // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Intentionally adding custom submenu items to WordPress admin menu
                 if ( $pos >= 0 ) {
+                    // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Intentionally adding custom submenu items to WordPress admin menu
                     $submenu[ $slug ] = array_merge(
                         array_slice( $list, 0, $pos + 1 ),
                         [ $gw_item ],
                         array_slice( $list, $pos + 1 )
                     );
                 } else {
+                    // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Intentionally adding custom submenu items to WordPress admin menu
                     $submenu[ $slug ][] = $gw_item;
                 }

If placed on a line by itself, this comment will ignore the line that the comment is on and the following line.

🤖 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 `@core/WP/Menu.php` around lines 57 - 67, The current PHPCS ignore only applies
to the conditional line, not the actual $submenu writes in Menu handling. Move
or duplicate the phpcs:ignore comment so it sits directly on the assignment
statements in the WP\Menu flow, covering both the array_merge-based
$submenu[$slug] update and the else-branch append to $submenu[$slug], matching
the suppression pattern used elsewhere in this file.

Source: Coding guidelines

src/Google_Workspace/Loader.php (1)

192-213: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Hardcoded utf8 charset instead of the site's actual charset/collation.

dbDelta tables here use DEFAULT CHARSET=utf8 rather than $wpdb->get_charset_collate(). If the rest of the WP install uses utf8mb4 (the modern default), this can cause collation mismatches on any future joins/comparisons against core tables and truncation issues with 4-byte characters (e.g., emoji in file names).

🔧 Proposed fix
-        dbDelta( "CREATE TABLE IF NOT EXISTS {$tokens} (
+        $charset_collate = $wpdb->get_charset_collate();
+        dbDelta( "CREATE TABLE IF NOT EXISTS {$tokens} (
             ...
-        ) DEFAULT CHARSET=utf8" );
+        ) {$charset_collate}" );
🤖 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/Loader.php` around lines 192 - 213, The table definition
in Loader::dbDelta() is hardcoding DEFAULT CHARSET=utf8 instead of using the
WordPress site charset/collation. Update the CREATE TABLE statement for the
files table to use the charset/collation from the database connection, via the
existing $wpdb integration, so it matches the rest of the install. Keep the
change localized to the schema creation block in Loader and preserve the
existing indexes and columns.
🤖 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-63: The new Google Workspace REST routes registered on
$wedevs_pm_router only set permissions and are missing the required router-level
validation/sanitization contract. Add matching ->validator() and ->sanitizer()
handlers to each route in this file, covering the route params used by
Settings_Controller, OAuth_Controller, and Drive_Controller (such as project_id,
task_id, id, client_id, api_key, and drive_on). Keep the checks at the routing
layer rather than relying on controller-side sanitize/filter calls.

In `@src/Comment/Observers/Comment_Observer.php`:
- Around line 76-93: The Drive/Meet metadata is only added in comment_on_task(),
so other comment activity handlers never populate has_drive and has_meet even
though the activity transformer expects them. Update the other comment_on_*
methods in Comment_Observer to merge in gw_meta() using the same delete guard as
comment_on_task(), so comments on task lists, discussion boards, milestones,
projects, and files carry the same metadata.

In `@src/Google_Workspace/Controllers/Drive_Controller.php`:
- Around line 126-146: The Drive_Controller::index() method is missing the same
Drive-usability permission gate used by attach() and destroy(), so it can expose
attached file metadata without verifying project Drive access. Add the
Google_Service::user_can_use_drive( $project_id ) check in index() before
loading or returning any files, and return the same forbidden response path used
elsewhere when the user cannot use Drive for that project. Keep the existing
ownership validation in place after the usability gate.

In `@src/Google_Workspace/Google_Client.php`:
- Around line 73-78: The Google_Client::revoke() method is discarding the
wp_remote_post() result, so failures are invisible to callers; update it to
inspect the response with is_wp_error() and verify the HTTP status before
treating revocation as successful. If the request fails, log the error with
context in Google_Client::revoke() and surface a failure signal back to callers
so Google_Service::disconnect_user() and purge_stale() can avoid deleting the
local token row when revocation did not succeed.

In `@src/Google_Workspace/Google_Service.php`:
- Around line 355-359: The key() helper in Google_Service currently falls back
to fixed literals when AUTH_KEY or AUTH_SALT are missing, which makes the
encryption key predictable. Update Google_Service::key() to require both
constants and fail loudly instead of returning a derived fallback, and ensure
the token storage/encryption path that uses this key logs a critical error and
refuses to persist or encrypt tokens when the constants are undefined.

In `@src/Google_Workspace/Loader.php`:
- Around line 38-47: The delete_attachments helper is bypassing the ORM by
calling $wpdb->delete() directly. Update the Loader::delete_attachments method
to use the Google_Drive_File Eloquent model for the delete-by-attachable query
instead of raw SQL access, matching the rest of the data layer and the
wp-eloquent guideline.
- Around line 17-20: `maybe_install()` is still calling `ensure_columns()` on
every `admin_init`, so the schema checks never stop after the migration is
complete. Update `Loader::maybe_install()` to cache whether columns have already
been ensured (for example with an option/transient keyed to the current schema
version) and skip `ensure_columns()` once that flag is set; keep the existing
`db_version` migration gate in place so `ensure_columns()` only runs when the
schema actually needs work.

In `@views/assets/src/components/google-workspace/DrivePickerModal.jsx`:
- Around line 138-144: The attachment flow in DrivePickerModal.jsx currently
closes the modal even when every attachFileFor dispatch fails, leaving the user
with no feedback. Update the file loop around attachFileFor and the onClose
handling so failures are detected when attached stays 0, and show an error toast
(or equivalent failure message) before closing instead of silently dismissing
the modal. Keep the success path in the same attachFileFor.fulfilled.match check
and only close after reporting the result.

In `@views/assets/src/components/google-workspace/GoogleDriveAttach.jsx`:
- Line 17: The GoogleDriveAttach component is bypassing the app’s shared
notification abstraction by importing toast directly from sonner. Update the
notification usage in GoogleDriveAttach.jsx to use the existing useToast hook
pattern used elsewhere (for example, DiscussionsPage/index.jsx), and replace any
direct toast calls with the hook-provided notifier while keeping the same
success/error behavior.

In `@views/assets/src/components/google-workspace/GoogleWorkspacePage.jsx`:
- Line 22: Replace the direct sonner toast import in GoogleWorkspacePage with
the project’s useToast hook, since this component currently calls
toast.success/toast.error directly and bypasses the standard notification API.
Update GoogleWorkspacePage to initialize the toast helper from useToast and
route all success/error notifications through it, matching the approach used in
GoogleWorkspaceSettingsTab and other views/assets components.

In `@views/assets/src/components/projects/DiscussionsPage/index.jsx`:
- Around line 128-138: The staged Drive file attach flow in DiscussionsPage is
swallowing failures because dispatch(attachFileFor(...)) returns a rejected
action instead of throwing, and the current for-loop serializes each request.
Update the newDisc/stagedDrive attachment block to surface errors by unwrapping
each attachFileFor dispatch (or otherwise checking the rejected result) and run
the attachments in parallel rather than awaiting inside the loop. Keep the
existing success path and form resets only after all attachments complete
successfully.

---

Nitpick comments:
In `@core/WP/Menu.php`:
- Around line 57-67: The current PHPCS ignore only applies to the conditional
line, not the actual $submenu writes in Menu handling. Move or duplicate the
phpcs:ignore comment so it sits directly on the assignment statements in the
WP\Menu flow, covering both the array_merge-based $submenu[$slug] update and the
else-branch append to $submenu[$slug], matching the suppression pattern used
elsewhere in this file.

In `@src/Activity/Transformers/Activity_Transformer.php`:
- Around line 155-158: The Google Workspace flag handling is currently in the
comment meta parsing path, but the Drive/Meet flow stores these flags on task
resources instead, so this branch never applies. Remove the
`has_drive`/`has_meet` checks from
`Activity_Transformer::parse_meta_for_comment()` and place them in the
task-specific path, likely `parse_meta_for_task()`, where the meta is already
passed through and the flags will be available for `Comment_Observer`.

In `@src/Google_Workspace/Loader.php`:
- Around line 192-213: The table definition in Loader::dbDelta() is hardcoding
DEFAULT CHARSET=utf8 instead of using the WordPress site charset/collation.
Update the CREATE TABLE statement for the files table to use the
charset/collation from the database connection, via the existing $wpdb
integration, so it matches the rest of the install. Keep the change localized to
the schema creation block in Loader and preserve the existing indexes and
columns.

In `@views/assets/src/components/google-workspace/DriveCommentInsert.jsx`:
- Around line 14-20: The Drive icon is duplicated in MonoDrive instead of
reusing the shared glyph. Replace the local SVG in DriveCommentInsert with the
existing DriveMonoGlyph from GoogleIcons.jsx, and update any import/use sites so
the component references that shared icon like TaskDetailSheet/index.jsx does.
Remove the redundant inline definition to keep the Drive glyph single-sourced
and consistent across Google Workspace components.
- Around line 36-59: The status/canUse fetch effects and the
storage-access/picker-opening logic in DriveCommentInsert are duplicated across
multiple Google Drive components. Extract the shared behavior into a reusable
hook such as useGoogleDriveAccess(projectId) that owns the
fetchStatus/fetchCanUse effects and the openPicker/requestStorageAccessFor flow,
then update DriveCommentInsert, GoogleDriveStage, and GoogleDriveAttach to
consume it so the logic stays consistent in one place.

In `@views/assets/src/components/google-workspace/GoogleDriveCommentButton.jsx`:
- Around line 33-43: The Google Drive picker storage-access flow is duplicated
and inconsistent between openPicker in GoogleDriveCommentButton and the attach
flow in GoogleDriveAttach, so extract the shared logic into a
requestGoogleStorageAccess helper or hook and use it from both places. Make the
shared helper handle the requestStorageAccessFor path as well as the
hasStorageAccess/requestStorageAccess fallback, then update openPicker to call
that shared symbol before setPickerOpen(true) so both components stay aligned.

In `@views/assets/src/lib/google-links.js`:
- Around line 50-57: The safeHttpUrl helper is currently local-only, but it
should be reusable for other raw href renderers like TaskDetailSheet/index.jsx.
Export safeHttpUrl from google-links.js so call sites can import and use the
same URL validation instead of duplicating logic or skipping the check, and keep
the existing protocol guard behavior unchanged.
🪄 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: d9622421-e4b7-4c82-86d1-fbeb9016ef5d

📥 Commits

Reviewing files that changed from the base of the PR and between 2077621 and 3bdc23c.

📒 Files selected for processing (37)
  • bootstrap/start.php
  • core/WP/Menu.php
  • db/Create_Table.php
  • routes/google-workspace.php
  • src/Activity/Transformers/Activity_Transformer.php
  • src/Comment/Observers/Comment_Observer.php
  • src/Google_Workspace/Controllers/Drive_Controller.php
  • src/Google_Workspace/Controllers/OAuth_Controller.php
  • src/Google_Workspace/Controllers/Settings_Controller.php
  • src/Google_Workspace/Google_Client.php
  • src/Google_Workspace/Google_Service.php
  • src/Google_Workspace/Loader.php
  • src/Google_Workspace/Models/Google_Drive_File.php
  • src/Google_Workspace/Models/Google_Token.php
  • uninstall.php
  • views/assets/src/components/admin-settings/SettingsPage.jsx
  • views/assets/src/components/admin-settings/tabs/GoogleWorkspaceSettingsTab.jsx
  • views/assets/src/components/google-workspace/CommentLinkActions.jsx
  • views/assets/src/components/google-workspace/DriveCommentInsert.jsx
  • views/assets/src/components/google-workspace/DrivePickerModal.jsx
  • views/assets/src/components/google-workspace/GoogleDriveAttach.jsx
  • views/assets/src/components/google-workspace/GoogleDriveCommentButton.jsx
  • views/assets/src/components/google-workspace/GoogleDriveStage.jsx
  • views/assets/src/components/google-workspace/GoogleDriveTaskSection.jsx
  • views/assets/src/components/google-workspace/GoogleIcons.jsx
  • views/assets/src/components/google-workspace/GoogleWorkspacePage.jsx
  • views/assets/src/components/layout/AppSidebar.jsx
  • views/assets/src/components/projects/ActivitiesPage/constants.js
  • views/assets/src/components/projects/ActivitiesPage/parts/ActivityItem.jsx
  • views/assets/src/components/projects/DiscussionsPage/DiscussionDetailPage.jsx
  • views/assets/src/components/projects/DiscussionsPage/index.jsx
  • views/assets/src/components/tasks/SingleTaskListPage.jsx
  • views/assets/src/components/tasks/TaskDetailSheet/index.jsx
  • views/assets/src/index.jsx
  • views/assets/src/lib/google-links.js
  • views/assets/src/store/googleWorkspaceSlice.js
  • views/assets/src/store/index.js

Comment on lines +12 to +63
$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) ──
// Same gate as the project Capabilities/Settings tab where these toggles live.
$gw_manager = 'WeDevs\PM\Core\Permissions\Project_Settings_Page_Access';

$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 ] );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Missing ->validator()/->sanitizer() on every new route.

None of the 13 Google Workspace routes attach ->validator() or ->sanitizer(), only ->permission(). This is required for every REST endpoint in this codebase. Inline sanitize_text_field()/filter_var() calls inside Settings_Controller/OAuth_Controller don't substitute for the router-level contract, and params like project_id, task_id, id, client_id, api_key, drive_on, etc. reach controllers unvalidated at the routing layer.

As per coding guidelines, "Every REST endpoint must have a validator via ->validator() and sanitizer via ->sanitizer() methods."

🤖 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 - 63, The new Google Workspace
REST routes registered on $wedevs_pm_router only set permissions and are missing
the required router-level validation/sanitization contract. Add matching
->validator() and ->sanitizer() handlers to each route in this file, covering
the route params used by Settings_Controller, OAuth_Controller, and
Drive_Controller (such as project_id, task_id, id, client_id, api_key, and
drive_on). Keep the checks at the routing layer rather than relying on
controller-side sanitize/filter calls.

Source: Coding guidelines

Comment on lines +76 to +93
/** 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 ) );
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files 'src/Comment/Observers/Comment_Observer.php' 'src/**/Comment_Observer.php' | cat
echo '---'
rg -n "gw_meta\(|comment_on_(task|discussion_board|task_list|milestone|project|file)\(" src/Comment/Observers/Comment_Observer.php src -S
echo '---'
sed -n '1,220p' src/Comment/Observers/Comment_Observer.php

Repository: weDevsOfficial/wp-project-manager

Length of output: 12585


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- usages of has_drive / has_meet ---'
rg -n "\bhas_drive\b|\bhas_meet\b" src core -S

echo '--- remaining part of Comment_Observer.php ---'
sed -n '220,320p' src/Comment/Observers/Comment_Observer.php

echo '--- activity/comment UI references for comment_on_discussion_board and similar actions ---'
rg -n "comment_on_discussion_board|update_comment_on_discussion_board|reply_comment_on_discussion_board|has_drive|has_meet|drive.google.com|meet.google.com" src core -S

Repository: weDevsOfficial/wp-project-manager

Length of output: 6976


Propagate Drive/Meet metadata to all comment activity types. gw_meta() is only merged in comment_on_task(), so comments on task lists, discussion boards, milestones, projects, and files never set has_drive/has_meet even though the activity transformer already reads those flags. Merge it into the other comment_on_* handlers as well.

🤖 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
Drive/Meet metadata is only added in comment_on_task(), so other comment
activity handlers never populate has_drive and has_meet even though the activity
transformer expects them. Update the other comment_on_* methods in
Comment_Observer to merge in gw_meta() using the same delete guard as
comment_on_task(), so comments on task lists, discussion boards, milestones,
projects, and files carry the same metadata.

Comment on lines +126 to +146
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();

return [ 'data' => $this->transform_collection( $files ) ];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

index() skips the Drive-usability gate that attach()/destroy() enforce.

attach() (Line 176) and destroy() (Line 236) both call Google_Service::user_can_use_drive( $project_id ) before proceeding, but index() only checks attachable ownership. A user whose personal Drive toggle is off, whose role lacks project Drive access, or when the admin master switch is disabled, can still list attached Drive file metadata (names, thumbnail/web view links) for that target, since only the ownership check runs.

🔒 Proposed fix to gate index() consistently
     public function index( WP_REST_Request $request ) {
         $project_id        = (int) $request->get_param( 'project_id' );
         list( $type, $id ) = $this->resolve_attachable( $request );
 
+        if ( ! Google_Service::user_can_use_drive( $project_id ) ) {
+            return new \WP_Error( 'pm_google_forbidden', __( 'You are not allowed to view Google Drive files in this project.', 'wedevs-project-manager' ), [ 'status' => 403 ] );
+        }
+
         if ( $type === '' ) {
             return [ 'data' => [] ];
         }
📝 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.

Suggested change
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();
return [ 'data' => $this->transform_collection( $files ) ];
}
public function index( WP_REST_Request $request ) {
$project_id = (int) $request->get_param( 'project_id' );
list( $type, $id ) = $this->resolve_attachable( $request );
if ( ! Google_Service::user_can_use_drive( $project_id ) ) {
return new \WP_Error( 'pm_google_forbidden', __( 'You are not allowed to view Google Drive files in this project.', 'wedevs-project-manager' ), [ 'status' => 403 ] );
}
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();
return [ 'data' => $this->transform_collection( $files ) ];
}
🤖 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/Drive_Controller.php` around lines 126 -
146, The Drive_Controller::index() method is missing the same Drive-usability
permission gate used by attach() and destroy(), so it can expose attached file
metadata without verifying project Drive access. Add the
Google_Service::user_can_use_drive( $project_id ) check in index() before
loading or returning any files, and return the same forbidden response path used
elsewhere when the user cannot use Drive for that project. Keep the existing
ownership validation in place after the usability gate.

Comment on lines +73 to +78
public function revoke( $token ) {
wp_remote_post( self::REVOKE_URL, [
'timeout' => 15,
'body' => [ 'token' => $token ],
] );
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

revoke() silently swallows request failures.

The response from wp_remote_post() is discarded — no is_wp_error() check, no status check, no logging. Downstream, Google_Service::disconnect_user() and purge_stale() delete the local token row unconditionally after calling revoke(), so a failed revocation leaves the grant still valid at Google with no local record left to retry revoking it.

🔧 Proposed fix: surface and log failures
     public function revoke( $token ) {
-        wp_remote_post( self::REVOKE_URL, [
+        $res = wp_remote_post( self::REVOKE_URL, [
             'timeout' => 15,
             'body'    => [ 'token' => $token ],
         ] );
+        if ( is_wp_error( $res ) || (int) wp_remote_retrieve_response_code( $res ) >= 300 ) {
+            Loader::log( 'Token revoke failed: ' . ( is_wp_error( $res ) ? $res->get_error_message() : wp_remote_retrieve_response_code( $res ) ) );
+            return false;
+        }
+        return true;
     }
📝 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.

Suggested change
public function revoke( $token ) {
wp_remote_post( self::REVOKE_URL, [
'timeout' => 15,
'body' => [ 'token' => $token ],
] );
}
public function revoke( $token ) {
$res = wp_remote_post( self::REVOKE_URL, [
'timeout' => 15,
'body' => [ 'token' => $token ],
] );
if ( is_wp_error( $res ) || (int) wp_remote_retrieve_response_code( $res ) >= 300 ) {
Loader::log( 'Token revoke failed: ' . ( is_wp_error( $res ) ? $res->get_error_message() : wp_remote_retrieve_response_code( $res ) ) );
return false;
}
return 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 `@src/Google_Workspace/Google_Client.php` around lines 73 - 78, The
Google_Client::revoke() method is discarding the wp_remote_post() result, so
failures are invisible to callers; update it to inspect the response with
is_wp_error() and verify the HTTP status before treating revocation as
successful. If the request fails, log the error with context in
Google_Client::revoke() and surface a failure signal back to callers so
Google_Service::disconnect_user() and purge_stale() can avoid deleting the local
token row when revocation did not succeed.

Comment on lines +355 to +359
private static function key() {
$salt = defined( 'AUTH_KEY' ) ? AUTH_KEY : 'pm-google-fallback';
$salt .= defined( 'AUTH_SALT' ) ? AUTH_SALT : 'pm-google-fallback-salt';
return hash( 'sha256', $salt, true );
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Hardcoded fallback encryption key if AUTH_KEY/AUTH_SALT are undefined.

When these WP constants aren't defined, the key derivation falls back to fixed, source-visible literals. Since this class's docblock promises tokens are "encrypted at rest," a predictable fallback key defeats that guarantee for any install missing these constants — refresh/access tokens would be recoverable by anyone with the plugin source.

Recommend failing loudly (e.g., refuse to store/encrypt and log a critical error) instead of silently using a guessable 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 `@src/Google_Workspace/Google_Service.php` around lines 355 - 359, The key()
helper in Google_Service currently falls back to fixed literals when AUTH_KEY or
AUTH_SALT are missing, which makes the encryption key predictable. Update
Google_Service::key() to require both constants and fail loudly instead of
returning a derived fallback, and ensure the token storage/encryption path that
uses this key logs a critical error and refuses to persist or encrypt tokens
when the constants are undefined.

Comment on lines +38 to +47
private static function delete_attachments( $type, $id ) {
global $wpdb;
if ( empty( $id ) ) {
return;
}
$wpdb->delete( $wpdb->prefix . 'pm_google_drive_files', [
'attachable_type' => $type,
'attachable_id' => (int) $id,
] );
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use the Google_Drive_File Eloquent model instead of raw $wpdb->delete().

{src,core}/**/*.php is expected to use Eloquent for data queries; this bypasses the model (referenced elsewhere in this PR's layer) for a straightforward delete-by-attachable query.

As per coding guidelines, "Use Eloquent ORM (via tareq1988/wp-eloquent) for database queries."

🤖 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/Loader.php` around lines 38 - 47, The delete_attachments
helper is bypassing the ORM by calling $wpdb->delete() directly. Update the
Loader::delete_attachments method to use the Google_Drive_File Eloquent model
for the delete-by-attachable query instead of raw SQL access, matching the rest
of the data layer and the wp-eloquent guideline.

Source: Coding guidelines

Comment on lines +138 to +144
let attached = 0
for (const file of files) {
const res = await dispatch(attachFileFor({ projectId, attachableType, attachableId, file }))
if (attachFileFor.fulfilled.match(res)) attached++
}
if (attached > 0) toast.success(__('File attached.', 'wedevs-project-manager'))
onClose()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Silent failure when all attach requests fail.

If every attachFileFor dispatch is rejected (e.g. permission or network error), attached stays 0, no success toast fires, and the modal closes without telling the user anything went wrong.

🩹 Proposed fix
     let attached = 0
     for (const file of files) {
       const res = await dispatch(attachFileFor({ projectId, attachableType, attachableId, file }))
       if (attachFileFor.fulfilled.match(res)) attached++
     }
-    if (attached > 0) toast.success(__('File attached.', 'wedevs-project-manager'))
+    if (attached > 0) toast.success(__('File attached.', 'wedevs-project-manager'))
+    else if (files.length > 0) toast.error(__('Failed to attach file(s).', 'wedevs-project-manager'))
     onClose()
📝 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.

Suggested change
let attached = 0
for (const file of files) {
const res = await dispatch(attachFileFor({ projectId, attachableType, attachableId, file }))
if (attachFileFor.fulfilled.match(res)) attached++
}
if (attached > 0) toast.success(__('File attached.', 'wedevs-project-manager'))
onClose()
let attached = 0
for (const file of files) {
const res = await dispatch(attachFileFor({ projectId, attachableType, attachableId, file }))
if (attachFileFor.fulfilled.match(res)) attached++
}
if (attached > 0) toast.success(__('File attached.', 'wedevs-project-manager'))
else if (files.length > 0) toast.error(__('Failed to attach file(s).', 'wedevs-project-manager'))
onClose()
🤖 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` around
lines 138 - 144, The attachment flow in DrivePickerModal.jsx currently closes
the modal even when every attachFileFor dispatch fails, leaving the user with no
feedback. Update the file loop around attachFileFor and the onClose handling so
failures are detected when attached stays 0, and show an error toast (or
equivalent failure message) before closing instead of silently dismissing the
modal. Keep the success path in the same attachFileFor.fulfilled.match check and
only close after reporting the result.

import { fetchStatus, fetchCanUse, fetchAttachmentsFor, detachFileFor } from '@store/googleWorkspaceSlice'
import { Button } from '@components/ui/button'
import { FileText, Plus, ExternalLink, Trash2, Link2, Lock, X } from 'lucide-react'
import { toast } from 'sonner'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use useToast instead of raw sonner import.

Other files in this cohort (e.g. DiscussionsPage/index.jsx) call notifications via the project's useToast hook, but this component imports toast directly from sonner, bypassing the shared abstraction.

♻️ Proposed fix
-import { toast } from 'sonner'
+import { useToast } from '`@hooks/useToast`'
 export default function GoogleDriveAttach(...) {
+  const toast = useToast()

Also applies to: 80-83

🤖 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 bypassing the app’s shared notification
abstraction by importing toast directly from sonner. Update the notification
usage in GoogleDriveAttach.jsx to use the existing useToast hook pattern used
elsewhere (for example, DiscussionsPage/index.jsx), and replace any direct toast
calls with the hook-provided notifier while keeping the same success/error
behavior.

Source: Coding guidelines

import { ShieldCheck, Unlink, HardDrive, Settings as SettingsIcon, Lock, Info } from 'lucide-react'
import { CalendarGlyph, MeetGlyph } from '@components/google-workspace/GoogleIcons'
import ProBadge from '@components/common/ProBadge'
import { toast } from 'sonner'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use the useToast hook instead of importing sonner directly.

This file calls toast.success/toast.error from the raw sonner import (lines 93, 106, 111-113), bypassing the project's useToast hook used everywhere else (e.g., in the sibling GoogleWorkspaceSettingsTab.jsx). As per coding guidelines, views/assets/src/**/*.{jsx,js}: "Use custom useToast hook for showing notifications".

♻️ Proposed fix
-import { toast } from 'sonner'
+import { useToast } from '`@hooks/useToast`'
 export default function GoogleWorkspacePage() {
   const dispatch = useAppDispatch()
+  const toast = useToast()
   const { status, statusLoading } = useAppSelector(s => s.googleWorkspace)

Also applies to: 93-93, 106-106, 111-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` at line
22, Replace the direct sonner toast import in GoogleWorkspacePage with the
project’s useToast hook, since this component currently calls
toast.success/toast.error directly and bypasses the standard notification API.
Update GoogleWorkspacePage to initialize the toast helper from useToast and
route all success/error notifications through it, matching the approach used in
GoogleWorkspaceSettingsTab and other views/assets components.

Source: Coding guidelines

Comment on lines +128 to +138
// Attach any staged Drive files now that the discussion has an id.
if (newDisc?.id && stagedDrive.length) {
for (const file of stagedDrive) {
await dispatch(attachFileFor({ projectId, attachableType: 'discussion', attachableId: newDisc.id, file }));
}
}
setFormTitle("");
setFormDesc("");
setFormMilestone("-1");
setFormFiles([]);
setStagedDrive([]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Silent failure & serial execution when attaching staged Drive files.

attachFileFor resolves via rejectWithValue on error rather than throwing, so a failed attach in this loop is never surfaced to the user — the discussion is created but some files silently fail to attach. The await inside the loop also serializes requests unnecessarily.

🐛 Proposed fix
-        if (newDisc?.id && stagedDrive.length) {
-          for (const file of stagedDrive) {
-            await dispatch(attachFileFor({ projectId, attachableType: 'discussion', attachableId: newDisc.id, file }));
-          }
-        }
+        if (newDisc?.id && stagedDrive.length) {
+          const results = await Promise.all(
+            stagedDrive.map((file) =>
+              dispatch(attachFileFor({ projectId, attachableType: 'discussion', attachableId: newDisc.id, file }))
+            )
+          );
+          if (results.some((r) => attachFileFor.rejected.match(r))) {
+            toast.error(__('Some Drive files failed to attach', 'wedevs-project-manager'));
+          }
+        }
📝 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.

Suggested change
// Attach any staged Drive files now that the discussion has an id.
if (newDisc?.id && stagedDrive.length) {
for (const file of stagedDrive) {
await dispatch(attachFileFor({ projectId, attachableType: 'discussion', attachableId: newDisc.id, file }));
}
}
setFormTitle("");
setFormDesc("");
setFormMilestone("-1");
setFormFiles([]);
setStagedDrive([]);
// Attach any staged Drive files now that the discussion has an id.
if (newDisc?.id && stagedDrive.length) {
const results = await Promise.all(
stagedDrive.map((file) =>
dispatch(attachFileFor({ projectId, attachableType: 'discussion', attachableId: newDisc.id, file }))
)
);
if (results.some((r) => attachFileFor.rejected.match(r))) {
toast.error(__('Some Drive files failed to attach', 'wedevs-project-manager'));
}
}
setFormTitle("");
setFormDesc("");
setFormMilestone("-1");
setFormFiles([]);
setStagedDrive([]);
🤖 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` around lines
128 - 138, The staged Drive file attach flow in DiscussionsPage is swallowing
failures because dispatch(attachFileFor(...)) returns a rejected action instead
of throwing, and the current for-loop serializes each request. Update the
newDisc/stagedDrive attachment block to surface errors by unwrapping each
attachFileFor dispatch (or otherwise checking the rejected result) and run the
attachments in parallel rather than awaiting inside the loop. Keep the existing
success path and form resets only after all attachments complete successfully.

arifulhoque7 and others added 4 commits July 3, 2026 16:07
…leIcons.jsx

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ect 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>
…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>
…alendar-secured

# Conflicts:
#	views/assets/src/components/layout/AppSidebar.jsx
…alendar-secured

# Conflicts:
#	bootstrap/start.php
#	views/assets/src/store/index.js
@arifulhoque7 arifulhoque7 merged commit d22f629 into weDevsOfficial:develop Jul 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants