Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
fc989dd
feat(google-workspace): add Google Drive integration (OAuth + Picker)
anik-fahmid Jun 20, 2026
9539800
refactor(google-workspace): split admin setup from per-user connection
anik-fahmid Jun 20, 2026
7555116
style(google-workspace): monochrome nav/settings logo; sidebar label …
anik-fahmid Jun 20, 2026
f79b10f
feat(google-workspace): admin toggle to enable/disable Google Drive (…
anik-fahmid Jun 21, 2026
1037499
style(google-workspace): literal G Drive/G Workspace labels + outline…
anik-fahmid Jun 21, 2026
ce4a0ff
style(google-workspace): refine monochrome Drive nav/settings glyph
anik-fahmid Jun 21, 2026
a9ec5f3
style(google-workspace): adopt 2026 Drive icon — monochrome in nav/se…
anik-fahmid Jun 21, 2026
7b1ac96
style(google-workspace): outline the 2026 Drive icon in nav/settings
anik-fahmid Jun 21, 2026
f4cb579
feat(google-workspace): per-project Drive role access + move G Worksp…
anik-fahmid Jun 21, 2026
00d207a
feat(google-workspace): clearer 'View only' message in task Drive sec…
anik-fahmid Jun 21, 2026
9b7cd6c
fix(google-workspace): strict role gating on frontend (show Attach on…
anik-fahmid Jun 21, 2026
f10e167
fix(google-workspace): no Connect prompt / hide section for roles wit…
anik-fahmid Jun 21, 2026
ba41840
refactor(google-workspace): polymorphic attachments (task/comment/dis…
anik-fahmid Jun 21, 2026
166c9f1
feat(google-workspace): attach Drive files to task comments (+ orphan…
anik-fahmid Jun 21, 2026
2acb2ce
fix(google-workspace): comment Drive attach restricted to comment aut…
anik-fahmid Jun 21, 2026
d1bb174
style(google-workspace): section button label 'Attach'; compact comme…
anik-fahmid Jun 21, 2026
9583ec8
feat(google-workspace): Drive attach in discussions (body + comments)…
anik-fahmid Jun 21, 2026
b31ca5c
feat(google-workspace): attach Drive files while creating a discussio…
anik-fahmid Jun 21, 2026
d98d534
feat(google-workspace): expose Drive components via window.PM for pro
anik-fahmid Jun 21, 2026
da31127
feat(google-workspace): allow 'file' attachable type for Drive attach…
anik-fahmid Jun 21, 2026
87b5531
feat(google-workspace): clamp list to 2 + Show all; adder avatar; out…
anik-fahmid Jun 21, 2026
6d9c762
fix(google-workspace): comment Drive button reveal via opacity (was h…
anik-fahmid Jun 21, 2026
b6db5f2
feat(google-workspace): comment Drive add as icon-only button beside …
anik-fahmid Jun 21, 2026
58bf81d
feat(google-workspace): show 'Google Drive' label only in task sectio…
anik-fahmid Jun 21, 2026
9fbf23d
feat(google-workspace): drop comment Drive chips; icon button beside …
anik-fahmid Jun 21, 2026
5039d4c
feat(google-workspace): add Pro extension slots + upgrade covers for …
anik-fahmid Jun 21, 2026
6556c55
feat(google-workspace): incremental Calendar-scope consent infra (free)
anik-fahmid Jun 21, 2026
f7d3c8d
feat(google-workspace): move Calendar config to the sidebar G Workspa…
anik-fahmid Jun 21, 2026
3e76174
feat(google-workspace): Workspace page = per-feature connection cards…
anik-fahmid Jun 22, 2026
c64ecb2
feat(google-workspace): per-user service prefs on Workspace page
anik-fahmid Jun 22, 2026
26cf82a
feat(google-workspace): note that all features use one account; recon…
anik-fahmid Jun 22, 2026
531a527
feat(google-workspace): fire pm_google_before_disconnect (token still…
anik-fahmid Jun 22, 2026
77c2b5b
feat(google-workspace): disconnect confirm modal (plugin AlertDialog,…
anik-fahmid Jun 22, 2026
8555a81
feat(google-workspace): G Workspace sidebar label; smaller settings d…
anik-fahmid Jun 22, 2026
fb1c399
fix(google-workspace): use plugin ProBadge for teasers; neutral calen…
anik-fahmid Jun 22, 2026
a561a32
fix(google-workspace): settings tab — G Workspace heading, shorter de…
anik-fahmid Jun 22, 2026
2894974
fix(google-workspace): settings — Google glyph + 'Google Workspace' h…
anik-fahmid Jun 22, 2026
f0c2d82
fix(google-workspace): settings header glyph violet (plugin accent), …
anik-fahmid Jun 22, 2026
ac13e54
fix(google-workspace): settings header glyph inline + w-5 (match Push…
anik-fahmid Jun 22, 2026
43629a3
feat(google-workspace): incremental Meet scope (meetings.space.create…
anik-fahmid Jun 22, 2026
b09de19
fix(google-workspace): reset calendar/meet connected state on disconnect
anik-fahmid Jun 22, 2026
b6b771e
fix(google-workspace): Drive toggle off when account disconnected
anik-fahmid Jun 22, 2026
c6e1378
feat(google-workspace): comment.composer.action slot in task + discus…
anik-fahmid Jun 23, 2026
8855828
fix(comment): guard null parent_comment/commentable in activity loggi…
anik-fahmid Jun 23, 2026
c23d629
fix(google-workspace): show Drive attachments under task + discussion…
anik-fahmid Jun 23, 2026
07dcaf4
feat(google-workspace): unified Drive+Meet link insertion in comment …
anik-fahmid Jun 23, 2026
72e00c3
fix(google-workspace): drop emoji from inserted Drive link (4-byte em…
anik-fahmid Jun 23, 2026
f0f4515
feat(google-workspace): icon-only Drive/Meet composer buttons; add Me…
anik-fahmid Jun 23, 2026
b67d28f
feat(google-workspace): unify Drive icon in discussion create (Commen…
anik-fahmid Jun 23, 2026
3602e71
fix(google-workspace): normalize sidebar Drive nav glyph size/centeri…
anik-fahmid Jun 23, 2026
32c0eff
revert: sidebar Drive nav glyph viewBox back to 0 0 24 24 (no resize)
anik-fahmid Jun 23, 2026
231d7b9
fix(google-workspace): top-align sidebar Google icon with the label (…
anik-fahmid Jun 23, 2026
3e9fcd9
style(google-meet): update Meet brand icon (colored places); monochro…
anik-fahmid Jun 23, 2026
2473629
feat(google-workspace): log Drive attach/detach + Meet activity, mono…
anik-fahmid Jun 24, 2026
2012098
feat(google-workspace): decorate Drive/Meet links in comment view (fi…
anik-fahmid Jun 24, 2026
4e3238c
copy(google-workspace): use 'team members' wording in Drive comments …
anik-fahmid Jun 24, 2026
2aeeb45
chore(google-workspace): remove dead Meet scope chain (MEET_SCOPE/use…
anik-fahmid Jun 25, 2026
0d23756
security(google-workspace): prevent injected markup in comment link r…
anik-fahmid Jun 25, 2026
8b8f103
fix(google-workspace): add G Workspace to admin submenu under Sprints…
anik-fahmid Jun 25, 2026
6d7fc47
fix(google-workspace): enforce Drive master toggle on usage + Drive c…
anik-fahmid Jun 25, 2026
3bdc23c
fix(drive): bind Drive attachments to the route project (IDOR)
arifulhoque7 Jul 3, 2026
a06f1e3
harden(google): safe token-key fallback + gate picker_config to Drive…
arifulhoque7 Jul 3, 2026
b0d7807
refactor(google): consolidate inline Google/Drive/Meet SVGs into Goog…
arifulhoque7 Jul 3, 2026
609b985
feat(google): hide Drive/Meet comment links from members without proj…
arifulhoque7 Jul 3, 2026
c23e327
fix(uninstall): preserve user data — stop dropping tables/options on …
arifulhoque7 Jul 3, 2026
45f471a
Merge remote-tracking branch 'upstream/develop' into feature/google-c…
arifulhoque7 Jul 3, 2026
48b4c02
Merge remote-tracking branch 'upstream/develop' into feature/google-c…
arifulhoque7 Jul 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bootstrap/start.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
wedevs_pm_register_routes();
wedevs_pm_clean_svg();

( new \WeDevs\PM\Google_Workspace\Loader() )->boot();
new WeDevs\PM\Kanban\Kanban();

do_action( 'wedevs_pm_loaded' );
Expand Down
30 changes: 30 additions & 0 deletions core/WP/Menu.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,36 @@ public static function admin_menu() {

do_action( 'wedevs_cpm_admin_menu', self::$capability, $home );

// 3b. G Workspace — placed directly under Sprints (or in the views group
// when Sprints isn't present). Shown when any Google feature is on.
if ( class_exists( '\WeDevs\PM\Google_Workspace\Google_Service' ) ) {
$gw = '\WeDevs\PM\Google_Workspace\Google_Service';
$gw_on = $gw::drive_enabled() || $gw::calendar_master_enabled() || $gw::meet_master_enabled();
if ( $gw_on ) {
$gw_item = [ __( 'G Workspace', 'wedevs-project-manager' ), self::$capability, "admin.php?page={$slug}#/google-workspace" ];
$list = isset( $submenu[ $slug ] ) ? $submenu[ $slug ] : [];

// Find the integer POSITION of the Sprints entry (its array key may
// be a string, so never do arithmetic on the key itself).
$pos = -1; $i = 0;
foreach ( $list as $it ) {
if ( isset( $it[2] ) && strpos( $it[2], '#/sprints' ) !== false ) { $pos = $i; break; }
$i++;
}

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

// 4. Upgrade to Pro (free only)
if ( ! $wedevs_pm_pro ) {
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Intentionally adding custom submenu items to WordPress admin menu
Expand Down
1 change: 1 addition & 0 deletions db/Create_Table.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public function __construct() {
$this->crate_role_project_users_table();
$this->update_version();
$this->task_types();
\WeDevs\PM\Google_Workspace\Loader::install();
}

private function prefix() {
Expand Down
63 changes: 63 additions & 0 deletions routes/google-workspace.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php
use WeDevs\PM\Core\Router\Router;

$wedevs_pm_router = Router::singleton();

$gw_base = 'WeDevs/PM/Google_Workspace/Controllers/';
$gw_admin = 'WeDevs\PM\Core\Permissions\Settings_Page_Access';
$gw_auth = 'WeDevs\PM\Core\Permissions\Authentic';
$gw_access = 'WeDevs\PM\Core\Permissions\Access_Project';

// ── Admin: site-level OAuth credentials + Picker keys ────────────────
$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 ] );
Comment on lines +12 to +63

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

14 changes: 14 additions & 0 deletions src/Activity/Transformers/Activity_Transformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ private function parse_meta_for_comment( Activity $activity ) {
}
}

// 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; }

return $meta;
}

Expand Down Expand Up @@ -228,6 +232,16 @@ private function get_nested_value( $data, $path ) {
*/
private function get_activity_message_by_action( $action ) {
switch ( $action ) {
// Google Workspace activities
case 'attach_drive_file':
return __( '{{actor.data.display_name}} attached a Google Drive file to {{meta.task_title}}.', 'wedevs-project-manager' );

case 'detach_drive_file':
return __( '{{actor.data.display_name}} removed a Google Drive file from {{meta.task_title}}.', 'wedevs-project-manager' );

case 'create_meet':
return __( '{{actor.data.display_name}} started a Google Meet meeting.', 'wedevs-project-manager' );

// Project activities
case 'create_project':
/* translators: 1: User display name, 2: Project title */
Expand Down
19 changes: 19 additions & 0 deletions src/Comment/Observers/Comment_Observer.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,14 @@ protected function content( Comment $comment, $old_value ) {

private function log_activity( Comment $comment, $action_type ) {
$parent_comment = Comment::parent_comment( $comment->id );
if ( ! $parent_comment ) {
return; // can't resolve the thread root — skip activity logging
}
$commentable_type = $parent_comment->commentable_type;
$commentable = $this->get_commentable( $parent_comment );
if ( ! $commentable ) {
return; // commentable (task/discussion/file) gone — skip logging
}

switch ( $commentable_type ) {
case 'task':
Expand Down Expand Up @@ -67,11 +73,24 @@ private function log_activity( Comment $comment, $action_type ) {
}
}

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

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.


if ( $action_type == 'create' && $comment->commentable_type == 'comment' ) {
$action = 'reply_comment_on_task';
Expand Down
2 changes: 1 addition & 1 deletion src/Comment/Transformers/Comment_Transformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class Comment_Transformer extends TransformerAbstract {
public function transform( Comment $item ) {
return [
'id' => (int) $item->id,
'content' => wedevs_pm_get_content( $item->content ),
'content' => apply_filters( 'wedevs_pm_comment_content_visibility', wedevs_pm_get_content( $item->content ), (int) $item->project_id ),
'commentable_type' => $item->commentable_type,
'commentable_id' => $item->commentable_id,
'created_at' => wedevs_pm_format_date( $item->created_at ),
Expand Down
Loading
Loading