diff --git a/Controller/GrouperGroupsController.php b/Controller/GrouperGroupsController.php index 21dfedd..bc41852 100644 --- a/Controller/GrouperGroupsController.php +++ b/Controller/GrouperGroupsController.php @@ -158,15 +158,11 @@ public function groupSubscribers(): void //Need to see if coming from AdHoc or from a WG (Working Group) $groupNameFormatted = strpos($groupName, ':') === false ? 'ref:incommon-collab:' . $groupName . ':users' : $groupName; - //Set initial - $scope = [ - 'groupName' => $groupNameFormatted - ]; try { - $subscribers = $this->GrouperGroup->membersInGroup($scope, - $this->userId, - $this->CoGrouperLiteWidget->getConfig()); + $subscribers = $this->GrouperGroup->getGrouperGroupMembers($this->userId, + $groupNameFormatted, + $this->CoGrouperLiteWidget->getConfig()); CakeLog::write('debug', __METHOD__ . '::response: ' . var_export($subscribers, true)); } catch (Exception $e) { @@ -219,109 +215,28 @@ public function addSubscriber(): void } /** - * The majority of this code is copied from the find() function in the CoPeopleController that was added by - * Arlen. Eventually this will become a component, so for now just tweaking the code needed for the Grouper plugin - * to work. + * Perform a "keyword" search for CO People, sort of like the CO Dashboard + * cross controller search, but intended specifically for "people finder" + * search while you type API calls. * + * @since COmanage Registry v3.3.0 */ - public function findSubscriber() { - $findUserId = urldecode($this->request->query['term']); - $co = urldecode($this->request->query['co']); - - $mode = urldecode($this->request->query['mode']); - // jquery Autocomplete sends the search as url?term=foo - $coPersonIds = array(); - if(!empty($findUserId)) { - // Leverage model specific keyword search - - // Note EmailAddress and Identifier don't support substring search - foreach(array('Name', 'EmailAddress', 'Identifier') as $m) { - $hits = $this->CoPerson->$m->search($co, $findUserId, 25); + public function findSubscriber(): void + { + $this->autoRender = false; + $this->request->allowMethod('ajax'); + $this->layout = 'ajax'; - foreach($hits as $hit) { - if ($hit['CoPerson']['status'] == 'A') { - $coPersonIds[] = $hit['CoPerson']['id']; - } - } - // $coPersonIds = array_merge($coPersonIds, Hash::extract($hits, '{n}.CoPerson.id')); - } + // What search mode should we use? + if(empty($this->request->query['mode'])) { + $this->Api->restResultHeader(HttpStatusCodesEnum::HTTP_BAD_REQUEST, 'Mode Not Specified'); + return; } - $coPersonIds = array_unique($coPersonIds); - - // Look up additional information to provide hints as to which person is which. - // We only do this when there are relatively small numbers of results to - // avoid making a bunch of database queries early in the search. - - $matches = array(); - - if(count($coPersonIds) > 100) { - // We don't return large sets to avoid slow performance - - $this->response->type('json'); - $this->response->statusCode(401); - $this->response->body(json_encode(array('status' => 'ERROR', 'message' => 'Too Many Results'))); - $this->response->send(); - $matches = ''; - } else { - $people = $this->CoPerson->filterPicker($co, $coPersonIds, $mode); - $pickerEmailType = $this->Co->CoSetting->getPersonPickerEmailType($co); - $pickerIdentifierType = $this->Co->CoSetting->getPersonPickerIdentifierType($co); - $pickerDisplayTypes = $this->Co->CoSetting->getPersonPickerDisplayTypes($co); - - foreach($people as $p) { - $label = generateCn($p['Name'][0]); - $idArr = $p['Identifier']; - $emailArr = $p['EmailAddress']; - $email = ''; - $emailLabel = ''; - $id = ''; - $idLabel = ''; - - // Iterate over the email array - if(!empty($emailArr) && !empty($pickerEmailType)) { - if(!empty($pickerDisplayTypes)) { - $emailLabel = _txt('fd.extended_type.generic.label', array(_txt('fd.email_address.mail'), $pickerEmailType)); - } - else { - $emailLabel = _txt('fd.email_address.mail') . ': '; - } - foreach($emailArr as $e) { - if($e['type'] == $pickerEmailType) { - $email = $e['mail']; - break; - } - } - } - - // Set the identifier for display (and limit it to 30 characters max) - if(!empty($idArr[0]['identifier']) && !empty($pickerIdentifierType)) { - if(!empty($pickerDisplayTypes)) { - $idLabel = _txt('fd.extended_type.generic.label', array(_txt('fd.identifier.identifier'), $pickerIdentifierType)); - } - else { - $idLabel = _txt('fd.identifier.identifier') . ': '; - } - foreach($idArr as $i) { - if($i['type'] == $pickerIdentifierType) { - //$id = mb_strimwidth($i['identifier'], 0, 30, '...'); - $id = $i['identifier']; - break; - } - } - } - - $matches[] = array( - 'value' => $p['CoPerson']['id'], - 'label' => $label, - 'email' => $email, - 'emailLabel' => $emailLabel, - 'identifier' => $id, - 'identifierLabel' => $idLabel - ); - } - } + $matches = $this->GrouperGroup->findForPicker($this->cur_co['Co']['id'], + $this->request->query['mode'], + $this->request->query['term'] ?? null); $this->set(compact('matches')); $this->set('_serialize', 'matches'); diff --git a/Lib/GrouperApiAccess.php b/Lib/GrouperApiAccess.php index f904923..0f1589e 100644 --- a/Lib/GrouperApiAccess.php +++ b/Lib/GrouperApiAccess.php @@ -372,13 +372,14 @@ public function getGrouperGroupInfo(array $queryData) * Note: Params added at end make sure that the groups returned can only be viewed by the member logged into * Grouper Lite * - * @param string $userId + * @param string $actorUserId + * @param string $userId * * @return array Membership records that User is a member of in Grouper * * @throws GrouperLiteWidgetException */ - public function getGrouperMemberOfGroups(string $userId) + public function getUserGrouperGroupMemberships(string $actorUserId, string $userId): array { if(empty($userId)) { return []; @@ -386,7 +387,7 @@ public function getGrouperMemberOfGroups(string $userId) $actionEndpoint = "/subjects/{$userId}/groups?" . 'wsLiteObjectType=WsRestGetGroupsLiteRequest' - . "&actAsSubjectId={$userId}"; + . "&actAsSubjectId={$actorUserId}"; try { $results = $this->http->sendRequest('GET', $actionEndpoint); } catch (Exception $e) { @@ -400,20 +401,23 @@ public function getGrouperMemberOfGroups(string $userId) /** * Get members associated to a specific Grouper Group * - * @param array $queryData Array of conditions for querying + * @param string $actorUserId + * @param string $groupName * * @return array Listing of Members belonging to Grouper Group - * @throws GrouperLiteWidgetException|JsonException + * @throws GrouperLiteWidgetException + * @throws JsonException */ - public function getMembersInGroup(array $queryData) { + public function getGrouperGroupMembers(string $actorUserId, string $groupName): array + { //Build request logic $usersToShow = [ 'WsRestGetMembersRequest' => [ 'actAsSubjectLookup' => [ - 'subjectId' => $queryData['userId'] + 'subjectId' => $actorUserId ], 'wsGroupLookups' => [ - ['groupName' => $queryData['groupName']] + ['groupName' => $groupName] ], 'subjectAttributeNames' => ['name'] ] diff --git a/Lib/GrouperHTTPWrapper.php b/Lib/GrouperHTTPWrapper.php index 00a29f9..727ae79 100644 --- a/Lib/GrouperHTTPWrapper.php +++ b/Lib/GrouperHTTPWrapper.php @@ -174,6 +174,8 @@ public function setUser(string $user): void * @throws GrouperLiteWidgetException If issue with Grouper WS connection * * TODO: Handle unauthorized request-responses + * TODO: Handle SUBJECT_NOT_FOUND, + * TODO: Handle GROUP_NOT_FOUND, */ public function sendRequest(string $method, string $endPoint, string $body = ''): array { $uri = "{$this->_serviceUrl}{$endPoint}"; diff --git a/Lib/enum.php b/Lib/enum.php index f9a2cf7..e430d7a 100644 --- a/Lib/enum.php +++ b/Lib/enum.php @@ -14,12 +14,16 @@ class GrouperSpecialGroups { /** Group whose members cannot see Grouper button, even if admin */ const GROUPER_VISIBLE_GROUP = 'app:comanage:LiteUI:grouperVisible'; + + /* Stem for email groups, only email groups using this stem are viewable */ + const GROUPER_EMAIL_STEM = 'app:sympa'; } class GrouperResultCodesEnum { const IS_MEMBER = 'IS_MEMBER'; const SUCCESS = 'SUCCESS'; const GROUP_NOT_FOUND = 'GROUP_NOT_FOUND'; + const SUBJECT_NOT_FOUND = 'SUBJECT_NOT_FOUND'; const NO_SUCH_OBJECT = 'NO_SUCH_OBJECT'; const SUCCESS_NO_INFO = 'SUCCESS_NO_INFO'; const EXECUTE_FAILED = 'EXECUTE_FAILED'; diff --git a/Model/GrouperGroup.php b/Model/GrouperGroup.php index 58216f0..f535a7a 100644 --- a/Model/GrouperGroup.php +++ b/Model/GrouperGroup.php @@ -44,12 +44,6 @@ class GrouperGroup extends GrouperLiteWidgetAppModel /** @var GrouperApiAccess $grouperAPI */ private $grouperAPI = null; - /** @var string Group whose members can create Groups via Template process */ - private $templateCreationGroup = 'ref:workinggroupadmins'; - - /** @var string Group whose members cannot see Grouper button, even if admin */ - private $grouperVisibleGroup = 'app:comanage:LiteUI:grouperVisible'; - private $wgStemsTopLevel = array( 'ref:incommon-collab', 'ref:internet2-collab' @@ -65,9 +59,6 @@ class GrouperGroup extends GrouperLiteWidgetAppModel 'ref:internet2-collab' ); - // Stem for email groups, only email groups using this stem are viewable - private $emailStem = 'app:sympa'; - /** * Verifies if user is an owner/admin of a group and then stores results in Session. @@ -148,7 +139,7 @@ public function filteredMemberOfGroups(array $conditions, array $cfg) $this->initApi($cfg); try { - $memberOfGroups = $this->memberOfGroups($conditions, $cfg); + $memberOfGroups = $this->memberOfGroups($conditions['userId'], $conditions['userId'], $cfg); $conditions['groupType'] = 'optouts'; // Determine which groups can be left by user, if want. @@ -167,21 +158,134 @@ public function filteredMemberOfGroups(array $conditions, array $cfg) } } + + /** + * Find People based on mode and term + * + * @param integer $coId CO ID + * @param string $mode Search mode to apply filters for + * @param string|null $term Search block + * + * @return array Array of CO Person records + * @since COmanage Registry v4.4.0 + * + * XXX Remove this as soon as the change is present in COmanage core + * + */ + public function findForPicker(int $coId, string $mode, ?string $term): array + { + $coPersonIds = []; + $this->CoPerson = ClassRegistry::init('CoPerson'); + $this->Co = ClassRegistry::init('Co'); + + // jquery Autocomplete sends the search as url?term=foo + if(!empty($term)) { + // Leverage model specific keyword search + + // Note EmailAddress and Identifier don't support substring search + foreach(array('Name', 'EmailAddress', 'Identifier') as $m) { + $hits = $this->CoPerson->$m->search($coId, $term, 25); + + $coPersonIds = array_merge($coPersonIds, Hash::extract($hits, '{n}.CoPerson.id')); + } + } + + $coPersonIds = array_unique($coPersonIds); + + // Look up additional information to provide hints as to which person is which. + // We only do this when there are relatively small numbers of results to + // avoid making a bunch of database queries early in the search. + + $matches = array(); + + if(count($coPersonIds) > 100) { + // We don't return large sets to avoid slow performance + + $matches[] = array( + 'value' => -1, + 'label' => _txt('er.picker.toomany') + ); + } else { + $people = $this->CoPerson->filterPicker($coId, $coPersonIds, $mode); + $pickerEmailType = $this->Co->CoSetting->getPersonPickerEmailType($coId); + $pickerIdentifierType = $this->Co->CoSetting->getPersonPickerIdentifierType($coId); + $pickerDisplayTypes = $this->Co->CoSetting->getPersonPickerDisplayTypes($coId); + + foreach($people as $p) { + $label = generateCn($p['Name'][0]); + $idArr = $p['Identifier']; + $emailArr = $p['EmailAddress']; + $email = ''; + $emailLabel = ''; + $id = ''; + $idLabel = ''; + + // Iterate over the email array + if(!empty($emailArr) && !empty($pickerEmailType)) { + if(!empty($pickerDisplayTypes)) { + $emailLabel = _txt('fd.extended_type.generic.label', array(_txt('fd.email_address.mail'), $pickerEmailType)); + } + else { + $emailLabel = _txt('fd.email_address.mail') . ': '; + } + foreach($emailArr as $e) { + if($e['type'] == $pickerEmailType) { + $email = $e['mail'] . ' ' . $pickerDisplayTypes; + break; + } + } + } + + // Set the identifier for display (and limit it to 30 characters max) + if(!empty($idArr[0]['identifier']) && !empty($pickerIdentifierType)) { + if(!empty($pickerDisplayTypes)) { + $idLabel = _txt('fd.extended_type.generic.label', array(_txt('fd.identifier.identifier'), $pickerIdentifierType)); + } + else { + $idLabel = _txt('fd.identifier.identifier') . ': '; + } + foreach($idArr as $i) { + if($i['type'] == $pickerIdentifierType) { + $id = mb_strimwidth($i['identifier'], 0, 30, '...'); + break; + } + } + } + + // Make sure we don't already have an entry for this CO Person ID + if(!Hash::check($matches, '{n}[value='.$p['CoPerson']['id'].']')) { + $matches[] = array( + 'value' => $p['CoPerson']['id'], + 'label' => $label, + 'email' => $email, + 'emailLabel' => $emailLabel, + 'identifier' => $id, + 'identifierLabel' => $idLabel + ); + } + } + } + + return $matches; + } + /** * Internal process used by other functions to fetch Groups the User is a member of * - * @param array $conditions Listing of conditions for display of records, including UserId + * @param string $actorUserId + * @param string $userId + * @param array $cfg + * * @return array Records of Groups from Grouper that the User belongs to * @throws GrouperLiteWidgetException - * * @since COmanage Registry v4.4.0 */ - private function memberOfGroups(array $conditions, array $cfg) + private function memberOfGroups(string $actorUserId, string $userId, array $cfg) { $this->initApi($cfg); try { - return $this->grouperAPI->getGrouperMemberOfGroups($conditions['userId']); + return $this->grouperAPI->getUserGrouperGroupMemberships($actorUserId, $userId); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; @@ -264,21 +368,21 @@ public function ownerGroups(array $conditions, array $cfg) * NOTE: This list only shows members, it does not pull in other groups that may be attached in Grouper as * members * - * @param array $conditions Listing of conditions for display of records - * @param string $userId Id of User + * @param string $actorUserId Id of User + * @param string $groupName + * @param array $cfg + * * @return array Listing of members in requested Grouper Group * @throws GrouperLiteWidgetException Captured in Controller - * + * @throws JsonException * @since COmanage Registry v4.4.0 */ - public function membersInGroup(array $conditions, string $userId, array $cfg) + public function getGrouperGroupMembers(string $actorUserId, string $groupName, array $cfg): array { $this->initApi($cfg); - $conditions['userId'] = $userId; - try { - $groupMembers = $this->grouperAPI->getMembersInGroup($conditions); + $groupMembers = $this->grouperAPI->getGrouperGroupMembers($actorUserId, $groupName); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; @@ -371,7 +475,7 @@ public function optinGroups(array $conditions, array $cfg) // Groups the user can join or leave $joinOrLeave = $this->grouperAPI->getOptionalGroups($conditions); // Groups the user is a member of - $userGroups = $this->memberOfGroups($conditions, $cfg); + $userGroups = $this->memberOfGroups($conditions['userId'], $conditions['userId'], $cfg); // Extract the names of the Groups the user is a member of $userGroupsNames = Hash::extract($userGroups, '{n}.name'); // Return the groups the user can join and is not a member of diff --git a/webroot/js/autocomplete.js b/webroot/js/autocomplete.js index 284ff57..5e24fe5 100644 --- a/webroot/js/autocomplete.js +++ b/webroot/js/autocomplete.js @@ -21,7 +21,7 @@ export default { const input = $(this.$el).find('#add-user-input'); input.autocomplete({ source: `${this.api.find}?co=${this.api.co}&mode=${this.api.mode}`, - minLength: 1, + minLength: 3, maxShowItems: 10, focus: function( event, ui ) { this.search = ui.item.label;