<?php /** * COmanage Registry Grouper Lite Widget Groups Model * * Portions licensed to the University Corporation for Advanced Internet * Development, Inc. ("UCAID") under one or more contributor license agreements. * See the NOTICE file distributed with this work for additional information * regarding copyright ownership. * * UCAID licenses this file to you under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with the * License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * @link http://www.internet2.edu/comanage COmanage Project * @package registry-plugin * @since COmanage Registry v3.2.0 * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ App::uses('GrouperApiAccess', 'GrouperLiteWidget.Lib/'); App::uses('GrouperAttribute', 'GrouperLiteWidget.Model/'); /*** * Class GrouperGroup * * Model class that does most of the heavy lifting in Grouper Lite Widget */ class GrouperGroup extends GrouperLiteWidgetAppModel { // XXX According to the documentation (https://spaces.at.internet2.edu/display/Grouper/UI+Terminology) // the displayExtension is the Friendly Name of the Group. /** @var string $name used by CakePHP for locating model */ public $name = 'GrouperGroup'; /** @var GrouperApiAccess $grouperAPI */ private $grouperAPI = null; private $wgStemsTopLevel = array( 'ref:incommon-collab', 'ref:internet2-collab' ); // Current listing of groups that are associated to Working Groups private $wgStemsAllGroups = array( 'app:jira', 'app:confluence', 'app:sympa:internet2', 'app:sympa:incommon', 'ref:incommon-collab', 'ref:internet2-collab' ); /** * Verifies if a user is an owner/admin of a group. * Session variable is reset on Group Creation and Group Deletion * * @param string $userId Id of User * @param string $actorUserId * @param array $cfg * * @return bool T or F * @throws GrouperLiteWidgetException * @since COmanage Registry v4.4.0 */ public function isUserGroupOwner(string $userId, string $actorUserId, array $cfg): bool { $this->initApi($cfg); if(empty($userId) || empty($actorUserId)) { return false; } try { $resultsAdmin = $this->grouperAPI->getUserMemberships($userId, $actorUserId, GrouperGroupTypeEnum::ADMIN); $resultsUpdate = $this->grouperAPI->getUserMemberships($userId, $actorUserId, GrouperGroupTypeEnum::UPDATE); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } return count($resultsAdmin['WsGetMembershipsResults']['wsGroups'] ?? []) > 0 || count($resultsUpdate['WsGetMembershipsResults']['wsGroups'] ?? []) > 0; } /** * Verifies if user can see the Grouper link button. Even if owner, may be not allowed to see it. * * @param string $userId Id of User * @return String T or F * @throws GrouperLiteWidgetException * * @since COmanage Registry v4.4.0 */ public function isGrouperVisible(string $userId, array $cfg): string { $this->initApi($cfg); try { $isMember = $this->grouperAPI->isMemberOfGroup(GrouperSpecialGroups::GROUPER_VISIBLE_GROUP, $userId); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } return $isMember ? 'T' : 'F'; } /** * Grouper API Instance Singleton * * @throws GrouperLiteWidgetException * * @since COmanage Registry v4.4.0 */ private function initApi(array $cfg) { if ($this->grouperAPI === null) { $this->grouperAPI = new GrouperApiAccess($cfg); } } /** * Add a member to a specific Grouper Group * * @param string $actorUserId Id of User * @param string $groupName * @param string $addUserId * @param array $cfg * * @return string success of Request * @throws GrouperLiteWidgetException Captured in Controller * @throws JsonException * @since COmanage Registry v4.4.0 */ public function addGroupMember(string $actorUserId, string $groupName, string $addUserId, array $cfg) { $this->initApi($cfg); return $this->grouperAPI->addGroupMember($actorUserId, $groupName, $addUserId); } /** * Breakout Working Groups from AdHoc Groups. * * @param array $recordSet * @return array[] * */ public function breakoutWrkFromAdHocGroups(array $recordSet): array { $wgData = []; $notWGData = []; //Parse out the Working Groups from the Ad-hoc groups foreach ($recordSet as $record) { if (isset($record['WGName'])) { $wgData[] = $record; } else { $notWGData[] = $record; } } return [ 'adhoc' => $notWGData, 'working' => $wgData ]; } /** * Create a new Grouper Group using the Template methodology in Grouper * * @param string $userId Id of User * @param array $groupData Data needed to create new Grouper Group via Template * @return array status and error message, if applicable * @throws GrouperLiteWidgetException * * @since COmanage Registry v4.4.0 */ public function createGroupWithTemplate(string $userId, array $groupData, array $cfg) { $this->initApi($cfg); //Need to massage incoming data to meet Grouper Template requirements $fields = array( 'gsh_input_isSympa', 'gsh_input_isSympaModerated', 'gsh_input_isOptin', 'gsh_input_isConfluence', 'gsh_input_isJira' ); // Template does not except true/false, so convert to string and send that way foreach ($fields as $field) { ($groupData[$field] == '0') ? $groupData[$field] = 'false' : $groupData[$field] = 'true'; } $args = array(); $args['userId'] = $userId; $args['data'] = $groupData; try { return $this->grouperAPI->createGroupWithTemplate($args); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } } /** * Construct picker(like) response data based on mode and term * * @param integer $coId CO ID * @param string $mode Search mode to apply filters for * @param array $coPersonIds List of PersonIds * * @return array Array of CO Person records * @since COmanage Registry v4.4.0 */ public function dataConstructForPicker(int $coId, string $mode, array $coPersonIds): array { if(empty($coPersonIds)) { return []; } $this->CoPerson = ClassRegistry::init('CoPerson'); $this->Co = ClassRegistry::init('Co'); $matches = []; 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 = ''; $email_short = ''; $emailLabel = ''; $id = ''; $id_short = ''; $idLabel = ''; // Iterate over the email array if(!empty($emailArr) && !empty($pickerEmailType)) { $emailLabel = !empty($pickerDisplayTypes) ? _txt('fd.extended_type.generic.label', array(_txt('fd.email_address.mail'), $pickerEmailType)) : _txt('fd.email_address.mail') . ': '; foreach($emailArr as $e) { if($e['type'] == $pickerEmailType) { $email = $e['mail']; $email_short = mb_strimwidth($e['mail'], 0, 30, '...'); 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_short = mb_strimwidth($i['identifier'], 0, 30, '...'); $id = $i['identifier']; 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, 'emailShort' => $email_short, 'emailLabel' => $emailLabel, 'emailType' => $pickerEmailType, 'identifier' => $id, 'identifierShort' => $id_short, 'identifierLabel' => $idLabel, 'identifierType' => $pickerIdentifierType ); } } } return $matches; } /** * Return all Groups that a User belongs to in Grouper. * Will also add OptOut Groups and flag them as joined so can display an Optout option in the UI. * * @param string $userId * @param string $actorUserId * @param array $cfg * * @return array Records of Groups from Grouper that the User belongs to * @throws GrouperLiteWidgetException * @since COmanage Registry v4.4.0 */ public function filteredMemberOfGroups(string $userId, string $actorUserId, array $cfg): array { $this->initApi($cfg); try { $memberOfGroups = $this->memberOfGroups($actorUserId, $userId, $cfg); // Determine which groups can be left by user, if wanted. $optOutGroups = $this->grouperAPI->getUserMemberships($userId, $actorUserId, GrouperGroupTypeEnum::OPTOUTS); $optOutGroupsNames = Hash::combine($optOutGroups, '{n}.name', '{n}.displayExtension'); foreach ($memberOfGroups as &$memberOfGroup) { $memberOfGroup['optOut'] = isset($optOutGroupsNames[$memberOfGroup['name']]); } return $this->getFriendlyWorkingGroupName($memberOfGroups, 'member'); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } } /** * 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 */ public function findForPicker(int $coId, string $mode, ?string $term): array { $coPersonIds = []; $this->CoPerson = ClassRegistry::init('CoPerson'); // 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. return $this->dataConstructForPicker($coId, $term,$coPersonIds); } /** * Return array of Working Groups for display on COManage site. * Logic is for each WG to have one key=>value of main WG name, then array of all associated * Groups. * * NOTE: This is a major hack due to Grouper not giving us the right logic for displaying, so have to run all * groups through a mapped listing of types of Groups in a WG to see if match and then parse and massage to display * * @param array $groups Listing of Groups * @return array Listing of Groups in WG format for display * @since COmanage Registry v4.4.0 */ private function getFriendlyWorkingGroupName(array $groups, string $method) { $arrayIndex = 0; $workingGroups = array(); //First need to loop through all groups and pull in all top levels $topLevelWG = array(); foreach ($groups as $group) { foreach ($this->wgStemsTopLevel as $stem) { $len = strlen($stem); if (substr(strtolower($group['name']), 0, $len) === $stem) { $stemSections = explode(':', $group['name']); //Get a third section, since will always by after ref:something:here if (in_array($stemSections[2], $topLevelWG) === false) { $topLevelWG[] = $stemSections[2]; } } } } //Loop through groups to see if possibly part of a Working Group foreach ($groups as &$group) { foreach ($this->wgStemsAllGroups as $stem) { $len = strlen($stem); // if match to name of group within WG mapping then start making a WG group array if (substr(strtolower($group['name']), 0, $len) === $stem) { $tempGroup = $group; if ($stem == 'ref:incommon-collab' || $stem == 'ref:internet2-collab') { $mainGroup = true; } else { $mainGroup = false; } $stemSections = explode(':', $group['name']); $displaySections = explode(':', $group['displayName']); //Get second to last stem section $sectionCount = 2; //If group not part of a top level WG, then do not show! if (in_array($stemSections[$sectionCount], $topLevelWG) === false) { break; } $tempGroup['WGName'] = $stemSections[$sectionCount]; $tempGroup['WGShowName'] = $displaySections[$sectionCount]; // Get user type, which is after the WG name if (isset($stemSections[$sectionCount + 1])) { $tempGroup['WGRole'] = $stemSections[$sectionCount + 1]; } else { $tempGroup['WGRole'] = ''; } $appCount = 0; $appName = ''; foreach ($stemSections as $stemSection) { //Skip first entry if ($appCount > 0) { if ($appCount < $sectionCount) { if ($appCount == 1) { $appName = $stemSection; } else { $appName = $appName . " - " . $stemSection; } } } $appCount += 1; } //changed the way email list are displayed to actually show lists email address. if ($appName == 'sympa - internet2' || $appName == 'sympa - incommon') { if ($appName == 'sympa - internet2') { $appName = $tempGroup['WGName'] . '@lists.' . 'internet2.edu'; } else { $appName = $tempGroup['WGName'] . '@lists.' . 'incommon.org'; } } $tempGroup['WGApp'] = $appName; if ($method == 'member') { if(!$mainGroup) { $workingGroups[] = $tempGroup; unset($groups[$arrayIndex]); } } else { $workingGroups[] = $tempGroup; unset($groups[$arrayIndex]); } } } $arrayIndex += 1; } $finalWorkingGroups = array(); foreach ($workingGroups as $workingGroup) { //Need to set first group in final Working Group array if (count($finalWorkingGroups) == 0) { $finalWorkingGroups[] = array( 'WGName' => $workingGroup['WGName'], 'WGShowName' => $workingGroup['WGShowName'], 'Groups' => array($workingGroup) ); } else { $foundMatch = false; foreach ($finalWorkingGroups as &$finalWorkingGroup) { if ($finalWorkingGroup['WGName'] == $workingGroup['WGName']) { $finalWorkingGroup['WGShowName'] = $workingGroup['WGShowName']; $finalWorkingGroup['Groups'][] = $workingGroup; $foundMatch = true; } } if (!$foundMatch) { $finalWorkingGroups[] = array( 'WGName' => $workingGroup['WGName'], 'WGShowName' => $workingGroup['WGShowName'], 'Groups' => array($workingGroup) ); } } } $friendlyGroups = array_values($groups); //Now need to add the groups back together for one set foreach ($friendlyGroups as $friendlyGroup) { $finalWorkingGroups[] = $friendlyGroup; } return $finalWorkingGroups; } /** * Retrieve the identifier for a CO Person * * @param int $co_person_id * @param string $ident_type * * @return string|null */ public function getIdentifierFromPersonId(int $co_person_id, string $ident_type): ?string { if(empty($co_person_id) || empty($ident_type)) { return null; } // Find the identifier $args = array(); $args['conditions']['Identifier.type'] = $ident_type; $args['conditions']['Identifier.status'] = SuspendableStatusEnum::Active; $args['conditions']['Identifier.co_person_id'] = $co_person_id; $args['contain'] = false; $this->Identifier = ClassRegistry::init('Identifier'); $identifier = $this->Identifier->find('first', $args); if (!empty($identifier)) { return $identifier['Identifier']['identifier']; } return null; } /** * Get members associated to a specific Grouper Group * NOTE: This list only shows members, it does not pull in other groups that may be attached in Grouper as * members * * @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 getGroupMembers(string $actorUserId, string $groupName, array $cfg): array { $this->initApi($cfg); try { $groupMembers = $this->grouperAPI->getGroupMembers($actorUserId, $groupName); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } if (count($groupMembers) < 1) { return $groupMembers; } $finalMembers = []; foreach ($groupMembers as $member) { if ($member['sourceId'] !== 'g:gsa') { $finalMembers[] = $member; } } return $finalMembers; } /** * Return all Grouper Groups that * - the User(me) has a role of owner/admin * - the User (member User) is a member * * @param string $managerId User(me) has a role of owner/admin * @param string $userId User (member User) is a member * @param array $cfg * * @return array * @throws GrouperLiteWidgetException * @since COmanage Registry v4.4.0 */ public function getManagedUsers(string $managerId, string $userId, array $cfg): array { if(empty($userId) || empty($managerId)) { return false; } $this->initApi($cfg); try { $resultsManagerAdmin = $this->grouperAPI->getUserMemberships($managerId, $managerId, GrouperGroupTypeEnum::ADMIN); $resultsManagerUpdate = $this->grouperAPI->getUserMemberships($managerId, $managerId, GrouperGroupTypeEnum::UPDATE); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } $managerGroupSet = $this->removeDuplicates($resultsManagerAdmin, $resultsManagerUpdate); try { // Groups the user is a member of $membersGroup = $this->grouperAPI->getUserGroups($managerId, $userId); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } // Extract the names of the Groups the member-user is a member of $memberGroupNames = Hash::extract($membersGroup, '{n}.name'); // Return the groups the user can join and is not a member of return array_values( // Restart indexing from 0(zero) on the final array array_filter( // Return the groups the member-user is a member $managerGroupSet, static fn($value) => in_array($value['name'], $memberGroupNames) ) ); } /** * Return all Grouper Groups that the User has a role of owner/admin * * @param string $userId * @param string $actorUserId * @param array $cfg * * @return array * @throws GrouperLiteWidgetException * @since COmanage Registry v4.4.0 */ public function getOwnedGroups(string $userId, string $actorUserId, array $cfg): array { if(empty($userId) || empty($actorUserId)) { return false; } $this->initApi($cfg); try { $resultsAdmin = $this->grouperAPI->getUserMemberships($userId, $actorUserId, GrouperGroupTypeEnum::ADMIN); $resultsUpdate = $this->grouperAPI->getUserMemberships($userId, $actorUserId, GrouperGroupTypeEnum::UPDATE); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } return $this->removeDuplicates($resultsAdmin, $resultsUpdate); } /** * Potential use was for creating an adhoc group by a user, not associated to WG. * * Gets all Stems/Folders where User is admin/owner * * @param string $userId * @param string $actorUserId * * @return array Array of Stems/Folders from Grouper * @throws GrouperLiteWidgetException */ public function getOwnedStems(string $userId, string $actorUserId): array { try { return $this->grouperAPI->getUserMemberships($userId, $actorUserId, GrouperGroupTypeEnum::ADMIN); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } } /** * Search for Groups/Lists related to Search term. * * Will import all records the user can see and then do search in this code rather than call Grouper WS Search * functionality. This is because the grouperName is autogenerated and this app needs to search * attributes which the Grouper WS does not do. * * @param string $userId * @param string $searchCriteria * @param string $searchPage * @param array $cfg * * @return array Records that meet search criteria * @throws GrouperLiteWidgetException * @since COmanage Registry v4.4.0 */ public function getSearchedGroups(string $userId, string $actorUserId, string $searchCriteria, string $searchPage, array $cfg): array { $this->initApi($cfg); try { // Breakout page where search was called and forward to appropriate method for processing $pageResults = isset($searchPage) ? $this->$searchPage(userId: $userId, actorUserId: $actorUserId, cfg: $cfg) : []; $returnResults = []; foreach ($pageResults as $result) { $compare = $result; unset($compare['extension']); unset($compare['uuid']); unset($compare['enabled']); unset($compare['typeOfGroup']); unset($compare['idIndex']); $match = preg_grep("/$searchCriteria/i", $compare); if (!empty($match)) { $returnResults[] = $result; } } return $searchCriteria == 'getSearchedGroups' ? $this->getFriendlyWorkingGroupName($returnResults, 'member') : $returnResults; } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } } /** * Determine if a User can use the Grouper Template to create a Working Group. * * @param string $userId User ID * @param string $groupName Group Name * @param array $cfg * * @return bool T for True and F for False * @throws GrouperLiteWidgetException * @since COmanage Registry v4.4.0 */ public function isGroupMember(string $userId, string $groupName, array $cfg): bool { $this->initApi($cfg); try { $isMember = $this->grouperAPI->isMemberOfGroup($groupName, $userId); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } return (bool)$isMember; } /** * Determine if a User can use the Grouper Template to create a Working Group. * * @param string $userId User ID * @param array $cfg * * @return bool * @throws GrouperLiteWidgetException * @since COmanage Registry v4.4.0 */ public function isTemplateUser(string $userId, array $cfg): bool { $this->initApi($cfg); try { return $this->grouperAPI->isMemberOfGroup(GrouperSpecialGroups::TEMPLATE_CREATION_GROUP, $userId); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } } /** * Internal process used by other functions to fetch Groups the User is a member of * * @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(string $actorUserId, string $userId, array $cfg) { $this->initApi($cfg); try { return $this->grouperAPI->getUserGroups($actorUserId, $userId); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } } /** * Return all Groups the User can JOIN * Get all Groups with Optin attribute set and display ones User can join. * Will Match up with Groups User is already a member of to determine which Optin groups to not display * * @param string $userId * @param string $actorUserId * @param array $cfg * * @return array Listing of Optin groups available in Grouper * @throws GrouperLiteWidgetException Captured in Controller * @since COmanage Registry v4.4.0 */ public function optinGroups(string $userId, string $actorUserId, array $cfg): array { $this->initApi($cfg); try { // Groups the user can join or leave $joinOrLeave = $this->grouperAPI->getUserMemberships($userId, $actorUserId, GrouperGroupTypeEnum::OPTINS); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } try { // Groups the user is a member of $userGroups = $this->grouperAPI->getUserGroups($actorUserId, $userId); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } // I am currently not a member to any Group. Return everything if(empty($userGroups)) { return $joinOrLeave; } // 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 return array_values( // Restart indexing from 0(zero) on the final array array_filter( // Return the groups I am currently not a member $joinOrLeave, static fn($value) => !in_array($value['name'], $userGroupsNames) ) ); } /** * Remove a member from a specific Grouper Group * * @param string $actorUserId * @param string $groupName * @param string $removeUserId * @param array $cfg * * @return bool success of Request * @throws GrouperLiteWidgetException Captured in Controller * @throws JsonException Captured in Controller * @since COmanage Registry v4.4.0 */ public function removeGroupMember(string $actorUserId, string $groupName, string $removeUserId, array $cfg): bool { $this->initApi($cfg); return $this->grouperAPI->removeGroupMember($actorUserId, $groupName, $removeUserId); } /** * Removes duplicates where the user is the owner and the updater of the group. Just one line instead of two. * * @param array $arrOne * @param array $arrTwo * @return array */ public function removeDuplicates(array $arrOne, array $arrTwo) { // If one of the arrays is empty then return the other one as is if(empty($arrOne) && empty($arrTwo)) { return []; } else if(empty($arrOne)) { return $arrTwo; } else if(empty($arrTwo)) { return $arrOne; } $uniqueArr = Hash::combine($arrOne, '{n}.uuid', '{n}'); foreach($arrTwo as $data) { if(!isset($uniqueArr[ $data['uuid'] ])) { $uniqueArr[ $data['uuid'] ] = $data; } } return array_values($uniqueArr); } }