Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
277 changes: 202 additions & 75 deletions wp-content/civi-extensions/goonjcustom/Civi/CollectionBaseService.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Civi;

use Civi\Api4\Address;
use Civi\Api4\Activity;
use Civi\Api4\Contribution;
use Civi\Api4\CustomField;
Expand Down Expand Up @@ -33,7 +34,10 @@ class CollectionBaseService extends AutoSubscriber {
*/
public static function getSubscribedEvents() {
return [
'&hook_civicrm_selectWhereClause' => 'aclCollectionCamp',
'&hook_civicrm_selectWhereClause' => [
['aclCollectionCamp'],
// ['checkIfPosterNeedsToBeGenerated'],
],
'&hook_civicrm_pre' => [
['handleAuthorizationEmails'],
['checkIfPosterNeedsToBeGenerated'],
Expand All @@ -53,6 +57,137 @@ public static function getSubscribedEvents() {
];
}

/**
*
*/
public static function aclCollectionCamp($entity, &$clauses, $userId, $conditions = []) {
if (!in_array($entity, ['Eck_Collection_Camp', 'Eck_Institution_Visit'])) {
return FALSE;
}

$restrictedRoles = [
'admin', 'urban_ops_admin', 'ho_account',
'project_team_ho', 's2s_ho_team', 'njpc_ho_team', 'project_ho_and_accounts',
];

if (\CRM_Core_Permission::checkAnyPerm($restrictedRoles)) {
return;
}

try {
// Step 0: Get user's group-controlled states
$teamGroupContact = GroupContact::get(FALSE)
->addSelect('group_id')
->addWhere('contact_id', '=', $userId)
->addWhere('status', '=', 'Added')
->addWhere('group_id.Chapter_Contact_Group.Use_Case', '=', 'chapter-team')
->execute()
->first();

if (!$teamGroupContact) {
error_log("ACL: User $userId has no chapter team group.");
return FALSE;
}

$groupId = $teamGroupContact['group_id'];

$group = Group::get(FALSE)
->addSelect('Chapter_Contact_Group.States_controlled')
->addWhere('id', '=', $groupId)
->execute()
->first();

$statesControlled = $group['Chapter_Contact_Group.States_controlled'] ?? [];
if (empty($statesControlled)) {
error_log("ACL: Group $groupId controls no states.");
$clauses['id'][] = 'IN (null)';
return TRUE;
}

$statesControlled = array_unique($statesControlled);
error_log('ACL: Controlled states: ' . print_r($statesControlled, TRUE));

// Step 1: Fetch all camps as array
try {
$campsResult = \Civi\Api4\EckEntity::get('Institution_Visit', FALSE)
->addSelect('Urban_Planned_Visit.Which_Goonj_Processing_Center_do_you_wish_to_visit_')
->execute();

$camps = iterator_to_array($campsResult); //
} catch (\Exception $e) {
error_log("ACL: Unable to fetch Institution_Visit: " . $e->getMessage());
$clauses['id'][] = 'IN (null)';
return TRUE;
}

error_log('API4: Fetched camps: ' . print_r($camps, TRUE));

// Step 2: Extract unique processing center IDs
$processingCenterIds = array_unique(array_map(function ($camp) {
return $camp['Urban_Planned_Visit.Which_Goonj_Processing_Center_do_you_wish_to_visit_'] ?? null;
}, $camps));
error_log('API4: Processing Center IDs: ' . print_r($processingCenterIds, TRUE));

if (empty($processingCenterIds)) {
$clauses['id'][] = 'IN (null)';
return TRUE;
}

// Step 3: Fetch states of processing center contacts as array
$addressesResult = \Civi\Api4\Address::get(FALSE)
->addSelect('contact_id')
->addSelect('state_province_id')
->addWhere('contact_id', 'IN', $processingCenterIds)
->execute();

$addresses = iterator_to_array($addressesResult); // <-- Works too
error_log('API4: Fetched addresses: ' . print_r($addresses, TRUE));

$contactStates = [];
foreach ($addresses as $addr) {
$contactStates[$addr['contact_id']] = $addr['state_province_id'];
}

error_log('ACL: Contact states: ' . print_r($contactStates, TRUE));

// Step 4: Filter camps based on group-controlled states
$allowedCampIds = [];
foreach ($camps as $camp) {
$contactId = $camp['Urban_Planned_Visit.Which_Goonj_Processing_Center_do_you_wish_to_visit_'] ?? null;
$campId = $camp['id'] ?? null;

if ($contactId && $campId && isset($contactStates[$contactId]) && in_array($contactStates[$contactId], $statesControlled)) {
$allowedCampIds[] = $campId;
}
}

if (empty($allowedCampIds)) {
$clauses['id'][] = 'IN (null)';
return TRUE;
}

error_log('ACL: Allowed camp IDs: ' . print_r($allowedCampIds, TRUE));

// Step 5: Add allowed camp IDs to ACL clauses
if (!empty($allowedCampIds)) {
// Instead of pushing into array, directly set the clause string:
$clauses['id'] = ['IN (' . implode(',', array_map('intval', $allowedCampIds)) . ')'];
} else {
$clauses['id'] = ['IN (null)'];
}


return TRUE;

} catch (\Exception $e) {
\Civi::log()->warning("ACL: Unable to apply ACL on $entity for user $userId. " . $e->getMessage());
error_log("ACL Exception: " . $e->getMessage());
return FALSE;
}
Comment on lines +63 to +186
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Restore correct ACL filtering for Collection Camps

When aclCollectionCamp fires for Eck_Collection_Camp, we still fetch Institution_Visit records and push their IDs into $clauses['id']. That mismatched dataset makes the collection-camp listings collapse to an empty result (unless camp IDs coincidentally match visit IDs), effectively blocking normal access. Please either fall back to the old branch for Eck_Collection_Camp or derive the allowed IDs from the camp tables before shipping.

🧰 Tools
🪛 PHPMD (2.15.0)

63-63: Avoid unused parameters such as '$conditions'. (undefined)

(UnusedFormalParameter)

🤖 Prompt for AI Agents
In wp-content/civi-extensions/goonjcustom/Civi/CollectionBaseService.php around
lines 63 to 186, the code currently fetches Institution_Visit records and uses
their IDs for ACL when entity is Eck_Collection_Camp, producing a mismatch and
empty results; change the logic so that when $entity === 'Eck_Collection_Camp'
you either 1) use the original branch that queries the camp table
(Eck_Collection_Camp) to derive allowed camp IDs directly (filter by the
processing center/contact state mapping you already compute), or 2) if camps are
related to Institution_Visit via a specific foreign key, fetch Institution_Visit
-> map to the related camp IDs (follow that relation) and build $clauses['id']
from those camp IDs; keep the existing Institution_Visit flow only for entity
=== 'Eck_Institution_Visit', ensure you set $clauses['id'] to a single string
'IN (...)' of integer IDs (or 'IN (null)' when empty), and preserve error
handling/logging as-is.

}



/**
*
*/
Expand Down Expand Up @@ -725,80 +860,72 @@ public static function html2image($htmlContent, $outputPath) {
/**
*
*/
public static function aclCollectionCamp($entity, &$clauses, $userId, $conditions) {
if (!in_array($entity, ['Eck_Collection_Camp', 'Eck_Institution_Visit'])) {
return FALSE;
}

$restrictedRoles = ['admin', 'urban_ops_admin', 'ho_account', 'project_team_ho', 's2s_ho_team', 'njpc_ho_team', 'project_ho_and_accounts'];

$hasRestrictedRole = \CRM_Core_Permission::checkAnyPerm($restrictedRoles);

if ($hasRestrictedRole) {
return;
}

try {
$teamGroupContacts = GroupContact::get(FALSE)
->addSelect('group_id')
->addWhere('contact_id', '=', $userId)
->addWhere('status', '=', 'Added')
->addWhere('group_id.Chapter_Contact_Group.Use_Case', '=', 'chapter-team')
->execute();

$teamGroupContact = $teamGroupContacts->first();

if (!$teamGroupContact) {
// @todo we should handle it in a better way.
// if there is no chapter assigned to the contact
// then ideally she should not see any collection camp which
// can be done but then it limits for the admin user as well.
return FALSE;
}

$groupId = $teamGroupContact['group_id'];

$chapterGroups = Group::get(FALSE)
->addSelect('Chapter_Contact_Group.States_controlled')
->addWhere('id', '=', $groupId)
->execute();

$group = $chapterGroups->first();
$statesControlled = $group['Chapter_Contact_Group.States_controlled'];

if (empty($statesControlled)) {
// Handle the case when the group is not controlling any state.
$clauses['id'][] = 'IN (null)';
return TRUE;
}

$statesControlled = array_unique($statesControlled);
$statesList = implode(',', array_map('intval', $statesControlled));

$stateFields = self::getStateFieldDbDetails($entity);

$clausesArray = [];
foreach ($stateFields as $stateField) {
$selectQueries[] = sprintf(
'SELECT entity_id FROM `%1$s` WHERE `%2$s` IN (%3$s)',
$stateField['tableName'],
$stateField['columnName'],
$statesList,
);
}

$concatenatedQuery = implode(' UNION ', $selectQueries);

$clauseString = "IN ($concatenatedQuery)";

$clauses['id'][] = $clauseString;
}
catch (\Exception $e) {
\Civi::log()->warning("Unable to apply acl on collection camp for user $userId. " . $e->getMessage());
}

return TRUE;
}
// Public static function aclCollectionCamp($entity, &$clauses, $userId, $conditions) {
// if (!in_array($entity, ['Eck_Collection_Camp', 'Eck_Institution_Visit'])) {
// return FALSE;
// }.
// $restrictedRoles = ['admin', 'urban_ops_admin', 'ho_account', 'project_team_ho', 's2s_ho_team', 'njpc_ho_team', 'project_ho_and_accounts'];
// $hasRestrictedRole = \CRM_Core_Permission::checkAnyPerm($restrictedRoles);
// if ($hasRestrictedRole) {
// return;
// }.
// try {
// $teamGroupContacts = GroupContact::get(FALSE)
// ->addSelect('group_id')
// ->addWhere('contact_id', '=', $userId)
// ->addWhere('status', '=', 'Added')
// ->addWhere('group_id.Chapter_Contact_Group.Use_Case', '=', 'chapter-team')
// ->execute();
// $teamGroupContact = $teamGroupContacts->first();
// error_log('teamGroupContact: ' . print_r($teamGroupContact, TRUE));
// if (!$teamGroupContact) {
// // @todo we should handle it in a better way.
// // if there is no chapter assigned to the contact
// // then ideally she should not see any collection camp which
// // can be done but then it limits for the admin user as well.
// return FALSE;
// }.
// $groupId = $teamGroupContact['group_id'];
// error_log('groupId: ' . print_r($groupId, TRUE));
// $chapterGroups = Group::get(FALSE)
// ->addSelect('Chapter_Contact_Group.States_controlled')
// ->addWhere('id', '=', $groupId)
// ->execute();
// $group = $chapterGroups->first();
// error_log('group: ' . print_r($group, TRUE));
// $statesControlled = $group['Chapter_Contact_Group.States_controlled'];
// error_log('statesControlled: ' . print_r($statesControlled, TRUE));
// if (empty($statesControlled)) {
// // Handle the case when the group is not controlling any state.
// $clauses['id'][] = 'IN (null)';
// return TRUE;
// }.
// $statesControlled = array_unique($statesControlled);
// error_log('statesControlled2: ' . print_r($statesControlled, TRUE));
// $statesList = implode(',', array_map('intval', $statesControlled));
// error_log('statesList: ' . print_r($statesList, TRUE));
// $stateFields = self::getStateFieldDbDetails($entity);
// error_log('stateFields: ' . print_r($stateFields, TRUE));
// $clausesArray = [];
// foreach ($stateFields as $stateField) {
// $selectQueries[] = sprintf(
// 'SELECT entity_id FROM `%1$s` WHERE `%2$s` IN (%3$s)',
// $stateField['tableName'],
// $stateField['columnName'],
// $statesList,
// );
// }
// $concatenatedQuery = implode(' UNION ', $selectQueries);
// $clauseString = "IN ($concatenatedQuery)";
// $clauses['id'][] = $clauseString;
// }
// catch (\Exception $e) {
// \Civi::log()->warning("Unable to apply acl on collection camp for user $userId. " . $e->getMessage());
// }
// return TRUE;
// }.
// /**
// *.

/**
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1214,6 +1214,13 @@ public static function institutionCollectionCampTabset($tabsetName, &$tabs, $con
'template' => 'CRM/Goonjcustom/Tabs/MonetaryContribution.tpl',
'permissions' => ['goonj_chapter_admin', 'ho_account'],
],
'InstituteMaterialContribution' => [
'title' => ts('Monetary Contribution'),
'module' => 'afsearchAutofillInstitutionMaterialContributions',
'directive' => 'afsearch-autofill-institution-material-contributions',
'template' => 'CRM/Goonjcustom/Tabs/MonetaryContribution.tpl',
'permissions' => ['goonj_chapter_admin', 'urbanops', 'urban_ops_admin', 's2s_ho_team', 'project_team_ho', 'project_team_chapter', 'urban_ops_and_accounts_chapter_team', 'mmt_and_accounts_chapter_team', 'project_ho_and_accounts'],
],
Comment on lines +1217 to +1223
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Differentiate the new tab’s title

This adds a second tab labeled “Monetary Contribution,” so the UI now shows two indistinguishable tabs. Users won’t know which one surfaces Institute material data vs. the monetary view. Please rename the new tab (e.g., “Institute Material Contribution”) to keep the navigation clear.

🤖 Prompt for AI Agents
In
wp-content/civi-extensions/goonjcustom/Civi/InstitutionCollectionCampService.php
around lines 1254 to 1260, the new tab is titled "Monetary Contribution" which
duplicates an existing tab; update the 'title' value to a distinct label such as
ts('Institute Material Contribution') (or another clear name) so users can
distinguish the institute material tab from the monetary view; keep the ts()
translation wrapper and leave the rest of the array unchanged.

// 'monetaryContributionForUrbanOps' => [
// 'title' => ts('Monetary Contribution'),
// 'module' => 'afsearchMonetaryContributionForUrbanOps',
Expand Down