diff --git a/Config/Schema/empty b/Config/Schema/empty deleted file mode 100644 index e69de29..0000000 diff --git a/Config/Schema/schema.xml b/Config/Schema/schema.xml index 1e39027..9b65be3 100644 --- a/Config/Schema/schema.xml +++ b/Config/Schema/schema.xml @@ -25,7 +25,7 @@ must be specified in raw SQL, which needs the prefixed table name. --> - +
@@ -34,8 +34,10 @@ REFERENCES cm_co_dashboard_widgets(id) + + @@ -44,9 +46,34 @@ - + co_dashboard_widget_id
+ + + + + + + + REFERENCES cm_co_grouper_lite_widgets(id) + + + REFERENCES cm_co_people(id) + + + REFERENCES cm_co_people(id) + + + + + + co_grouper_lite_widget_id + + + co_person_id + +
\ No newline at end of file diff --git a/Controller/ActAsPeopleController.php b/Controller/ActAsPeopleController.php new file mode 100644 index 0000000..b4ddd18 --- /dev/null +++ b/Controller/ActAsPeopleController.php @@ -0,0 +1,292 @@ + [ + 'validatePost' => false, + 'csrfUseOnce' => false + ] + ]; + + + public $name = 'ActAsPeople'; + + /** + * Overrides parent beforeFilter to verify that Session contains the correct API settings. + * + * @return void + */ + public function beforeFilter() + { + parent::beforeFilter(); + + if(empty($this->request->params['named']['glid'])) { + throw new InvalidArgumentException(_txt('er.grouperlite.glid'), + HttpStatusCodesEnum::HTTP_BAD_REQUEST); + } + $this->response->disableCache(); + $this->RequestHandler->addInputType('json', ['json_decode', true]); + + $this->Security->unlockedActions = [ + 'delete', + 'upsert', + ]; + + // Get the config + $args = array(); + $args['conditions']['CoGrouperLiteWidget.id'] = $this->request->params['named']['glid']; + $args['contain'] = false; + $cfg = $this->CoGrouperLiteWidget->find('first', $args); + // Set the config so that everybody can access it + $this->CoGrouperLiteWidget->setConfig($cfg); + } + + /** + * Update or Insert an ActAs user + * + * @since COmanage Registry v4.4.0 + */ + + public function upsert(): void + { + $this->request->allowMethod('ajax'); + $this->layout = 'ajax'; + + if(!$this->request->is('restful') + && !$this->request->is('ajax')) { + throw new RuntimeException('HTTP Method Not Allowed', HttpStatusCodesEnum::HTTP_METHOD_NOT_ALLOWED); + } + + $data = $this->request->data; + + $fullName = $data['fullname']; + unset($data['fullname']); + + $args = []; + $args['conditions']['ActAsPerson.co_person_id'] = $data['co_person_id']; + if(!empty($this->request->named['actrecordId'])) { + $args['conditions']['ActAsPerson.id'] = $this->request->named['actrecordId']; + } + $args['conditions']['ActAsPerson.act_as_co_person_id'] = $data['act_as_co_person_id']; + $args['contain'] = false; + if(!empty($this->ActAsPerson->find('first', $args))) { + $this->Api->restResultHeader(HttpStatusCodesEnum::HTTP_CONFLICT, 'Already Exists'); + $error = ErrorsEnum::Conflict; + $this->set(compact('error')); + $this->set('_serialize', 'error'); + return; + } + + // We will update + if(!empty($this->request->named['actrecordId'])) { + $this->ActAsPerson->id = $this->request->named['actrecordId']; + } + + try { + $ret = $this->ActAsPerson->save($data); + } + catch(Exception $e) { + $err = filter_var($e->getMessage(),FILTER_SANITIZE_SPECIAL_CHARS); + $this->Flash->set($err, ['key' => 'error']); + } + + if($ret) { + $this->Api->restResultHeader(HttpStatusCodesEnum::HTTP_CREATED, 'Added'); + // I am registering a flash message. It will render after the frontend triggers a reload + $this->Flash->set("Act As {$fullName} Enabled" , ['key' => 'success']); + + $args = []; + $args['conditions']['ActAsPerson.id'] = $this->ActAsPerson->id; + $args['contain'] = false; + $data = $this->ActAsPerson->find('first', $args); + + $resp = [ + 'ActAsPerson' => $data['ActAsPerson'] + ]; + $this->set(compact('resp')); + $this->set('_serialize', 'resp'); + } else { + $fs = $this->ActAsPerson->invalidFields(); + + if(!empty($fs)) { + $this->Api->restResultHeader(HttpStatusCodesEnum::HTTP_BAD_REQUEST, 'Invalid Fields'); + $this->Flash->set('Invalid Fields', ['key' => 'error']); + $this->set(compact('fs')); + $this->set('_serialize', 'fs'); + } elseif ($e + && isset(_txt('en.http.status.codes')[$e->getCode()]) ) { + $this->Api->restResultHeader($e->getCode(), _txt('en.http.status.codes')[$e->getCode()]); + if(!empty($e->getMessage())) { + $vv_error = $e->getMessage(); + $this->set(compact('vv_error')); + $this->set('_serialize', 'vv_error'); + $this->Flash->set($e->getMessage(), ['key' => 'error']); + } + } else { + $this->Api->restResultHeader(HttpStatusCodesEnum::HTTP_INTERNAL_SERVER_ERROR, 'Other Error'); + $error = ErrorsEnum::Error; + $this->set(compact('error')); + $this->set('_serialize', 'error'); + } + } + } + + /** + * Handle an Act As Person Delete request. + * + * @since COmanage Registry v4.4.0 + */ + + public function delete():void + { + $this->request->allowMethod('ajax'); + $this->layout = 'ajax'; + + if(!$this->request->is('delete') + && !$this->request->is('ajax') + ) { + throw new RuntimeException('HTTP Method Not Allowed', HttpStatusCodesEnum::HTTP_METHOD_NOT_ALLOWED); + } + + if(empty($this->request->named['copersonid']) + || empty($this->request->named['act_as_copersonid'])) { + $this->log(__METHOD__ . '::message: Named Parameter missing', LOG_ERROR); + throw new BadRequestException('Named Parameter missing'); + } + + try { + $conditions = [ + 'ActAsPerson.co_person_id' => $this->request->named['copersonid'], + 'ActAsPerson.act_as_co_person_id' => $this->request->named['act_as_copersonid'] + ]; + + $ret = $this->ActAsPerson->deleteAll($conditions, true, true); + } catch(Exception $e) { + $error = filter_var($e->getMessage(),FILTER_SANITIZE_SPECIAL_CHARS); + $this->Api->restResultHeader(HttpStatusCodesEnum::HTTP_INTERNAL_SERVER_ERROR, 'Other Error'); + $this->set(compact('error')); + $this->set('_serialize', 'error'); + return; + } + + // Set flash message + if(!$ret) { + $error = ErrorsEnum::NotDeleted; + $this->Api->restResultHeader(HttpStatusCodesEnum::HTTP_BAD_REQUEST, 'Bad Request'); + $this->set(compact('error')); + $this->set('_serialize', 'error'); + return; + } + + // We will register a Flash message here. The frontend will reload and it will be rendered + $this->Flash->set('Act As Disabled', ['key' => 'success']); + + // We do not redirect but we pass data back to the frontend because we + // want to keep the view structure as simple as possible. If we redirect we will + // have to add more JSON view infrastructure. We will let the frontend + // handle the redirect + $this->Api->restResultHeader(HttpStatusCodesEnum::HTTP_OK, 'Success'); + $success = ['status' => 'Success', 'message' => 'Succeeded']; + $this->set(compact('success')); + $this->set('_serialize', 'success'); + } + + + /** + * NOTE: All permissions will be done on the Grouper side. All Authenticated users will be able to + * use this plugin for self-admin of groups. + * + * Authorization for this Controller, called by Auth component + * - precondition: Session.Auth holds data used for authz decisions + * - postcondition: $permissions set with calculated permissions + * + * @return array|bool Permissions + * @since COmanage Registry v4.4.0 + */ + public function isAuthorized(): array|bool + { + $roles = $this->Role->calculateCMRoles(); + $this->set('roles', $roles); + + $pids = $this->parsePersonID($this->request->data); + + $cfg = $this->CoGrouperLiteWidget->getConfig(); + // Find the identifier + $args = array(); + $args['conditions']['Identifier.type'] = $cfg['CoGrouperLiteWidget']['identifier_type']; + $args['conditions']['Identifier.status'] = SuspendableStatusEnum::Active; + $args['conditions']['Identifier.co_person_id'] = !empty($roles['copersonid']) ? $roles['copersonid'] : $pids['copersonid']; + $args['contain'] = false; + + $identifiers = $this->Identifier->find('first', $args); + if(!empty($identifiers) + && is_array($identifiers) + && isset($identifiers['Identifier']['identifier']) + ) { + $this->setUserId($identifiers['Identifier']['identifier']); + } + + // Find if the user belongs to Group + $eligibleGroup = $cfg['CoGrouperLiteWidget']['act_as_grp_name']; + $isActAsEligibilityGroupmember = false; + + if(!empty($eligibleGroup)) { + $isActAsEligibilityGroupmember = $this->GrouperGroup + ->isGroupMember($this->getUserId(), $eligibleGroup, $cfg); + } + + // Determine what operations this user can perform + // Construct the permission set for this user, which will also be passed to the view. + $p = []; + + $p['delete'] = $isActAsEligibilityGroupmember; + $p['upsert'] = $isActAsEligibilityGroupmember; + + $this->set('permissions', $p); + + return ($p[$this->action]); + } + + /** + * @return null + */ + public function getUserId() + { + return $this->userId; + } + + + /** + * @param null $userId + */ + private function setUserId($userId): void + { + $this->userId = $userId; + } +} \ No newline at end of file diff --git a/Controller/CoGrouperLitesController.php b/Controller/CoGrouperLiteWidgetsController.php similarity index 69% rename from Controller/CoGrouperLitesController.php rename to Controller/CoGrouperLiteWidgetsController.php index ccbaa42..bd6e92c 100644 --- a/Controller/CoGrouperLitesController.php +++ b/Controller/CoGrouperLiteWidgetsController.php @@ -2,16 +2,18 @@ App::uses("SDWController", "Controller"); -class CoGrouperLitesController extends SDWController { +class CoGrouperLiteWidgetsController extends SDWController { public $helpers = array('Html', 'Form', 'Flash'); public $components = array('Flash'); // Class name, used by Cake - public $name = "CoGrouperLites"; + public $name = "CoGrouperLiteWidgets"; public $uses = array( - "GrouperLite.CoGrouperLite" + 'GrouperLiteWidget.CoGrouperLiteWidget', + 'Identifier', + 'Co', ); /** @@ -21,16 +23,29 @@ class CoGrouperLitesController extends SDWController { */ function beforeRender() { + parent::beforeRender(); + $this->set('title_for_layout', _txt('pl.grouperlite.config.edit.title')); - parent::beforeRender(); + // Gather the available identifier address types for the config form + $this->set('vv_available_types', $this->Identifier->types($this->cur_co['Co']['id'], 'type')); + // Pass the config + $cfg = $this->CoGrouperLiteWidget->getConfig(); + $this->set('vv_config', $cfg); } + + /** + * Render the widget according to the requested user and current configuration. + * + * @since COmanage Registry v4.4.0 + * @param Integer $id CO Grouper Lite Widget ID + */ public function display($id) { - $cfg = $this->CoGrouperLite->getConfig(); + $cfg = $this->CoGrouperLiteWidget->getConfig(); $this->set('pl_grouperlite_index_url', Router::url([ - 'plugin' => "grouper_lite", + 'plugin' => 'grouper_lite', 'controller' => 'grouper_groups', 'action' => 'index', 'co' => $this->cur_co['Co']['id'], @@ -43,6 +58,14 @@ public function display($id) { // Pass the config so we know which div to overwrite $this->set('vv_config', $cfg); + + // Pass the Dashboard Widget configuration + $args = array(); + $args['conditions']['CoGrouperLiteWidget.id'] = $id; + $args['contain']['CoDashboardWidget'][] = 'CoDashboard'; + + $codw = $this->CoGrouperLiteWidget->find('first', $args); + $this->set('vv_codw', $codw); } diff --git a/Controller/GrouperGroupsController.php b/Controller/GrouperGroupsController.php index 03f0146..f50c289 100644 --- a/Controller/GrouperGroupsController.php +++ b/Controller/GrouperGroupsController.php @@ -26,8 +26,9 @@ */ App::uses('Validator', 'Vendor/cakephp/Validation'); -App::uses('CoGrouperLite', 'GrouperLite.Model/'); -App::uses('GrouperGroup', 'GrouperLite.Model/'); +App::uses('CoGrouperLite', 'GrouperLiteWidget.Model/'); +App::uses('GrouperGroup', 'GrouperLiteWidget.Model/'); +App::uses('ActAsPerson', 'GrouperLiteWidget.Model/'); App::uses('Identifier', 'Model'); /** @@ -35,28 +36,87 @@ * Main Class for Grouper Lite functionality * */ -class GrouperGroupsController extends GrouperLiteAppController +class GrouperGroupsController extends GrouperLiteWidgetAppController { - public $helpers = array('Html', 'Form', 'Flash'); - public $uses = array('GrouperLite.GrouperGroup', 'CoPerson'); - public $components = array('Flash', 'Paginator', 'RequestHandler', 'Security' => array( - 'validatePost' => false, - 'csrfUseOnce' => false - )); + public $helpers = ['Html', 'Form', 'Flash']; + + // Dynamic properties are deprecated, so we will define the property here + private $userId = null; + private $actAsIdentifier = null; + + public $uses = [ + 'GrouperLiteWidget.GrouperGroup', + 'GrouperLiteWidget.CoGrouperLiteWidget', + 'GrouperLiteWidget.ActAsPerson', + 'Identifier', + 'CoPerson' + ]; + + public $components = [ + 'Flash', + 'Paginator', + 'RequestHandler', + 'Security' => [ + 'validatePost' => false, + 'csrfUseOnce' => false + ] + ]; public $name = 'GrouperGroups'; /** - * Overrides parent beforeFilter to verify that Session contains the correct API settings. + * Add a new member to a group + * Called from all pages via AJAX call * - * @return CakeResponse|void|null + * @throws JsonException + * @throws Exception + */ + public function addSubscriber(): void + { + $this->layout = null; + $this->autoRender = false; + + $groupName = $this->request->data['group']; + $addUserId = $this->request->data['userId']; + + // Need to see if coming from AdHoc or from a WG (Working Group) + $groupNameFormatted = strpos($groupName, ':') === false ? 'ref:incommon-collab:' . $groupName . ':users' + : $groupName; + + try { + if(!$this->GrouperGroup->addGroupMember($this->getActAsIdentifier(), + $groupNameFormatted, + $addUserId, + $this->CoGrouperLiteWidget->getConfig())) { + // The Request returned unsuccessful, but we have not more infomration. In this case we will just return + // forbidden since we do not actually now what happened + $this->restResponse(HttpStatusCodesEnum::HTTP_FORBIDDEN); + } + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); + throw $e; + } + + $this->restResponse(HttpStatusCodesEnum::HTTP_CREATED); + } + + /** + * Overrides parent beforeFilter to verify that Session contains the correct API settings. * + * @return void */ public function beforeFilter() { parent::beforeFilter(); - $this->Security->unlockedActions = array( + if(empty($this->request->params['named']['glid'])) { + throw new InvalidArgumentException(_txt('er.grouperlite.glid'), + HttpStatusCodesEnum::HTTP_BAD_REQUEST); + } + $this->response->disableCache(); + $this->RequestHandler->addInputType('json', ['json_decode', true]); + + $this->Security->unlockedActions = [ 'removeSubscriber', 'addSubscriber', 'joinGroup', @@ -64,430 +124,238 @@ public function beforeFilter() 'groupMember', 'groupOptin', 'groupOwner' - ); + ]; - //Need to find which plugin instance choosing, if more than one from cm_co_grouper_lites - // table being used in COmanage. - $grouperConnData = $this->Session->read('Plugin.Grouper.Api'); - if ($this->Session->check('Plugin.Grouper.Api.id') && count($grouperConnData) == 10) { - if (isset($this->passedArgs['glid'])) { - if ($this->Session->read('Plugin.Grouper.Api.id') !== $this->passedArgs['glid']) { - $this->setConnection(); - } - } - } else { - if (!isset($this->passedArgs['glid'])) { - //If user links directly to GrouperLite url, will be redirected to main index to choose an - //appropriate Dashboard Widget. - return $this->redirect('/'); - } - $this->setConnection(); - } + // Get the config + $args = array(); + $args['conditions']['CoGrouperLiteWidget.id'] = $this->request->params['named']['glid']; + $args['contain'] = false; + $cfg = $this->CoGrouperLiteWidget->find('first', $args); + // Set the config so that everybody can access it + $this->CoGrouperLiteWidget->setConfig($cfg); - //Also check for CO of Organization - if (!$this->Session->check('Plugin.Grouper.Api.co')) { - $this->Session->write('Plugin.Grouper.Api.co', $this->passedArgs['co']); + // XXX We will make the ActAs calculations here the beforeRender callback does not always fire + // This is happening because we have a mixed schema with API calls as well as MVC structure. + // Get the ActAs User Data + $roles = $this->Role->calculateCMRoles(); + $co_person_id = $roles['copersonid']; + // Get the act as data from the database + if(!empty($co_person_id)) { + $args = []; + $args['conditions']['ActAsPerson.co_person_id'] = $co_person_id; + $args['contain'] = false; + $act_as_record = $this->ActAsPerson->find('first', $args); + } + $this->set('vv_act_as_people', []); + + // Get ActAs configuration + if(!empty($act_as_record)) { + $act_as_person = $this->GrouperGroup->dataConstructForPicker($this->cur_co['Co']['id'], + PeoplePickerModeEnum::All, + [$act_as_record['ActAsPerson']['act_as_co_person_id']]); + $this->set('vv_act_as_people', $act_as_person); + $this->set('vv_act_as_record_id', $act_as_record['ActAsPerson']['id']); + $act_as_identifier = $this->GrouperGroup->getIdentifierFromPersonId($act_as_record['ActAsPerson']['act_as_co_person_id'], + $cfg['CoGrouperLiteWidget']['identifier_type']); + $this->setActAsIdentifier($act_as_identifier); } - } - /** - * Adding Grouper Conn info to SESSION for use in Lib/GrouperApiAccess.php - */ - private function setConnection() - { - $this->Session->write('Plugin.Grouper.Api.id', $this->passedArgs['glid']); - $this->Session->write('Plugin.Grouper.Api.co', $this->passedArgs['co']); - - //Now get the setup Dasboard instance from db for connection info. - $getConnInfo = new CoGrouperLite(); - $connectionInfo = $getConnInfo->findById($this->passedArgs['glid']); - $this->Session->write('Plugin.Grouper.Api.url', $connectionInfo['CoGrouperLite']['conn_url']); - $this->Session->write('Plugin.Grouper.Api.version', $connectionInfo['CoGrouperLite']['conn_ver']); - $this->Session->write('Plugin.Grouper.Api.user', $connectionInfo['CoGrouperLite']['conn_user']); - $this->Session->write('Plugin.Grouper.Api.pass', $connectionInfo['CoGrouperLite']['conn_pass']); - $this->Session->write('Plugin.Grouper.Api.grouperUrl', $connectionInfo['CoGrouperLite']['grouper_url']); - - $this->Session->write('Plugin.Grouper.Api.adHocHeading', $connectionInfo['CoGrouperLite']['adhoc_heading']); - $this->Session->write('Plugin.Grouper.Api.wgHeading', $connectionInfo['CoGrouperLite']['wg_heading']); - $this->Session->write('Plugin.Grouper.Api.defaultCollapse', $connectionInfo['CoGrouperLite']['default_collapse']); - } + // XXX Localizations + // Get all the localizations in an array + global $cm_texts; + global $cm_lang; - private function getConfig($page = 'groupmember') - { - $this->set('title', _txt('pl.grouperlite.title.groupmember')); - - $config = [ - "grouperbaseurl" => $this->Session->read('Plugin.Grouper.Api.grouperUrl'), - "isuserowner" => $this->GrouperGroup->isUserOwner($this->userId), - "isTemplateUser" => $this->GrouperGroup->isTemplateUser($this->userId), - "isGrouperVisible" => $this->GrouperGroup->isGrouperVisible($this->userId), - "defaultCollapse" => CakeSession::read('Plugin.Grouper.Api.defaultCollapse'), - "adHocHeading" => CakeSession::read('Plugin.Grouper.Api.adHocHeading'), - "wgHeading" => CakeSession::read('Plugin.Grouper.Api.wgHeading'), - 'co' => CakeSession::read('Plugin.Grouper.Api.co'), - 'glid' => $this->Session->read('Plugin.Grouper.Api.id'), - 'view' => $page - ]; + $texts = []; - return $config; + foreach(array_keys($cm_texts[$cm_lang]) as $k) { + if(!is_array($cm_texts[$cm_lang][$k]) + // Also skip strings that can already be dynamically changed + && !preg_match('/^em\./', $k)) { + $texts[$k] = $cm_texts[$cm_lang][$k]; + } + } + + ksort($texts); + + $this->set('vv_cm_texts', Hash::expand($texts)); } /** - * No true Index page, so sent to default page of My Membership + * Callback after controller methods are invoked but before views are rendered. + * - Precondition: A request Handler component has set $this->request * - * @return CakeResponse Redirect to MyMembership page + * @since COmanage Registry v4.4.0 */ - public function index() - { - $this->set('config', $this->getConfig('groupmember')); - } - public function groupMember() - { - $this->set('config', $this->getConfig('groupmember')); - $this->render('index'); - } + public function beforeRender() { + parent::beforeRender(); + $cfg = $this->CoGrouperLiteWidget->getConfig(); + $this->set('vv_config', $cfg); - public function groupOptin() - { - $this->set('config', $this->getConfig('groupoptin')); - $this->render('index'); + $this->set('vv_title', _txt('pl.grouperlite.title.dashboard')); + $this->set('vv_coid', $this->cur_co['Co']['id']); + + $roles = $this->Role->calculateCMRoles(); + $this->set('vv_copersonid', $roles['copersonid'] ?? null); + $this->set('vv_picker_mode', PeoplePickerModeEnum::All); + + $this->set('vv_act_as_identifier', $this->getActAsIdentifier()); + $this->set('vv_is_user_owner', + $this->GrouperGroup->isUserGroupOwner($this->getUserId(), + $this->getActAsIdentifier(), + $cfg) + ); +// $this->set('vv_is_template_user', $this->GrouperGroup->isTemplateUser($this->getUserId(), $cfg) ); +// $this->set('vv_is_grouper_visible', $this->GrouperGroup->isGrouperVisible($this->getUserId(), $cfg)); } - public function groupOwner() + /** + * @return null|string + */ + public function getActAsIdentifier(): ?string { - $this->set('config', $this->getConfig('groupowner')); - $this->render('index'); + // XXX We are anot acting as but we are impersonating an other user. As a result + // both the actor and the user need to have the same identifier + // If the actor Identifier is not set we will return the current user + return $this->actAsIdentifier ?? $this->getUserId(); } /** - * Show all members of a group - * Called from all pages via AJAX call + * 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 groupSubscribers() - { - $groupName = urldecode($this->request->query['groupname']); - - if ($this->request->is('ajax')) { - $this->response->disableCache(); - } - //Need to see if coming from AdHoc or from a WG (Working Group) - if (strpos($groupName, ':') === false) { - $groupNameFormatted = 'ref:incommon-collab:' . $groupName . ':users'; - } else { - $groupNameFormatted = $groupName; - } - - //Set initial - $scope = [ - 'groupName' => $groupNameFormatted - ]; - - try { - $subscribers = $this->GrouperGroup->membersInGroup($scope, $this->userId); - - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); + public function findSubscriber(): void + { + $this->request->allowMethod('ajax'); + $this->layout = 'ajax'; - $this->Flash->set(_txt('pl.grouperlite.message.flash.group-detail-members-failed'), array('key' => 'error')); + // What search mode should we use? + if(empty($this->request->query['mode'])) { + $this->Api->restResultHeader(HttpStatusCodesEnum::HTTP_BAD_REQUEST, 'Mode Not Specified'); + return; } - if(count($subscribers) < 1){ - $this->response->type('json'); - $this->response->statusCode(404); - //$this->response->body(json_encode(array('status' => 'ERROR', 'message' => 'NO ACCESS'))); - $this->response->send(); - $subscribers = ''; - } elseif (count($subscribers) == 1 && $subscribers[0]['sourceId'] == "NoAccess") { - $this->response->type('json'); - $this->response->statusCode(403); - //$this->response->body(json_encode(array('status' => 'ERROR', 'message' => 'NO ACCESS'))); - $this->response->send(); - $subscribers = ''; - } - $this->set(compact('subscribers')); - $this->set('_serialize', 'subscribers'); + $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'); } /** - * Add a new member to a group - * Called from all pages via AJAX call + * @param bool $self By passes the actAsIdentifier condition * + * @return null|string */ - public function addSubscriber() + public function getUserId(bool $self = false): ?string { - $groupName = urldecode($this->request->query['group']); - $addUserId = urldecode($this->request->query['userId']); - - if ($this->request->is('ajax')) { - $this->response->disableCache(); - } - - //Need to see if coming from AdHoc or from a WG (Working Group) - if (strpos($groupName, ':') === false) { - $groupNameFormatted = 'ref:incommon-collab:' . $groupName . ':users'; - } else { - $groupNameFormatted = $groupName; - } - - //Set initial - $scope = [ - 'groupName' => $groupNameFormatted, - 'addUserId' => $addUserId - ]; - - try { - $resultAdd = $this->GrouperGroup->addMemberToGroup($scope, $this->userId); - - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); - $subscribers = 'ERROR'; - - // $this->Flash->set(_txt('pl.grouperlite.message.flash.group-detail-members-failed'), array('key' => 'error')); - } - - if ($resultAdd == 'SUCCESS') { - // Do nothing - } elseif ($resultAdd == 'EXCEPTION') { - $this->response->type('json'); - $this->response->statusCode(404); - $this->response->body(json_encode(array('status' => 'ERROR', 'message' => 'EXCEPTION'))); - $this->response->send(); - $resultAdd = ''; - } else { - $this->response->type('json'); - $this->response->statusCode(401); - $this->response->body(json_encode(array('status' => 'ERROR', 'message' => 'ERROR'))); - $this->response->send(); - $resultAdd = ''; + if($self) { + return $this->userId; } - $this->set(compact('resultAdd')); - $this->set('_serialize', 'resultAdd'); - + // XXX We are anot acting as but we are impersonating an other user. As a result + // both the actor and the user need to have the same identifier + return $this->actAsIdentifier ?? $this->userId; } /** - * 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. + * GroupMember vue route for rendering groupmemberapi results * + * @return void */ - public function findSubscriber() + public function groupMember(): void { + $this->render('index'); + } - if ($this->request->is('ajax')) { - $this->response->disableCache(); - } - - // $groupName = urldecode($this->request->query['group']); - $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); - - foreach($hits as $hit) { - if ($hit['CoPerson']['status'] == 'A') { - $coPersonIds[] = $hit['CoPerson']['id']; - } - } - // $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 - - $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 - ); - } - } + /** + * GroupOptin vue route for rendering groupoptinapi results + * + * @return void + */ + public function groupOptin(): void + { + $this->render('index'); + } - $this->set(compact('matches')); - $this->set('_serialize', 'matches'); + /** + * GroupOwner vue route for rendering groupownerapi results + * + * @return void + */ + public function groupOwner(): void + { + $this->render('index'); } /** - * Remove a member from a group + * Show all members of a group * Called from all pages via AJAX call * + * @throws Exception */ - public function removeSubscriber() + public function groupSubscribers(): void { - $groupName = urldecode($this->request->query['group']); - $remUserId = urldecode($this->request->query['userId']); - - if ($this->request->is('ajax')) { - $this->response->disableCache(); - } + $groupName = urldecode($this->request->query['groupname']); + $subscribers = []; //Need to see if coming from AdHoc or from a WG (Working Group) - if (strpos($groupName, ':') === false) { - $groupNameFormatted = 'ref:incommon-collab:' . $groupName . ':users'; - } else { - $groupNameFormatted = $groupName; - } - - //Set initial - $scope = [ - 'groupName' => $groupNameFormatted, - 'remUserId' => $remUserId - ]; + $groupNameFormatted = strpos($groupName, ':') === false ? 'ref:incommon-collab:' . $groupName . ':users' + : $groupName; try { - $resultRemove = $this->GrouperGroup->removeMemberToGroup($scope, $this->userId); - + $subscribers = $this->GrouperGroup->getGroupMembers($this->getActAsIdentifier(), + $groupNameFormatted, + $this->CoGrouperLiteWidget->getConfig()); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); - - //$this->Flash->set(_txt('pl.grouperlite.message.flash.group-detail-members-failed'), array('key' => 'error')); + throw $e; } - if ($resultRemove == 'SUCCESS') { - // Do nothing - } else { - $this->response->type('json'); - $this->response->statusCode(404); - $this->response->body(json_encode(array('status' => 'ERROR', 'message' => 'ERROR'))); - $this->response->send(); - $resultRemove = ''; - } - $this->set(compact('resultRemove')); - $this->set('_serialize', 'resultRemove'); + $this->set(compact('subscribers')); + $this->set('_serialize', 'subscribers'); } /** * Listing of all Grouper Groups owned/admin by User Or search those Grouper Groups */ - public function groupOwnerApi() - { - if ($this->request->is('ajax')) { - $this->response->disableCache(); - } - + public function groupOwnerApi(): void { //Set initial setting - $scope = [ - 'userId' => $this->userId + $arguments = [ + 'userId' => $this->getUserId(), + 'actorUserId' => $this->getActAsIdentifier(), + 'cfg' => $this->CoGrouperLiteWidget->getConfig() ]; if (isset($this->request->query['search'])) { $searchCriteria = urldecode($this->request->query['search']); - $this->set('searchcriteria', $searchCriteria); - try { - //Add settings for search Owned Groups - $scope['method'] = 'getSearchedGroups'; - $scope['searchcriteria'] = $searchCriteria; - $scope['searchpage'] = 'ownerGroups'; - - $groupowners = $this->GrouperGroup->getSearchedGroups($scope); + //Add settings for search Owned Groups + $arguments['searchCriteria'] = $searchCriteria; + $arguments['searchPage'] = 'getOwnedGroups'; - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ' Search: ' . var_export($e->getMessage(), true)); - $this->response->type('json'); - $this->response->statusCode(500); - $this->response->send(); - $this->set('groupsowners', array()); - - $this->Flash->set(_txt('pl.grouperlite.message.flash.owner-group-failed'), array('key' => 'error')); - return; - } + $func = 'getSearchedGroups'; + $errorHint = 'Search'; } else { - try { - $scope['method'] = 'ownerGroups'; - - $groupowners = $this->GrouperGroup->ownerGroups($scope); - - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); - $this->response->type('json'); - $this->response->statusCode(500); - $this->response->send(); - $this->set('groupsowners', array()); + $func = 'getOwnedGroups'; + $errorHint = ''; + } + try { + $groupowners = $this->GrouperGroup->$func(...$arguments); + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . "::{$errorHint}: " . var_export($e->getMessage(), true)); + $this->restResponse(HttpStatusCodesEnum::HTTP_INTERNAL_SERVER_ERROR, ErrorsEnum::Exception); - $this->Flash->set(_txt('pl.grouperlite.message.flash.owner-group-failed'), array('key' => 'error')); - return; - } + $this->set('groupowners', []); + $this->Flash->set(_txt('pl.grouperlite.message.flash.owner-group-failed'), array('key' => 'error')); + return; } + $this->set(compact('groupowners')); $this->set('_serialize', 'groupowners'); } @@ -497,125 +365,84 @@ public function groupOwnerApi() * This includes self-joined Optin Groups, as well as required Groups User cannot leave * */ - public function groupMemberApi() - { - if ($this->request->is('ajax')) { - $this->response->disableCache(); - } - + public function groupMemberApi(): void { //Set initial setting - $scope = [ - 'userId' => $this->userId + $arguments = [ + 'userId' => $this->getUserId(), + 'actorUserId' => $this->getActAsIdentifier(), + 'cfg' => $this->CoGrouperLiteWidget->getConfig() ]; if (isset($this->request->query['search'])) { $searchCriteria = urldecode($this->request->query['search']); - $this->set('searchcriteria', $searchCriteria); - try { - //Add settings for search Member Groups - $scope['method'] = 'getSearchedGroups'; - $scope['searchcriteria'] = $searchCriteria; - $scope['searchpage'] = 'filteredMemberOfGroups'; - $scope['ContainsWG'] = true; - - $data = $this->GrouperGroup->getSearchedGroups($scope); - - $finalData = $this->breakoutGroups($data); + //Add settings for search Member Groups + $arguments['searchCriteria'] = $searchCriteria; + $arguments['searchPage'] = 'filteredMemberOfGroups'; - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ' Search: ' . var_export($e->getMessage(), true)); - $this->response->type('json'); - $this->response->statusCode(500); - $this->response->send(); - $this->set('groupmemberships', array()); - $this->set('wgmemberships', array()); - - $this->Flash->set("Your Search Group cannot be found, please try again later.", array('key' => 'error')); - return; - } + $func = 'getSearchedGroups'; + $errorHint = 'Search'; } else { - try { - //Add setting for Group Membership - $scope['method'] = 'filteredMemberOfGroups'; + //Add setting for Group Membership + $func = 'filteredMemberOfGroups'; + $errorHint = ''; + } - $data = $this->GrouperGroup->filteredMemberOfGroups($scope); + try { + $data = $this->GrouperGroup->$func(...$arguments); + $finalData = $this->GrouperGroup->breakoutWrkFromAdHocGroups($data); + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . "::{$errorHint}: " . var_export($e->getMessage(), true)); - $finalData = $this->breakoutGroups($data); + $this->restResponse(HttpStatusCodesEnum::HTTP_INTERNAL_SERVER_ERROR, ErrorsEnum::Exception); + $this->set('groupmemberships', []); + $this->set('wgmemberships', []); - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); - $this->response->type('json'); - $this->response->statusCode(500); - $this->response->send(); - $this->set('groupmemberships', array()); - $this->set('wgmemberships', array()); - - $this->Flash->set("Your Member Group cannot be found, please try again later.", array('key' => 'error')); - return; - } + $this->Flash->set('Your Member Group cannot be found, please try again later.', array('key' => 'error')); + return; } - $this->set(compact('finalData')); - $this->set('_serialize', 'finalData'); + $this->set(compact('finalData')); + $this->set('_serialize', 'finalData'); } /** * Display all Groups a User can Join */ - public function groupOptinApi() - { - if ($this->request->is('ajax')) { - $this->response->disableCache(); - } - + public function groupOptinApi() { //Set initial setting - $scope = [ - 'userId' => $this->userId + $arguments = [ + 'userId' => $this->getUserId(), + 'actorUserId' => $this->getActAsIdentifier(), + 'cfg' => $this->CoGrouperLiteWidget->getConfig() ]; if (isset($this->request->query['search'])) { $searchCriteria = urldecode($this->request->query['search']); - $this->set('searchcriteria', $searchCriteria); - try { - //Add settings for search Optin's - $scope['method'] = 'getSearchedGroups'; - $scope['searchcriteria'] = $searchCriteria; - $scope['searchpage'] = 'optinGroups'; - - $groupoptins = $this->GrouperGroup->getSearchedGroups($scope); + //Add settings for search Optin's + $arguments['searchCriteria'] = $searchCriteria; + $arguments['searchPage'] = 'optinGroups'; - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . 'Search: ' . var_export($e->getMessage(), true)); - $this->response->type('json'); - $this->response->statusCode(500); - $this->response->send(); - $this->set('groupoptins', array()); - - $this->Flash->set("Your Optin Group Search cannot be found, please try again later.", array('key' => 'error')); - return; - } + $errorHint = 'Search'; + $func = 'getSearchedGroups'; } else { - try { - //Add settings for optinGroups - $scope['method'] = 'optinGroups'; - - $groupoptins = $this->GrouperGroup->optinGroups($scope); - - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); - $this->response->type('json'); - $this->response->statusCode(500); - $this->response->send(); - $this->set('groupoptins', array()); + $func = 'optinGroups'; + $errorHint = ''; + } + try { + $groupoptins = $this->GrouperGroup->$func(...$arguments); + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . "::{$errorHint}: " . var_export($e->getMessage(), true)); + $this->restResponse(HttpStatusCodesEnum::HTTP_INTERNAL_SERVER_ERROR, ErrorsEnum::Exception); + $this->set('groupoptins', []); - $this->Flash->set("An error occurred with the Optin Groups, please try again later.", array('key' => 'error')); - return; - } + $this->Flash->set('An error occurred with the Optin Groups, please try again later.', array('key' => 'error')); + return; } + $this->set(compact('groupoptins')); $this->set('_serialize', 'groupoptins'); } @@ -626,14 +453,14 @@ public function groupOptinApi() * Note: This is tightly coupled code to requirements, so view is hardcoded to reflect current reqs. Will need * to update when reqs change or are updated!!! * - * Editing via a template will not be supported in this version of the plugin - Bill Kaufman - * */ public function groupCreateTemplate() { if ($this->request->is('post')) { try { - $status = $this->GrouperGroup->createGroupWithTemplate($this->userId, $this->request->data); + $status = $this->GrouperGroup->createGroupWithTemplate($this->getActAsIdentifier(), + $this->request->data, + $this->CoGrouperLiteWidget->getConfig()); if ($status['status'] !== true) { $this->Flash->set($status['message'], array('key' => 'error')); @@ -650,224 +477,289 @@ public function groupCreateTemplate() $this->set('title', _txt('pl.grouperlite.title.templatecreate')); } + /** + * No true Index page, so sent to default page of My Membership + * + * @return CakeResponse Redirect to MyMembership page + */ + public function index(): void + {} /** - * Process to join a group displayed on the "Optin" page + * NOTE: All permissions will be done on the Grouper side. All Authenticated users will be able to + * use this plugin for self-admin of groups. + * + * Authorization for this Controller, called by Auth component + * - precondition: Session.Auth holds data used for authz decisions + * - postcondition: $permissions set with calculated permissions * - * @return CakeResponse Redirect back to "Optin" page + * @return array|bool Permissions + * @since COmanage Registry v3.2.0 */ - public function joinGroup() + public function isAuthorized(): array|bool { - $name = urldecode($this->request->query['GroupName']); - $display = urldecode($this->request->query['GroupDisplayName']); - - if ($this->request->is('ajax')) { - $this->response->disableCache(); + $roles = $this->Role->calculateCMRoles(); + $this->set('roles', $roles); + $pids = $this->parsePersonID($this->request->data); + + $cfg = $this->CoGrouperLiteWidget->getConfig(); + // Find the identifier + $copersonid = !empty($roles['copersonid']) ? $roles['copersonid'] : $pids['copersonid']; + $this->setUserId($this->GrouperGroup->getIdentifierFromPersonId($copersonid, + $cfg['CoGrouperLiteWidget']['identifier_type'])); + + // Find if the user belongs to Group + $eligibleGroup = $cfg['CoGrouperLiteWidget']['act_as_grp_name']; + $isActAsEligibilityGroupmember = false; + + if(!empty($eligibleGroup)) { + $isActAsEligibilityGroupmember = $this->GrouperGroup->isGroupMember($this->getUserId(self: true), + $eligibleGroup, $cfg); } + // Determine what operations this user can perform + // Construct the permission set for this user, which will also be passed to the view. + + // XXX In ActAs mode not edit actions are allowed + $p = []; + + $p['index'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['groupowner'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['groupownerapi'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['groupoptin'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['groupoptinapi'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['groupmember'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['groupmemberapi'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['getBaseConfig'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['groupSubscribers'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['addSubscriber'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']) && !$isActAsEligibilityGroupmember; + $p['findSubscriber'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['usermanager'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['usermanagerapi'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']); + $p['removeSubscriber'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']) && !$isActAsEligibilityGroupmember; + + $p['groupCreate'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']) && !$isActAsEligibilityGroupmember; + $p['joinGroup'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']) && !$isActAsEligibilityGroupmember; + $p['leaveGroup'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']) && !$isActAsEligibilityGroupmember; + $p['groupcreatetemplate'] = ($roles['cmadmin'] || $roles['coadmin'] || $roles['comember']) && !$isActAsEligibilityGroupmember; + $p['actAsAction'] = $isActAsEligibilityGroupmember; + + $this->set('permissions', $p); + + return ($p[$this->action]); + } + + + /** + * Process to join a group displayed on the "Optin" page + * + * @throws Exception + */ + public function joinGroup(): void + { + $this->layout = null; + $this->autoRender = false; + // todo: add Subscriber and joinGroup should accept the same query parameters. Currently the join Group + // accepts a GroupName, while the addSubscriber accepts a group parameter + $groupName = urldecode($this->request->query['GroupName']); + try { - if ($this->GrouperGroup->joinGroup($this->userId, $name)) { - /*$this->Flash->set( - _txt('pl.grouperlite.message.flash.join-group-success', array(filter_var($display, FILTER_SANITIZE_SPECIAL_CHARS))), - array('key' => 'success') - );*/ - $resultAdd = "Success"; - } else { - $this->response->type('json'); - $this->response->statusCode(401); - $this->response->body(json_encode(array('status' => 'ERROR', 'message' => 'NOT ADDED'))); - $this->response->send(); - $resultAdd = ''; + // Add myself + if(!$this->GrouperGroup->addGroupMember($this->getActAsIdentifier(), + $groupName, + $this->userId, + $this->CoGrouperLiteWidget->getConfig())) { + // The Request returned unsuccessful, but we have not more infomration. In this case we will just return + // forbidden since we do not actually now what happened + $this->restResponse(HttpStatusCodesEnum::HTTP_FORBIDDEN); } } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); - $this->response->type('json'); - $this->response->statusCode(404); - $this->response->body(json_encode(array('status' => 'ERROR', 'message' => 'EXCEPTION'))); - $this->response->send(); - $resultAdd = ''; + throw $e; } - $this->set(compact('resultAdd')); - $this->set('_serialize', 'resultAdd'); + $this->restResponse(HttpStatusCodesEnum::HTTP_CREATED); } /** * Process to leave a group displayed on the "Member Of" page * - * @return CakeResponse Redirect back to "Member Of" page */ - public function leaveGroup() + public function leaveGroup(): void { - $name = urldecode($this->request->query['GroupName']); - $display = urldecode($this->request->query['GroupDisplayName']); - - if ($this->request->is('ajax')) { - $this->response->disableCache(); - } + $this->layout = null; + $this->autoRender = false; + $groupName = urldecode($this->request->query['GroupName']); try { - if ($this->GrouperGroup->leaveGroup($this->userId, $name)) { - $resultRemove = "Success"; - } else { - $this->response->type('json'); - $this->response->statusCode(401); - $this->response->body(json_encode(array('status' => 'ERROR', 'message' => 'NOT DELETED'))); - $this->response->send(); - $resultRemove = ''; + if(!$this->GrouperGroup->removeGroupMember($this->getActAsIdentifier(), + $groupName, + $this->getUserId(), + $this->CoGrouperLiteWidget->getConfig())) { + // The Request returned unsuccessful, but we have not more infomration. In this case we will just return + // forbidden since we do not actually now what happened + $this->restResponse(HttpStatusCodesEnum::HTTP_FORBIDDEN); } } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); - $this->response->type('json'); - $this->response->statusCode(404); - $this->response->body(json_encode(array('status' => 'ERROR', 'message' => 'EXCEPTION'))); - $this->response->send(); - $resultRemove = ''; + throw $e; } - $this->set(compact('resultRemove')); - $this->set('_serialize', 'resultRemove'); + $this->restResponse(HttpStatusCodesEnum::HTTP_OK); } /** - * NOTE: All permissions will be done on the Grouper side. All Authenticated users will be able to - * use this plugin for self-admin of groups. - * - * Authorization for this Controller, called by Auth component - * - precondition: Session.Auth holds data used for authz decisions - * - postcondition: $permissions set with calculated permissions + * For Models that accept a CO ID, find the provided CO ID. + * - precondition: A coid must be provided in $this->request (params or data) * - * @return Array Permissions - * @since COmanage Registry v3.2.0 + * @since COmanage Registry v4.3.0 + * @return Integer The CO ID if found, or -1 if not */ - function isAuthorized() - { - $roles = $this->Role->calculateCMRoles(); - /** - * The following code displays a few custom implementations of the - * login process used to crosswalk a user for Grouper authentication. - * - * You may need to further customize this section to meet your organization - * crosswalk needs. - */ - - /** - * Default when login-id is the same as grouper id - */ - // Default Begin =============================================== - - /* - if ($this->Session->check('Auth.User.username')) { - $this->userId = $this->Session->read('Auth.User.username'); + public function parseCOID($data = null): int { + if(!empty($this->request->params['named']['co'])) { + return (int)$this->request->params['named']['co']; } - */ - // Default End =============================================== - - /** - * Customized Crosswalk from login-id to Grouper Username - */ - // Custom Begin =============================================== - - $username = $this->Session->read('Auth.User.username'); + if(!empty($this->request->params['named']['glid'])) { + $connectionInfo = $this->CoGrouperLiteWidget->findById($this->passedArgs['glid']); + $this->CoGrouperLiteWidget->CoDashboardWidget->CoDashboard->id = $connectionInfo['CoDashboardWidget']['co_dashboard_id']; + $co_id = $this->CoGrouperLiteWidget->CoDashboardWidget->CoDashboard->field('co_id'); - if ($this->Session->check('Plugin.Grouper.UserId')) { - $this->userId = $this->Session->read('Plugin.Grouper.UserId'); - } else { - $uid = $this->getPersonIdFromUsername($username); - $this->userId = $this->getUserId($uid); - $this->Session->write('Plugin.Grouper.UserId', $this->userId); + if(!empty($co_id)) { + return (int)$co_id; + } } - // Custom End =============================================== + return -1; + } + /** + * Remove a member from a group + * + * @throws JsonException + */ + public function removeSubscriber(): void + { + $this->layout = null; + $this->autoRender = false; - // Determine what operations this user can perform - // Construct the permission set for this user, which will also be passed to the view. - //Note: Leaving in current format, in case need to restrict certain pages, can just remove true and add params. - $p = array(); - - $p['index'] = true; - $p['groupowner'] = true; - $p['groupownerapi'] = true; - $p['groupoptin'] = true; - $p['groupoptinapi'] = true; - $p['groupmember'] = true; - $p['groupmemberapi'] = true; - $p['getBaseConfig'] = true; - $p['groupSubscribers'] = true; - $p['addSubscriber'] = true; - $p['findSubscriber'] = true; - $p['removeSubscriber'] = true; - - $p['groupCreate'] = true; - $p['joinGroup'] = true; - $p['leaveGroup'] = true; - $p['groupcreatetemplate'] = true; + $groupName = urldecode($this->request->query['group']); + $remUserId = urldecode($this->request->query['userId']); - $this->set('permissions', $p); + //Need to see if coming from AdHoc or from a WG (Working Group) + $groupNameFormatted = (strpos($groupName, ':') === false) ? 'ref:incommon-collab:' . $groupName . ':users' + : $groupName; - return ($p[$this->action]); + try { + if(!$this->GrouperGroup->removeGroupMember($this->getActAsIdentifier(), + $groupNameFormatted, + $remUserId, + $this->CoGrouperLiteWidget->getConfig())) { + // The Request returned unsuccessful, but we have not more infomration. In this case we will just return + // forbidden since we do not actually now what happened + $this->restResponse(HttpStatusCodesEnum::HTTP_FORBIDDEN); + } + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . ': ' . var_export($e->getMessage(), true)); + throw $e; + } + + $this->restResponse(HttpStatusCodesEnum::HTTP_OK); } - private function getPersonIdFromUsername($username) - { - $args = array(); - $args['conditions']['Identifier.identifier'] = $username; - $args['conditions']['Identifier.status'] = SuspendableStatusEnum::Active; - $args['conditions']['Identifier.deleted'] = false; - $args['conditions']['Identifier.identifier_id'] = null; - $args['conditions']['NOT']['Identifier.co_person_id'] = null; - $args['conditions']['Identifier.type'] = 'eppn'; - $args['contain'] = false; + /** + * + * @param integer $status HTTP result code + * @param array $body HTTP result comment + * @param string $contentType HTTP Response content Type + * @param $statusTxt $contentType Status text + * + * @throws JsonException + * @since COmanage Registry v4.4.4 + */ - $Identifier = new Identifier(); - $co_person_id = $Identifier->find('first', $args); + public function restResponse(int $status, + array $body=[], + string $contentType = 'json', + ?string $statusTxt = null): void + { + if(isset($statusTxt)) { + // We need to update the text associated with $status + $this->response->httpCodes(array($status => $statusTxt)); + } - return $co_person_id['Identifier']['co_person_id']; + $this->response->type($contentType); + $this->response->statusCode($status); + $this->response->body(json_encode($body, JSON_THROW_ON_ERROR)); + $this->response->send(); } - private function getUserId($id) + /** + * @param null|string $actAsIdentifier + */ + public function setActAsIdentifier(?string $actAsIdentifier): void { - $args = array(); - $args['conditions']['Identifier.co_person_id'] = $id; - $args['conditions']['Identifier.type'] = 'I2CollabPN'; - $args['conditions']['Identifier.status'] = SuspendableStatusEnum::Active; - $args['contain'] = false; - - $Identifier = new Identifier(); - $grouper_identifier = $Identifier->find('first', $args); + $this->actAsIdentifier = $actAsIdentifier; + } - return $grouper_identifier['Identifier']['identifier']; + /** + * @param null|string $userId + */ + private function setUserId(?string $userId): void + { + $this->userId = $userId; } /** - * Breakout Working Groups from AdHoc Groups for display purposes in UI. - * - * @param array $recordSet - * @param string $type 'basic' = view in Member page, 'admin' = view in Admin page, 'optin' = View in Optin page - * @return array[] + * UserManager vue route for rendering * + * @return void */ - private function breakoutGroups(array $recordSet, $type = 'basic') + public function userManager(): void { - //TODO - May move this logic down into the GrouperGroup model file, once all is agreed upon. - $wgData = array(); - $notWGData = array(); - //Parse out the Working Groups from the Ad-hoc groups - foreach ($recordSet as $record) { - if (isset($record['WGName'])) { - $wgData[] = $record; - } else { - $notWGData[] = $record; - } + $this->render('index'); + } + + /** + * Display all Groups for the user i manage + */ + public function userManagerApi(): void + { + //Set initial setting + $cfg = $this->CoGrouperLiteWidget->getConfig(); + + if (!isset($this->request->query['memberid'])) { + $this->restResponse(HttpStatusCodesEnum::HTTP_BAD_REQUEST, ErrorsEnum::Exception); } - return array( - 'adhoc' => $notWGData, - 'working' => $wgData - ); + $memberId = urldecode($this->request->query['memberid']); + + try { + $groupsimanage = $this->GrouperGroup->getManagedUsers($this->getActAsIdentifier(), + $memberId, + $cfg); + } catch (Exception $e) { + CakeLog::write('error', __METHOD__ . '::get Managed Users: ' . var_export($e->getMessage(), true)); + $this->restResponse(HttpStatusCodesEnum::HTTP_INTERNAL_SERVER_ERROR, ErrorsEnum::Exception); + + $this->set('groupsimanage', []); + return; + } + + $this->set(compact('groupsimanage')); + $this->set('_serialize', 'groupsimanage'); + } + /** + * Override the default check performed in AppController + * + * @since COmanage Registry v4.3.0 + * @return Boolean True if check is successful + */ + public function verifyRequestedId(): bool + { + return true; + } } \ No newline at end of file diff --git a/Controller/GrouperLiteAppController.php b/Controller/GrouperLiteAppController.php deleted file mode 100644 index 15ac4ac..0000000 --- a/Controller/GrouperLiteAppController.php +++ /dev/null @@ -1,7 +0,0 @@ -CoServicesWidget->getConfig(); - - // For now simply provide the services portal URL for the current CO. All necessary - // logic is already attended to there. See: Views/CoServices/portal.ctp - $services_url = Router::url([ - 'plugin' => null, - 'controller' => 'co_services', - 'action' => 'portal', - 'co' => $this->cur_co['Co']['id'] - ]); - $this->set('vv_services_url', $services_url); - - // Pass the config so we know which div to overwrite - $this->set('vv_config', $cfg); - - } - - /** - * Authorization for this Controller, called by Auth component - * - precondition: Session.Auth holds data used for authz decisions - * - postcondition: $permissions set with calculated permissions - * - * @since COmanage Registry v3.2.0 - * @return Array Permissions - */ - - function isAuthorized() { - //TODO - Need to work on this - - $roles = $this->Role->calculateCMRoles(); - - // Determine what operations this user can perform - - // Construct the permission set for this user, which will also be passed to the view. - // Ask the parent to calculate the display permission, based on the configuration. - // Note that the display permission is set at the Dashboard, not Dashboard Widget level. - $p = $this->calculateParentPermissions($roles); - - // Delete an existing CO Services Widget? - $p['delete'] = ($roles['cmadmin'] || $roles['coadmin']); - - // Edit an existing CO Services Widget? - $p['edit'] = ($roles['cmadmin'] || $roles['coadmin']); - - // View an existing CO Services Widget? - $p['view'] = ($roles['cmadmin'] || $roles['coadmin']); - - $this->set('permissions', $p); - return($p[$this->action]); - } - -} diff --git a/GrouperWidget/Controller/Component/empty b/GrouperWidget/Controller/Component/empty deleted file mode 100644 index e69de29..0000000 diff --git a/GrouperWidget/Controller/GrouperWidgetAppController.php b/GrouperWidget/Controller/GrouperWidgetAppController.php deleted file mode 100644 index 8255498..0000000 --- a/GrouperWidget/Controller/GrouperWidgetAppController.php +++ /dev/null @@ -1,7 +0,0 @@ -http = new GrouperHTTPWrapper(); - if (!CakeSession::check('Plugin.Grouper.Api.id')) { - CakeLog::write('error', __METHOD__ . ': No Widget record in Session'); - throw new GrouperLiteException("No GrouperLite instance captured"); + if (empty($cfg['CoGrouperLiteWidget']['id']) + || empty($cfg['CoGrouperLiteWidget']['conn_url']) + || empty($cfg['CoGrouperLiteWidget']['conn_user']) + || empty($cfg['CoGrouperLiteWidget']['conn_pass']) + || empty($cfg['CoGrouperLiteWidget']['conn_ver']) + ) { + throw new \http\Exception\RuntimeException('No GrouperLite instance captured'); } - $connUrl = CakeSession::read('Plugin.Grouper.Api.url'); - $connVer = CakeSession::read('Plugin.Grouper.Api.version'); - - $this->config['fullUrl'] = $connUrl . $this->_urlServlet . $connVer; - - $this->http->setUser(CakeSession::read('Plugin.Grouper.Api.user')); - $this->http->setPassword(CakeSession::read('Plugin.Grouper.Api.pass')); + $this->http->setServiceUrl($cfg['CoGrouperLiteWidget']['conn_url'], $cfg['CoGrouperLiteWidget']['conn_ver']); + $this->http->setUser($cfg['CoGrouperLiteWidget']['conn_user']); + $this->http->setPassword($cfg['CoGrouperLiteWidget']['conn_pass']); + // Assume json content responses + $this->http->setHeader(['Content-Type' => 'application/json', 'Accept' => 'application/json']); } /** - * Get Groups that User is a member of from Grouper. + * Add a member to a specific Grouper Group * - * Note: Params added at end make sure that the groups returned can only be viewed by the member logged into - * Grouper Lite + * @param string $actAsUserId + * @param string $groupName + * @param string $addUserId * - * @param array $queryData Array of conditions for querying - * @return array Membership records that User is a member of in Grouper - * @throws GrouperLiteException + * @example https://github.com/Internet2/grouper/blob/b1c7b14e30b3f73f58768fc75cf845ee9f1594ef/grouper-ws/grouper-ws/doc/samples/addMember/WsSampleAddMemberRest2_xml.txt + * @return bool Requests success or not + * @throws GrouperLiteWidgetException|JsonException */ - public function getGrouperMemberOfGroups(array $queryData) + public function addGroupMember(string $actAsUserId, + string $groupName, + string $addUserId): bool { + if(empty($actAsUserId) + || empty($groupName) + || empty($addUserId) + ) { + return false; + } + + // We have to urlencode the Group Names passed in the url + $groupName = $this->urlGrouperEncode($groupName); + $actionEndpoint = "/groups/$groupName/members"; //Build request logic - $userId = $queryData['userId']; - $connectionUrl = "{$this->config['fullUrl']}/subjects/$userId/groups?"; - $connectionUrl .= "wsLiteObjectType=WsRestGetGroupsLiteRequest&actAsSubjectId=$userId"; + $usersToAdd = [ + 'WsRestAddMemberRequest' => [ + 'subjectLookups' => [ + ['subjectId' => $addUserId], + ], + 'replaceAllExisting' => 'F', + 'actAsSubjectLookup' => [ + 'subjectId' => $actAsUserId + ] + ] + ]; try { - $results = $this->http->sendRequest('GET', $connectionUrl); - - // Parse out relevant records to send front end - if (isset($results['WsGetGroupsLiteResult']['wsGroups']) && $results['WsGetGroupsLiteResult']['wsGroups'] != NULL) { - return $results['WsGetGroupsLiteResult']['wsGroups']; - } + $results = $this->http->sendRequest('PUT', + $actionEndpoint, + json_encode($usersToAdd, JSON_THROW_ON_ERROR)); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } - return array(); + if(isset($results['error']) && $results['error']) { + $cakeExceptionClass = $results['cakeException']; + throw new $cakeExceptionClass($results['message']); + } + + return isset($results['WsAddMemberResults']['wsGroupAssigned']) + && $results['WsAddMemberResults']['wsGroupAssigned']['name'] === urldecode($groupName); } /** - * For Optin groups, calls Grouper WS to join or leave a group + * Method used to CREATE a new Group in Grouper via the Working Group Template method. + * + * @param array $queryData Array of conditions and data adding new Grouper Group + * @return array status and error message, if applicable + * @throws GrouperLiteWidgetException * - * @param array $queryData Array of conditions for querying - * @return bool True if join or leave successful, False if not - * @throws GrouperLiteException */ - public function grouperGroupLeaveOrJoin(array $queryData) + public function createGroupWithTemplate(array $queryData): array { + //Currently, only supporting create group, need to test if update a group will work! - $groupName = $queryData['groupName']; + $data = $queryData['data']; $userId = $queryData['userId']; - $groupLeaveOrJoin = $queryData['LeaveJoin']; - $groupName = urlencode($groupName); - - if ($groupLeaveOrJoin == "Leave") { - $memberRequest = "WsRestDeleteMemberRequest"; - $resultResponse = 'WsDeleteMemberResults'; - $resultGroup = 'wsGroup'; - } elseif ($groupLeaveOrJoin == "Join") { - $memberRequest = "WsRestAddMemberRequest"; - $resultResponse = 'WsAddMemberResults'; - $resultGroup = 'wsGroupAssigned'; - } else { - CakeLog::write('error', __METHOD__ . ": Option of $groupLeaveOrJoin is not supported"); - throw new GrouperLiteException("Received option of $groupLeaveOrJoin which is not supported"); + + //need to take out SympaDomain if domain not being created. + if ($data['gsh_input_isSympa'] == 'false') { + unset($data['gsh_input_sympaDomain'], $data['gsh_input_isSympaModerated']); } //Build request logic - $groupCommand = array( - $memberRequest => array( - "actAsSubjectLookup" => array( - "subjectId" => $userId - ), - "replaceAllExisting" => "F", - "subjectLookups" => array( - array("subjectId" => $userId) - ) - ) - ); - - $this->http->setHeader(array('Content-Type' => 'application/json', 'Accept' => 'application/json')); - $connectionUrl = "{$this->config['fullUrl']}/groups/$groupName/members"; + $inputFields = []; + foreach ($data as $key => $value) { + $inputFields[] = ['name' => $key, 'value' => $value]; + } - try { - $results = $this->http->sendRequest('PUT', $connectionUrl, json_encode($groupCommand)); + $groupToSave = [ + 'WsRestGshTemplateExecRequest' => [ + 'gshTemplateActAsSubjectLookup' => [ + 'subjectSourceId' => 'ldap', + 'subjectId' => $userId + ], + 'ownerStemLookup' => [ + 'stemName' => 'ref:incommon-collab' + ], + 'ownerType' => 'stem', + 'configId' => 'createNewWorkingGroup', + 'inputs' => $inputFields + ] + ]; + + $actionEndpoint = "/gshTemplateExec"; - if (isset($results[$resultResponse][$resultGroup]) && $results[$resultResponse][$resultGroup] != NULL) { - $groupAssigned = $results[$resultResponse][$resultGroup]['name']; - if ($groupAssigned == urldecode($groupName)) { - return true; - } - } + $status = true; + $message = ''; + try { + $results = $this->http->sendRequest('POST', + $actionEndpoint, + json_encode($groupToSave)); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } - return false; + if (isset($results['WsGshTemplateExecResult']['resultMetadata']['resultCode'])) { + $status = false; + $message = 'An error occurred, please try again later.'; + + CakeLog::write('error', __METHOD__ . ': An error occurred in creating your Working Group!'); + + if (isset($results['WsGshTemplateExecResult']['gshValidationLines'][0]['validationText']) + && stripos( + $results['WsGshTemplateExecResult']['gshValidationLines'][0]['validationText'], + 'already exist', + 0 ) !== false) + { + $message = 'There already is a WG named: ' . $data['gsh_input_workingGroupExtension']; + } elseif (stripos($results['WsGshTemplateExecResult']['resultMetadata']['resultCode'], 'EXCEPTION', 0) !== false) { + throw new GrouperLiteWidgetException('An error occurred in creating your Working Group!'); + } + } + + return compact('status', 'message'); } /** - * Gets all available Optin/OptOut groups in Grouper + * For creating/updating Ad-Hoc groups not using the Grouper Template format * - * Returns Optin/OptOut groups that can be joined/left + * Create or Update a Group where User is Admin/Owner * - * @param array $queryData Array of conditions for querying - * @return array Optin groups from Grouper - * @throws GrouperLiteException + * @param string $actAsUserId + * @param string $groupName + * @param string $stem + * @param string $groupDescription * + * @return bool True if added or updated successful + * @throws GrouperLiteWidgetException + * @throws JsonException + * + * @example https://github.com/Internet2/grouper/blob/master/grouper-ws/grouper-ws/doc/samples/groupSave/WsSampleGroupSaveRestLite_xml.txt */ - public function getOptionalGroups(array $queryData) + public function createUpdateGroup(string $actAsUserId, string $groupName, string $stem, string $groupDescription ): bool { + //Build request logic + $groupToSave = [ + 'WsRestGroupSaveRequest' => [ + 'actAsSubjectLookup' => [ + 'subjectId' => $actAsUserId + ], + 'wsGroupToSaves' => [ + [ + 'wsGroup' => [ + 'description' => $groupDescription, + 'name' => "{$stem}:{$groupName}", + 'displayExtension' => $groupName + ], + 'wsGroupLookup' => [ + 'groupName' => $groupDescription + ] + ] + ] + ] + ]; + $actionEndpoint = '/groups'; try { - $results = $this->useMembershipUrl($queryData); - - if (isset($results['WsGetMembershipsResults']['wsGroups']) && $results['WsGetMembershipsResults']['wsGroups'] != NULL) { - return $results['WsGetMembershipsResults']['wsGroups']; - } + $results = $this->http->sendRequest('POST', + $actionEndpoint, + json_encode($groupToSave, JSON_THROW_ON_ERROR)); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } - return array(); + + if(isset($results['error']) && $results['error']) { + $cakeExceptionClass = $results['cakeException']; + throw new $cakeExceptionClass($results['message']); + } + + // XXX This is not required, since by now i know that the request has succeeded, + // but i am leaving it here for the time being + return isset($results['WsGroupSaveResults']['resultMetadata']['resultCode']) + && in_array($results['WsGroupSaveResults']['resultMetadata']['resultCode'], + [GrouperResultCodesEnum::SUCCESS, GrouperResultCodesEnum::SUCCESS_UPDATED], + true); } /** - * Gets all groups in Grouper where user is an admin/owner or has update privs * - * @param array $queryData Array of conditions for querying - * @return array Array of groups from Grouper - * @throws GrouperLiteException + * Method used to DELETE a Group in Grouper via the Template method. * + * @param string $actAsUserId + * @param string $workingGroupExt + * + * @return bool True if deleted successfully + * @throws GrouperLiteWidgetException + * @throws JsonException */ - public function getOwnedGroups(array $queryData) + public function deleteGroupWithTemplate(string $actAsUserId, string $workingGroupExt): bool { + $groupToDelete = [ + 'WsRestGshTemplateExecRequest' => [ + 'gshTemplateActAsSubjectLookup' => [ + 'subjectSourceId' => 'ldap', + 'subjectId' => $actAsUserId + ], + 'ownerStemLookup' => [ + 'stemName' => 'ref:incommon-collab' + ], + 'ownerType' => 'stem', + 'configId' => 'createWorkingGroup', + 'inputs' => [ + [ + 'name' => 'gsh_input_workingGroupExtension', + 'value' => $workingGroupExt + ] + ] + ] + ]; + + $actionEndpoint = '/gshTemplateExec'; try { - $queryData['groupType'] = 'admin'; - $resultsAdmin = $this->useMembershipUrl($queryData); - - $queryData['groupType'] = 'update'; - $resultsUpdate = $this->useMembershipUrl($queryData); - - if (isset($resultsAdmin['WsGetMembershipsResults']['wsGroups']) && $resultsAdmin['WsGetMembershipsResults']['wsGroups'] != NULL) { - $admins = $resultsAdmin['WsGetMembershipsResults']['wsGroups']; - } else { - $admins = array(); - } - - if (isset($resultsUpdate['WsGetMembershipsResults']['wsGroups']) && $resultsUpdate['WsGetMembershipsResults']['wsGroups'] != NULL) { - $updaters = $resultsUpdate['WsGetMembershipsResults']['wsGroups']; - } else { - $updaters = array(); - } - - return $this->removeDuplicates($admins, $updaters); + $results = $this->http->sendRequest('POST', + $actionEndpoint, + json_encode($groupToDelete, JSON_THROW_ON_ERROR)); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } - } - - /** - * 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) - { - - //Determine which array is bigger and use as base - $countOne = count($arrOne); - $countTwo = count($arrTwo); - if ($countOne >= $countTwo) { - $arrL = $arrOne; - $arrS = $arrTwo; - } else { - $arrL = $arrTwo; - $arrS = $arrOne; - } - foreach ($arrL as $large) { - foreach ($arrS as $key => $val) { - if ($large['uuid'] == $val['uuid']) { - unset($arrS[$key]); - } - } + if (isset($results['WsGshTemplateExecResult']['resultMetadata']['resultCode']) && stripos( + $results['WsGshTemplateExecResult']['resultMetadata']['resultCode'], + 'SUCCESS', + 0 + ) !== false) { + return true; } - return array_merge_recursive($arrL, $arrS); + return false; } /** - * Get members associated to a specific Grouper Group + * Used to be used for Grouper group info page. * - * @param array $queryData Array of conditions for querying - * @return array Listing of Members belonging to Grouper Group - * @throws GrouperLiteException + * Grouper Group information plus a listing of attributes in Grouper for that given Group + * + * @param string $groupName + * + * @return array Record of Grouper attributes for given GroupName + * @throws GrouperLiteWidgetException + * @throws JsonException */ - public function getMembersInGroup(array $queryData) + public function getGroupInfo(string $groupName): array { - - try { - //Build request logic - $usersToShow = array( - "WsRestGetMembersRequest" => array( - "actAsSubjectLookup" => array( - "subjectId" => $queryData['userId'] - ), - "wsGroupLookups" => array( - array("groupName" => $queryData['groupName']) - ), - "subjectAttributeNames" => array( - "name" - ) - ) - ); - - $this->http->setHeader(array('Content-Type' => 'application/json', 'Accept' => 'application/json')); - $connectionUrl = "{$this->config['fullUrl']}/groups"; - - $results = $this->http->sendRequest('POST', $connectionUrl, json_encode($usersToShow)); - - // Parse out relevant records to send front end - if(isset($results['WsGetMembersResults']['results'][0]['resultMetadata']['resultCode']) && $results['WsGetMembersResults']['results'][0]['resultMetadata']['resultCode'] != NULL){ - if ($results['WsGetMembersResults']['results'][0]['resultMetadata']['resultCode'] == 'GROUP_NOT_FOUND'){ - return array(array("sourceId" => "NoAccess", "name" => "", "id" => "")); - } - } - if (isset($results['WsGetMembersResults']['results'][0]['wsSubjects']) && $results['WsGetMembersResults']['results'][0]['wsSubjects'] != NULL) { - return $results['WsGetMembersResults']['results'][0]['wsSubjects']; - } - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); - throw $e; + if(empty($groupName)) { + return []; } + $groupInfo = []; - return array(); - } - - /** - * Add a member to a specific Grouper Group - * - * @param array $queryData Array of conditions for querying - * @return string Requests success or not - * @throws GrouperLiteException - */ - public function addMemberToGroup(array $queryData) - { + //Build request logic + $stemToFind = [ + 'WsRestGetAttributeAssignmentsRequest' => [ + 'attributeAssignType' => 'group', + 'includeAssignmentsOnAssignments' => 'T', + 'wsOwnerGroupLookups' => [ + [ + 'groupName' => $groupName, + ] + ] + ] + ]; + $actionEndpoint = '/attributeAssignments'; try { - $groupName = $queryData['groupName']; - - //Build request logic - $usersToAdd = array( - "WsRestAddMemberRequest" => array( - "subjectLookups" => array( - array("subjectId" => $queryData['addUserId']), - ), - "replaceAllExisting" => "F", - "actAsSubjectLookup" => array( - "subjectId" => $queryData['userId'] - ) - ) - ); - - $this->http->setHeader(array('Content-Type' => 'application/json', 'Accept' => 'application/json')); - $connectionUrl = "{$this->config['fullUrl']}/groups/$groupName/members"; - - $results = $this->http->sendRequest('PUT', $connectionUrl, json_encode($usersToAdd)); - - // Parse out relevant records to send front end - if (isset($results['WsAddMemberResults']['results'][0]['resultMetadata']) && $results['WsAddMemberResults']['results'][0]['resultMetadata'] != NULL) { - return $results['WsAddMemberResults']['results'][0]['resultMetadata']['resultCode']; - } + $results = $this->http->sendRequest('POST', + $actionEndpoint, + json_encode($stemToFind, JSON_THROW_ON_ERROR)); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } - return ""; + //Get the group information + if (isset($results['WsGetAttributeAssignmentsResults']['wsGroups'])) { + $groupInfo = $results['WsGetAttributeAssignmentsResults']['wsGroups']; + } + + // Now get the Group Attributes and add them to group + $groupInfo[0]['attributes'] = $results['WsGetAttributeAssignmentsResults']['wsAttributeAssigns'] ?? []; + + return $groupInfo; } /** - * Remove a member from a specific Grouper Group + * Returns all the groups the active user is a member of, that they are allowed to see. + * + * THIS REQUEST TARGETS THE GROUPS ENDPOINT + * + * Note: Params added at end make sure that the groups returned can only be viewed by the member logged into + * Grouper Lite * - * @param array $queryData Array of conditions for querying - * @return string Requests success or not - * @throws GrouperLiteException + * @param string $actAsUserId + * @param string $userId + * + * @return array Membership records that User is a member of in Grouper + * + * @throws GrouperLiteWidgetException */ - public function removeMemberToGroup(array $queryData) + public function getUserGroups(string $actAsUserId, string $userId): array { + if(empty($userId) || empty($actAsUserId)) { + return []; + } + $actionEndpoint = "/subjects/{$userId}/groups?" + . 'wsLiteObjectType=WsRestGetGroupsLiteRequest' + . "&actAsSubjectId={$actAsUserId}"; try { - $groupName = $queryData['groupName']; - - //Build request logic - $userToDelete = array( - "WsRestDeleteMemberRequest" => array( - "subjectLookups" => array( - array("subjectId" => $queryData['remUserId']), - ), - "actAsSubjectLookup" => array( - "subjectId" => $queryData['userId'] - ) - ) - ); - - $this->http->setHeader(array('Content-Type' => 'application/json', 'Accept' => 'application/json')); - $connectionUrl = "{$this->config['fullUrl']}/groups/$groupName/members"; - $results = $this->http->sendRequest('POST', $connectionUrl, json_encode($userToDelete)); - - // Parse out relevant records to send front end - if (isset($results['WsDeleteMemberResults']['results'][0]['resultMetadata']) && $results['WsDeleteMemberResults']['results'][0]['resultMetadata'] != NULL) { - return $results['WsDeleteMemberResults']['results'][0]['resultMetadata']['resultCode']; - } + $results = $this->http->sendRequest('GET', $actionEndpoint); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } - return ""; + return $results['WsGetGroupsLiteResult']['wsGroups'] ?? []; } /** + * Returns either the groups the user is able to Opt into or can manage the memberships of. * Used for requests made to Membership endpoint in Grouper WS * - * @param array $queryData Array of conditions for querying + * THIS REQUEST TARGETS THE MEMBERSHIP ENDPOINT + * + * @param string $userId + * @param string $actAsUserId + * @param string $groupType + * * @return array Group records associated to calling method - * @throws GrouperLiteException + * @throws GrouperLiteWidgetException + * + * @example https://github.com/Internet2/grouper/blob/b1c7b14e30b3f73f58768fc75cf845ee9f1594ef/grouper-ws/grouper-ws/doc/samples/getMemberships/WsSampleGetMembershipsRest2_xml.txt#L20 + * + * $: > grouperClientAlias --debug --operation=getMembershipsWs --subjectIds=john.b.doe@at.internet2.edu,subjectAll --actAsSubjectId=john.b.doe@at.internet2.edu * - * @see getOwnedStems() - * @see getOptinGroups() - * @see getOptOutGroups() - * @see getOwnedGroups() + * POST /grouper-ws/servicesRest/5.8.1/memberships HTTP/1.1 + * Connection: close + * Authorization: Basic xxxxxxxxxxxxxxxx + * User-Agent: Jakarta Commons-HttpClient/3.1 + * Host: grouper.dev.at.internet2.edu:-1 + * Content-Length: 208 + * Content-Type: application/json; charset=UTF-8 + * + * { + * "WsRestGetMembershipsRequest":{ + * "actAsSubjectLookup":{ + * "subjectId":"john.b.doe@at.internet2.edu" + * }, + * "wsSubjectLookups":[ + * { + * "subjectId":"john.b.doe@at.internet2.edu" + * }, + * { + * "subjectId":"subjectAll" + * } + * ] + * } + * } */ - private function useMembershipUrl(array $queryData) + public function getUserMemberships(string $userId, string $actAsUserId, string $groupType): array { - $groupType = $queryData['groupType']; - $userId = $queryData['userId']; - - if ($groupType == 'optins' || $groupType == 'optouts') { - $subjectId = "GrouperAll"; - } elseif ($groupType == 'admin' || $groupType == 'update' || $groupType == 'stemAdmin') { - $subjectId = $userId; - } else { - CakeLog::write('error', __METHOD__ . ": Option of $groupType is not supported"); - throw new GrouperLiteException("Option of $groupType is not supported"); + if(empty($actAsUserId) + || empty($userId) + || empty($groupType) + ) { + return []; + } + + if(!in_array($groupType, [ + GrouperGroupTypeEnum::OPTINS, + GrouperGroupTypeEnum::OPTOUTS, + GrouperGroupTypeEnum::ADMIN, + GrouperGroupTypeEnum::UPDATE, + GrouperGroupTypeEnum::STEM_ADMIN + ], true) + ) { + CakeLog::write('error', __METHOD__ . ": Option of {$groupType} is not supported"); + throw new BadRequestException("Option of {$groupType} is not supported"); } - if ($groupType == 'optins' || $groupType == 'optouts') { - //Build request logic, 2 subjectId's, second is for when user in "Secret" Optin/Optout Group - $groupsToShow = array( - "WsRestGetMembershipsRequest" => array( - "fieldName" => $groupType, - "wsSubjectLookups" => array( - array("subjectId" => $subjectId), - array("subjectId" => $userId) - ) - ) - ); - } else { - //Build request logic - $groupsToShow = array( - "WsRestGetMembershipsRequest" => array( - "fieldName" => $groupType, - "wsSubjectLookups" => array( - array("subjectId" => $subjectId) - ) - ) - ); + $isOptinsOrOptouts = in_array($groupType, + [GrouperGroupTypeEnum::OPTINS, GrouperGroupTypeEnum::OPTOUTS], + true); + + // Build request logic + // XXX This is not working very well? + $data = [ + 'WsRestGetMembershipsRequest' => [ + 'fieldName' => $groupType, + 'actAsSubjectLookup' => [ + 'subjectId' => ($userId !== $actAsUserId) ? $actAsUserId : '' + ], + 'wsSubjectLookups' => [ + ['subjectId' => $isOptinsOrOptouts ? GrouperConfigEnums::ALL : $userId] + ], + ] + ]; + + if ($isOptinsOrOptouts) { + // Build request logic, 2 subjectId's, second is for when user in "Secret" Optin/Optout Group + $data['WsRestGetMembershipsRequest']['wsSubjectLookups'][1]['subjectId'] = $userId; } - $this->http->setHeader(array('Content-Type' => 'application/json', 'Accept' => 'application/json')); - $connectionUrl = "{$this->config['fullUrl']}/memberships"; + $actionEndpoint = '/memberships'; try { - return $this->http->sendRequest('POST', $connectionUrl, json_encode($groupsToShow)); + $results = $this->http->sendRequest('POST', + $actionEndpoint, + json_encode($data)); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } + + return $results['WsGetMembershipsResults']['wsGroups'] ?? []; } /** - * Method used to CREATE a new Group in Grouper via the Working Group Template method. + * Get members associated to a specific Grouper Group * - * @param array $queryData Array of conditions and data adding new Grouper Group - * @return array status and error message, if applicable - * @throws GrouperLiteException + * @param string $actAsUserId + * @param string $groupName * + * @return array Listing of Members belonging to Grouper Group + * @throws GrouperLiteWidgetException + * @throws JsonException + * @example https://github.com/Internet2/grouper/blob/master/grouper-ws/grouper-ws/doc/samples/getGroups/WsSampleGetGroupsRest_json.txt + * + * $: > grouperClientAlias --debug=true --operation=getMembersWs --actAsSubjectId=john.b.doe@at.internet2.edu --subjectAttributeNames=name --groupNames=ref:incommon-collab:co:member + * WebService: connecting to URL: 'https://grouper.dev.at.internet2.edu/grouper-ws/servicesRest/5.8.1/groups' + * + * ################ REQUEST START (indented) ############### + * + * POST /grouper-ws/servicesRest/5.8.1/groups HTTP/1.1 + * Connection: close + * Authorization: Basic xxxxxxxxxxxxxxxx + * User-Agent: Jakarta Commons-HttpClient/3.1 + * Host: grouper.dev.at.internet2.edu:-1 + * Content-Length: 201 + * Content-Type: application/json; charset=UTF-8 + * + * { + * "WsRestGetMembersRequest":{ + * "wsGroupLookups":[ + * { + * "groupName":"ref:incommon-collab:co:member" + * } + * ], + * actAsSubjectLookup":{ + * "subjectId":"john.b.doe@at.internet2.edu" + * }, + * "subjectAttributeNames":[ + * "name" + * ] + * } + * } */ - public function createGroupWithTemplate(array $queryData) + public function getGroupMembers(string $actAsUserId, string $groupName): array { - //Currently, only supporting create group, need to test if update a group will work! - - $data = $queryData['data']; - $userId = $queryData['userId']; - - //need to take out SympaDomain if domain not being created. - if ($data['gsh_input_isSympa'] == 'false') { - unset($data['gsh_input_sympaDomain']); - unset($data['gsh_input_isSympaModerated']); + if(empty($actAsUserId) + || empty($groupName) + ) { + return []; } //Build request logic - $inputFields = array(); - foreach ($data as $key => $value) { - $inputFields[] = array('name' => $key, 'value' => $value); - } - - $groupToSave = array( - "WsRestGshTemplateExecRequest" => array( - "gshTemplateActAsSubjectLookup" => array( - "subjectSourceId" => "ldap", - "subjectId" => $userId - ), - "ownerStemLookup" => array( - "stemName" => "ref:incommon-collab" - ), - "ownerType" => "stem", - "configId" => "createNewWorkingGroup", - "inputs" => $inputFields - ) - ); - - $this->http->setHeader(array('Content-Type' => 'application/json', 'Accept' => 'application/json')); - $connectionUrl = "{$this->config['fullUrl']}/gshTemplateExec"; + $usersToShow = [ + 'WsRestGetMembersRequest' => [ + 'actAsSubjectLookup' => [ + 'subjectId' => $actAsUserId + ], + 'wsGroupLookups' => [ + ['groupName' => $groupName] + ], + 'subjectAttributeNames' => ['name'] + ] + ]; + + $actionEndpoint = '/groups'; - $status = true; - $message = ''; try { - $results = $this->http->sendRequest('POST', $connectionUrl, json_encode($groupToSave)); - - if (isset($results['WsGshTemplateExecResult']['resultMetadata']['resultCode'])) { - if (stripos($results['WsGshTemplateExecResult']['resultMetadata']['resultCode'], "INVALID", 0) !== false) { - // Need to see what error message is - if (isset($results['WsGshTemplateExecResult']['gshValidationLines'])) { - //Just grab first one, since do not want to overload the user with errors, plus they won't understand message - $errorMessage = $results['WsGshTemplateExecResult']['gshValidationLines'][0]['validationText']; - $status = false; - if (stripos($errorMessage, 'already exist', 0) !== false) { - $message = 'There already is a WG named: ' . $data['gsh_input_workingGroupExtension']; - } else { - $message = 'An error occurred, please try again later.'; - } - } else { - $status = false; - $message = 'An error occurred, please try again later.'; - } - } elseif (stripos($results['WsGshTemplateExecResult']['resultMetadata']['resultCode'], "EXCEPTION", 0) !== false) { - throw new GrouperLiteException("An error occurred in creating your Working Group!"); - } - } + $results = $this->http->sendRequest('POST', + $actionEndpoint, + json_encode($usersToShow, JSON_THROW_ON_ERROR)); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } - return array( - 'status' => $status, - 'message' => $message - ); + if(isset($results['error']) && $results['error']) { + $cakeExceptionClass = $results['cakeException']; + throw new $cakeExceptionClass($results['message']); + } + return $results['WsGetMembersResults']['results'][0]['wsSubjects'] ?? []; } /** - * ======================== NOT BEING USED ======================== + * Is the user member of the Group * - * Method used to DELETE a Group in Grouper via the Template method. + * @param string $groupName + * @param string $userId * - * @param array $queryData Array of conditions and data adding new Grouper Group - * @return bool True if deleted successfully - * @throws GrouperLiteException + * @return bool * + * @throws GrouperLiteWidgetException */ - public function deleteGroupWithTemplate(array $queryData) + public function isMemberOfGroup(string $groupName, string $userId): bool { + if(empty($userId) || empty($groupName)) { + return []; + } - $workingGroupExt = $queryData['workingGroupExt']; - $userId = $queryData['userId']; - - $groupToDelete = array( - "WsRestGshTemplateExecRequest" => array( - "gshTemplateActAsSubjectLookup" => array( - "subjectSourceId" => "ldap", - "subjectId" => $userId - ), - "ownerStemLookup" => array( - "stemName" => "ref:incommon-collab" - ), - "ownerType" => "stem", - "configId" => "createWorkingGroup", - "inputs" => array( - array( - "name" => "gsh_input_workingGroupExtension", - "value" => $workingGroupExt - ) - ) - ) - ); - - $this->http->setHeader(array('Content-Type' => 'application/json', 'Accept' => 'application/json')); - $connectionUrl = "{$this->config['fullUrl']}/gshTemplateExec"; + $groupNameEncoded = $this->urlGrouperEncode($groupName); + $actionEndpoint = '/groups' + . "/{$groupNameEncoded}/members/{$userId}"; try { - $results = $this->http->sendRequest('POST', $connectionUrl, json_encode($groupToDelete)); - - if (isset($results['WsGshTemplateExecResult']['resultMetadata']['resultCode'])) { - if (stripos($results['WsGshTemplateExecResult']['resultMetadata']['resultCode'], "SUCCESS", 0) !== false) { - return true; - } - } + $results = $this->http->sendRequest('GET', $actionEndpoint); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } - return false; + + return isset($results['WsHasMemberLiteResult']['resultMetadata']['resultCode']) + && $results['WsHasMemberLiteResult']['resultMetadata']['resultCode'] === GrouperResultCodesEnum::IS_MEMBER; } /** - * ======================== NOT BEING USED ======================== + * Remove a member from a specific Grouper Group * - * For creating/updating Ad-Hoc groups not using the Grouper Template format + * @param string $actAsUserId The actor of the remove action + * @param string $groupName Group to remove the member from + * @param string $removeUserId User to be removed from the Group * - * Create or Update a Group where User is Admin/Owner + * @return bool True on success, false otherwise * - * @param array $queryData Array of conditions and data adding new Grouper Group - * @return bool True if added or updated successful - * @throws GrouperLiteException + * @throws GrouperLiteWidgetException + * @throws JsonException */ - public function createUpdateGroup(array $queryData) + public function removeGroupMember(string $actAsUserId, string $groupName, string $removeUserId): bool { - - $groupName = htmlentities($queryData['name']); - $stemName = htmlentities($queryData['stem']); - $userId = $queryData['userId']; - $groupDescription = htmlentities($queryData['description']); - $privileges = $queryData['privileges']; - - //Group name may be in "friendly" format, so need to do some small conversions before saving. - $newGroupName = ucfirst(strtolower($groupName)); - $newGroupName = str_replace(' ', '', $newGroupName); - - $newGroupToSave = "$stemName:$newGroupName"; + if(empty($actAsUserId) + || empty($groupName) + || empty($removeUserId) + ) { + return false; + } + // We have to urlencode the Group Names passed in the url + $groupName = $this->urlGrouperEncode($groupName); + $actionEndpoint = "/groups/$groupName/members"; //Build request logic - $groupToSave = array( - "WsRestGroupSaveRequest" => array( - "actAsSubjectLookup" => array( - "subjectId" => $userId - ), - "wsGroupToSaves" => array( - array( - "wsGroup" => array( - "description" => $groupDescription, - "name" => $newGroupToSave, - "displayExtension" => $groupName - ), - "wsGroupLookup" => array( - "groupName" => $groupDescription - ) - ) - ) - ) - ); - $this->http->setHeader(array('Content-Type' => 'application/json', 'Accept' => 'application/json')); - $connectionUrl = "{$this->config['fullUrl']}/groups"; + $userToDelete = [ + 'WsRestDeleteMemberRequest' => [ + 'subjectLookups' => [ + ['subjectId' => $removeUserId], + ], + 'actAsSubjectLookup' => [ + 'subjectId' => $actAsUserId + ] + ] + ]; - try { - $results = $this->http->sendRequest('POST', $connectionUrl, json_encode($groupToSave)); - if (isset($results['WsGroupSaveResults']['results']['resultMetadata']['resultCode'])) { - if (stripos($results['WsGroupSaveResults']['results']['resultMetadata']['resultCode'], "Success", 0) !== false) { - return true; - } - } + try { + $results = $this->http->sendRequest('POST', + $actionEndpoint, + json_encode($userToDelete, JSON_THROW_ON_ERROR)); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } - return false; - } - - /** - * ======================== NOT BEING USED ======================== - * Used to be used for Grouper group info page. - * - * Grouper Group information plus a listing of attributes in Grouper for that given Group - * - * @param array $queryData Array of conditions for querying - * @return array Record of Grouper attributes for given GroupName - * @throws GrouperLiteException - */ - public function getGrouperGroupInfo(array $queryData) - { - $groupName = $queryData['groupName']; - $groupInfo = array(); - - //Build request logic - $stemToFind = array( - "WsRestGetAttributeAssignmentsRequest" => array( - "attributeAssignType" => "group", - "includeAssignmentsOnAssignments" => "T", - "wsOwnerGroupLookups" => array( - array( - "groupName" => $groupName, - ) - ) - ) - ); - $this->http->setHeader(array('Content-Type' => 'application/json', 'Accept' => 'application/json')); - $connectionUrl = "{$this->config['fullUrl']}/attributeAssignments"; - - try { - $results = $this->http->sendRequest('POST', $connectionUrl, json_encode($stemToFind)); - - //Get the group information - if (isset($results['WsGetAttributeAssignmentsResults']['wsGroups']) && $results['WsGetAttributeAssignmentsResults']['wsGroups'] != NULL) { - $groupInfo = $results['WsGetAttributeAssignmentsResults']['wsGroups']; - } - - //Now get the Group Attributes and add them to group - if (isset($results['WsGetAttributeAssignmentsResults']['wsAttributeAssigns']) && $results['WsGetAttributeAssignmentsResults']['wsAttributeAssigns'] != NULL) { - $groupInfo[0]["attributes"] = $results['WsGetAttributeAssignmentsResults']['wsAttributeAssigns']; - } else { - $groupInfo[0]["attributes"] = array(); - } - return $groupInfo; - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); - throw $e; + if(isset($results['error']) && $results['error']) { + $cakeExceptionClass = $results['cakeException']; + throw new $cakeExceptionClass($results['message']); } + + return isset($results['WsDeleteMemberResults']['resultMetadata']['resultCode']) + && $results['WsDeleteMemberResults']['resultMetadata']['resultCode'] === GrouperResultCodesEnum::SUCCESS; } /** - * ======================== NOT BEING USED ======================== - * - * Potential use was for creating adhoc group by a user, not associated to WG. + * Properly encode the attribute for the URL * - * Gets all Stems/Folders where User is admin/owner + * @param string $url * - * @param array $queryData Array of conditions for querying - * @return array Array of Stems/Folders from Grouper - * @throws GrouperLiteException + * @return string */ - public function getOwnedStems(array $queryData) + public function urlGrouperEncode(string $url): string { - $queryData['groupType'] = 'stemAdmin'; - - try { - $results = $this->useMembershipUrl($queryData); - - if (isset($results['WsGetMembershipsResults']['wsStems']) && $results['WsGetMembershipsResults']['wsStems'] != NULL) { - return $results['WsGetMembershipsResults']['wsStems']; - } - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); - throw $e; - } - return array(); + $url = urlencode($url); + return str_replace(':', '%3A', $url); } - } diff --git a/Lib/GrouperHTTPWrapper.php b/Lib/GrouperHTTPWrapper.php index dbcd822..115d8cb 100644 --- a/Lib/GrouperHTTPWrapper.php +++ b/Lib/GrouperHTTPWrapper.php @@ -26,7 +26,7 @@ */ App::uses('HttpSocket', 'Network/Http'); -App::uses('GrouperLiteException', 'GrouperLite.Lib/'); +App::uses('GrouperLiteWidgetException', 'GrouperLiteWidget.Lib/'); /** * Class GrouperHTTPWrapper @@ -35,14 +35,42 @@ */ class GrouperHTTPWrapper extends HttpSocket { - //Connection array sent to HttpSocket - private $_request = array(); + /** + * @var array + * + * Connection array sent to HttpSocket + */ + private array $_request = []; + + /** + * @var string + * + * Grouper User Account to access Grouper Web Services + */ + private string $_user; + + /** + * @var string + * + * Password of Grouper User Account + */ + private string $_password; + + + /** + * @var string + * + * Grouper Servlet + */ + private $_urlServlet = '/grouper-ws/servicesRest/'; - // Grouper User Account to access Grouper Web Services - private $_user; + /** + * @var string + * + * Grouper Service Url + */ + private string $_serviceUrl; - // Password of Grouper User Account - private $_password; /** * GrouperHTTPWrapper constructor. @@ -57,23 +85,38 @@ public function __construct($config = array()) { $this->config['ssl_verify_peer'] = false; } + /** - * Setter for SSL Verify Peer + * Enable SSL Verify configuration * - * @param boolean $val + * @return void */ - public function setSSLVerify(bool $val) { - $this->config['ssl_verify_peer'] = $val; + public function enableSSLVerify(): void + { + $this->config['ssl_verify_peer'] = true; } + /** - * Setter - User account for accessing Grouper Web Services + * Disable SSL Verify Configuration * - * @param string $user + * @return void */ - public function setUser(string $user) { + public function disableSSLVerify(): void + { + $this->config['ssl_verify_peer'] = false; + } - $this->_user = $user; + /** + * Setter - Header settings needed to call Grouper Web Services + * + * @param array $headerSetting + */ + public function setHeader(array $headerSetting): void + { + if (!empty($headerSetting)) { + $this->_request['header'] = $headerSetting; + } } /** @@ -81,18 +124,44 @@ public function setUser(string $user) { * * @param string $password */ - public function setPassword(string $password) { + public function setPassword(string $password): void + { + if (!empty($password)) { + $this->_password = $password; + } + } - $this->_password = $password; + /** + * @param string $domain + * @param string $version + * + * @return void + */ + public function setServiceUrl(string $domain, string $version): void + { + if(!empty($domain) && !empty($version)) { + $this->_serviceUrl = $domain . $this->_urlServlet . $version; + } } /** - * Setter - Header settings needed to call Grouper Web Services + * @return string + */ + public function getServiceUrl(): string + { + return $this->_serviceUrl; + } + + /** + * Setter - User account for accessing Grouper Web Services * - * @param array $headerSetting + * @param string $user */ - public function setHeader(array $headerSetting) { - $this->_request['header'] = $headerSetting; + public function setUser(string $user): void + { + if (!empty($user)) { + $this->_user = $user; + } } /** @@ -102,28 +171,66 @@ public function setHeader(array $headerSetting) { * @param string $uri Grouper RESTful endpoint with request string, if applicable * @param string $body JSON formatted request, if applicable * @return array array of records | array with error message - * @throws GrouperLiteException If issue with Grouper WS connection + * @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 $uri, string $body = ''): array { - + public function sendRequest(string $method, string $endPoint, string $body = ''): array { + $uri = "{$this->_serviceUrl}{$endPoint}"; $this->_request['method'] = $method; $this->_request['uri'] = $this->_parseUri($uri); $this->_request['uri']['user'] = $this->_user; $this->_request['uri']['pass'] = $this->_password; $this->_request['body'] = $body; + CakeLog::write('debug', __METHOD__ . '::connection url: ' . $uri); + CakeLog::write('debug', __METHOD__ . '::data: ' . $body); try { $apiResults = $this->request($this->_request); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred: ' . var_export($e->getMessage(), true)); - throw new GrouperLiteException('An error occurred talking to Grouper WS'); + throw new RuntimeException(_txt('er.notfound-b', array( + _txt('pl.grouperlite.crumb.root')) + )); } - // Call may return non-200, which may be okay depending on call - if (!$apiResults->isOk()) { - CakeLog::write('error', __METHOD__ . ': Grouper WS returned non-200 of ' . var_export($apiResults->body(), true)); + $successHeader = $apiResults->getHeader('X-Grouper-success'); + if (empty($successHeader)) { + throw new RuntimeException('Web service did not even respond!'); } - return json_decode($apiResults->body(), true); + $payload = json_decode($apiResults->body(), true); + + $code = $apiResults->code; + $reasonPhrase = $apiResults->reasonPhrase; + + // The request returned with Failed status + if($successHeader == 'F' && !empty($payload)) { + CakeLog::write('debug', __METHOD__ . '::body ' . var_export($apiResults->body(), true)); + CakeLog::write('debug', __METHOD__ . '::headers: ' . var_export($apiResults->headers, true)); + + // I need to pop the element since the key is request specific. + // For example, it is WsAddMemberResults for an Add request + $error_payload = array_pop($payload); + $constant_error = 'GrouperCodesToExceptionClassEnum::' . $error_payload['resultMetadata']['resultCode']; + $cakeException = defined($constant_error) ? constant($constant_error) : 'BadRequestException'; + if($code == HttpStatusCodesEnum::HTTP_UNAUTHORIZED) { + $cakeException = 'UnauthorizedException'; + } + + return [ + 'error' => true, + 'grouperCode' => $error_payload['resultMetadata']['resultCode'], + 'message' => $error_payload['resultMetadata']['resultMessage'], + 'reasonPhrase' => $reasonPhrase, + 'cakeException' => $cakeException + ]; + } + +// CakeLog::write('debug', __METHOD__ . '::payload: ' . var_export($payload, true)); + + return $payload; } } diff --git a/Lib/GrouperLiteException.php b/Lib/GrouperLiteWidgetException.php similarity index 94% rename from Lib/GrouperLiteException.php rename to Lib/GrouperLiteWidgetException.php index 6e4621b..7ac0890 100644 --- a/Lib/GrouperLiteException.php +++ b/Lib/GrouperLiteWidgetException.php @@ -25,10 +25,10 @@ * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ -class GrouperLiteException extends Exception +class GrouperLiteWidgetException extends Exception { /** - * GrouperLiteException constructor. + * GrouperLiteWidgetException constructor. * @param $message * @param int $code * @param Exception|null $previous diff --git a/Lib/empty b/Lib/empty deleted file mode 100644 index e69de29..0000000 diff --git a/Lib/enum.php b/Lib/enum.php new file mode 100644 index 0000000..76a5413 --- /dev/null +++ b/Lib/enum.php @@ -0,0 +1,66 @@ + 'ERROR', 'message' => 'Already Exists']; + const Error = ['status' => 'ERROR', 'message' => 'Error']; + const Exception = ['status' => 'ERROR', 'message' => 'Exception']; + const NoAccess = ['status' => 'ERROR', 'message' => 'No Access']; + const NotAdded = ['status' => 'ERROR', 'message' => 'Not Added']; + const NotDeleted = ['status' => 'ERROR', 'message' => 'Not Deleted']; +} + +class GrouperSpecialGroups { + /** Group whose members can create Groups via Template process */ + const TEMPLATE_CREATION_GROUP = 'ref:workinggroupadmins'; + + /** 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'; +} + +// https://software.internet2.edu/grouper/doc/master/grouper-ws-parent/grouper-ws/apidocs/edu/internet2/middleware/grouper/ws/rest/WsRestResultProblem.html +// https://software.internet2.edu/grouper/doc/master/grouper-ws-parent/grouper-ws/apidocs/edu/internet2/middleware/grouper/ws/coresoap/WsAddMemberResult.WsAddMemberResultCode.html# +class GrouperResultCodesEnum { + const SUCCESS_ALREADY_EXISTED = 'SUCCESS_ALREADY_EXISTED'; + const EXECUTE_FAILED = 'EXECUTE_FAILED'; + const GROUP_NOT_FOUND = 'GROUP_NOT_FOUND'; + const IS_MEMBER = 'IS_MEMBER'; + const IS_NOT_MEMBER = 'IS_NOT_MEMBER'; + const INSUFFICIENT_PRIVILEGES = 'INSUFFICIENT_PRIVILEGES'; + const INVALID_QUERY = 'INVALID_QUERY'; + const NO_SUCH_OBJECT = 'NO_SUCH_OBJECT'; + const PROBLEM_WITH_ASSIGNMENT = 'PROBLEM_WITH_ASSIGNMENT'; + const PROBLEM_GETTING_MEMBERS = 'PROBLEM_GETTING_MEMBERS'; + const SUBJECT_NOT_FOUND = 'SUBJECT_NOT_FOUND'; + const SUCCESS = 'SUCCESS'; + const SUCCESS_INSERTED = 'SUCCESS_INSERTED'; + const SUCCESS_NO_CHANGES_NEEDED = 'SUCCESS_NO_CHANGES_NEEDED'; + const SUCCESS_NO_INFO = 'SUCCESS_NO_INFO'; + const SUCCESS_UPDATED = 'SUCCESS_UPDATED'; +} + +class GrouperCodesToExceptionClassEnum { + const EXECUTE_FAILED = 'BadRequestException'; + const GROUP_NOT_FOUND = 'NotFoundException'; + const INSUFFICIENT_PRIVILEGES = 'UnauthorizedException'; + const INVALID_QUERY = 'BadRequestException'; + const NO_SUCH_OBJECT = 'NotFoundException'; + const PROBLEM_WITH_ASSIGNMENT = 'BadRequestException'; + const SUBJECT_NOT_FOUND = 'NotFoundException'; + const SUCCESS_NO_INFO = 'MethodNotAllowedException'; +} + +class GrouperGroupTypeEnum { + const ADMIN = 'admin'; + const OPTINS = 'optins'; + const OPTOUTS = 'optouts'; + const STEM_ADMIN = 'stemAdmin'; + const UPDATE = 'update'; +} + +class GrouperConfigEnums { + // https://github.com/Internet2/grouper/blob/GROUPER_4_BRANCH/grouper/src/grouper/edu/internet2/middleware/grouper/cfg/GrouperConfig.java + const ALL = 'GrouperAll'; +} \ No newline at end of file diff --git a/Lib/lang.php b/Lib/lang.php index bcae1dd..8152ae0 100644 --- a/Lib/lang.php +++ b/Lib/lang.php @@ -1,6 +1,6 @@ 'Grouper Configuration Settings', 'pl.grouperlite.config.edit.title' => 'Edit Grouper Configuration Settings', 'pl.grouperlite.config.grouper-url' => 'Grouper Site URL', @@ -15,6 +15,10 @@ 'pl.grouperlite.config.wg-subscript' => '', 'pl.grouperlite.config.default-collapse' => 'Collapse groups by default?', 'pl.grouperlite.config.default-collapse-subscript' => '', + 'pl.grouperlite.config.identifier' => 'Identifier Type', + 'pl.grouperlite.config.identifier.desc' => 'Identifier eligible to make requests to Grouper', + 'pl.grouperlite.config.act-as-grp-name' => 'Act-As Group Name', + 'pl.grouperlite.config.act-as-grp-name.desc' => 'Members of the Group are authorized to Act-As another User.', 'pl.grouperlite.crumb.root' => 'Grouper', 'pl.grouperlite.nav.groups-can-join' => 'Groups I can join', @@ -28,11 +32,12 @@ 'pl.grouperlite.nav.create-email' => 'Create email list', 'pl.grouperlite.nav.emaillists' => 'Email Lists', 'pl.grouperlite.nav.groups' => 'Groups', + 'pl.grouperlite.nav.users-presided' => 'Users I manage', 'pl.grouperlite.dashboard.heading.groups' => 'Groups', 'pl.grouperlite.dashboard.heading.email-lists' => 'Email lists', - 'pl.grouperlite.title.root' => 'Grouper Collaborations:', + 'pl.grouperlite.title.dashboard' => 'Grouper Lite Dashboard', 'pl.grouperlite.title.groupinfo' => 'Group configuration and attributes', 'pl.grouperlite.title.groupowner' => 'Groups I manage', 'pl.grouperlite.title.groupmember' => 'My Memberships', @@ -43,6 +48,7 @@ 'pl.grouperlite.title.emaillistsinfo' => 'Email list configuration and attributes', 'pl.grouperlite.title.groupcreate' => 'Create a group', 'pl.grouperlite.title.templatecreate' => 'Create a working group', + 'pl.grouperlite.title.root' => 'Grouper Collaborations:', 'pl.grouperlite.message.flash.join-group-success' => 'You have been added to the group:', 'pl.grouperlite.message.flash.join-group-failed' => 'You are unable to join the group:', @@ -116,10 +122,14 @@ 'pl.grouperlite.action.view-members' => 'View members', 'pl.grouperlite.action.grouper' => 'Grouper', 'pl.grouperlite.action.members' => 'Members', + 'pl.grouperlite.action.memberships' => 'Memberships', 'pl.grouperlite.action.close' => 'Close', 'pl.grouperlite.action.clear' => 'Clear', 'pl.grouperlite.action.add-user' => 'Add', + 'pl.grouperlite.action.act-as' => 'Act As', 'pl.grouperlite.action.remove-user' => 'Remove', + 'pl.grouperlite.action.enable' => 'Enable', + 'pl.grouperlite.action.find' => 'Find', 'pl.grouperlite.message.user-not-found-error' => 'Error: User not found.', 'pl.grouperlite.message.user-not-added-error' => 'Error: Unable to add user.', 'pl.grouperlite.message.user-not-removed-error' => 'Error: Unable to remove user.', @@ -158,7 +168,7 @@ 'pl.grouperlite.form.template.value.positive' => 'Yes', 'pl.grouperlite.form.template.value.negative' => 'No', - 'pl.grouperlite.search.tags.text' => 'Search', + 'pl.grouperlite.title.search' => 'Search', 'pl.grouperlite.pagination.counter' => 'Viewing {:start}-{:end} of {:count}', 'pl.grouperlite.attributes.zero-state' => 'No Attributes Associated to this Group.', @@ -166,5 +176,7 @@ 'pl.grouperlite.working-groups.zero-state' => 'None.', 'pl.grouperlite.email-lists.zero-state' => 'No email lists found.', 'pl.grouperlite.members.noaccess' => 'You do not have access to view memberships.', - 'pl.grouperlite.members.empty' => 'This group has no member OR you are not authorized to see the members of this group.' -); \ No newline at end of file + 'pl.grouperlite.members.empty' => 'This group has no member OR you are not authorized to see the members of this group.', + + 'er.grouperlite.glid' => 'Named parameter glid was not found', +]; \ No newline at end of file diff --git a/Model/ActAsPerson.php b/Model/ActAsPerson.php new file mode 100644 index 0000000..15d6299 --- /dev/null +++ b/Model/ActAsPerson.php @@ -0,0 +1,77 @@ + ['ActAsPerson'], + 'CoPerson' => [ + 'ActAsPerson' => [ + 'className' => 'ActAsPerson', + 'foreignKey' => 'co_person_id' + ] + ], + 'CoPerson' => [ + 'ActAsPerson' => [ + 'className' => 'ActAsPerson', + 'foreignKey' => 'act_as_co_person_id' + ] + ] + ]; + + // Association rules from this model to other models + public $belongsTo = [ + 'ActAsCoPerson' => [ + 'className' => 'CoPerson', + 'foreignKey' => 'act_as_co_person_id' + ], + 'CoPerson', + 'CoGrouperLiteWidget' + ]; + + // Validation rules for table elements + public $validate = [ + 'co_grouper_lite_widget_id' => [ + 'rule' => 'numeric', + 'required' => true, + 'allowEmpty' => false + ], + 'act_as_co_person_id' => [ + 'rule' => 'numeric', + 'required' => true, + 'allowEmpty' => false + ], + 'co_person_id' => [ + 'rule' => 'numeric', + 'required' => true, + 'allowEmpty' => false + ], + ]; + +} \ No newline at end of file diff --git a/Model/CoGrouperLite.php b/Model/CoGrouperLite.php deleted file mode 100644 index 1226065..0000000 --- a/Model/CoGrouperLite.php +++ /dev/null @@ -1,98 +0,0 @@ - array( - "parent" => "CoDashboardWidget", - "fk" => "co_dashboard_widget_id" - ) - );*/ - - // Validation rules for table elements - //Tried adding rule to each field to make alphaNumeric, but keeps throwing errors. will research. - public $validate = array( - 'co_dashboard_widget_id' => array( - 'rule' => 'alphaNumeric', - 'required' => true - ), - 'conn_url' => array( - 'rule' => array('custom', '/^https?:\/\/.*/'), - 'required' => true - ), - 'conn_ver' => array( - 'rule' => array('minLength', 4), - 'required' => true - ), - 'grouper_url' => array( - 'rule' => array('custom', '/^https?:\/\/.*/'), - 'required' => true - ), - 'conn_user' => array( - 'rule' => array('minLength', 1), - 'required' => true - ), - 'conn_pass' => array( - 'rule' => array('minLength', 1), - 'required' => true - ), - 'adhoc_heading' => array( - 'rule' => array('minLength', 1), - 'required' => false, - 'default' => 'Ad-hoc groups' - ), - 'wg_heading' => array( - 'rule' => array('minLength', 1), - 'required' => false, - 'default' => 'Working groups' - ), - 'default_collapse' => array( - 'rule' => array('minLength', 1), - 'required' => false, - 'default' => 'collapsed' - ), - ); - -} \ No newline at end of file diff --git a/Model/CoGrouperLiteWidget.php b/Model/CoGrouperLiteWidget.php new file mode 100644 index 0000000..5181291 --- /dev/null +++ b/Model/CoGrouperLiteWidget.php @@ -0,0 +1,159 @@ + array( + 'rule' => 'numeric', + 'required' => true, + 'allowEmpty' => false + ), + 'conn_url' => array( + 'rule' => array('custom', '/^https?:\/\/.*/'), + 'required' => true, + 'allowEmpty' => false + ), + 'conn_ver' => array( + 'rule' => array('minLength', 4), + 'required' => true, + 'allowEmpty' => false + ), + 'grouper_url' => array( + 'rule' => array('custom', '/^https?:\/\/.*/'), + 'required' => true, + 'allowEmpty' => false + ), + 'conn_user' => array( + 'rule' => array('minLength', 1), + 'required' => true, + 'allowEmpty' => false + ), + 'conn_pass' => array( + 'rule' => array('minLength', 1), + 'required' => true, + 'allowEmpty' => false + ), + 'adhoc_heading' => array( + 'rule' => array('minLength', 1), + 'required' => false, + 'allowEmpty' => true, + 'default' => 'Ad-hoc groups' + ), + 'wg_heading' => array( + 'rule' => array('minLength', 1), + 'required' => false, + 'allowEmpty' => true, + 'default' => 'Working groups' + ), + 'default_collapse' => array( + 'rule' => array('minLength', 1), + 'required' => false, + 'default' => 'collapsed' + ), + 'identifier_type' => array( + 'content' => array( + 'rule' => array( + 'validateExtendedType', + array('attribute' => 'Identifier.type', + 'default' => array(IdentifierEnum::AffiliateSOR, + IdentifierEnum::Badge, + IdentifierEnum::Enterprise, + IdentifierEnum::ePPN, + IdentifierEnum::ePTID, + IdentifierEnum::ePUID, + IdentifierEnum::GuestSOR, + IdentifierEnum::HRSOR, + IdentifierEnum::Mail, + IdentifierEnum::National, + IdentifierEnum::Network, + IdentifierEnum::OIDCsub, + IdentifierEnum::OpenID, + IdentifierEnum::ORCID, + IdentifierEnum::ProvisioningTarget, + IdentifierEnum::Reference, + IdentifierEnum::SamlPairwise, + IdentifierEnum::SamlSubject, + IdentifierEnum::SORID, + IdentifierEnum::StudentSOR, + IdentifierEnum::UID))), + 'required' => true, + 'allowEmpty' => false + ) + ), + // TODO: Are there any limitation regarding the acceptable group name characters + // For COmanage there are so we need to check this as well + 'act_as_grp_name' => array( + 'rule' => '/.*/', + 'required' => false, + 'allowEmpty' => true + ) + ); + + /** + * Actions to take before a validate operation is executed. + * + * @since COmanage Registry v4.4.0 + */ + + public function beforeValidate($options = array()) { + if(!empty($this->data[$this->alias]['co_dashboard_widget_id']) + && isset($this->id)) { + // Dashboard Widget Plugins will refer to Dashboard Widget, which in turn + // refers to a Dashboard + + $args = array(); + $args['conditions'][$this->alias.'.id'] = $this->id; + $args['contain']['CoDashboardWidget'][] = 'CoDashboard'; + + $codw = $this->find('first', $args); + + if(!empty($codw["CoDashboardWidget"]["CoDashboard"]["co_id"])) { + $contentRule = $this->validator()->getField('identifier_type')->getRule('content')->rule; + $contentRule[1]['coid'] = $codw["CoDashboardWidget"]["CoDashboard"]["co_id"]; + $this->validator()->getField('identifier_type')->getRule('content')->rule = $contentRule; + } + } + + return parent::beforeValidate($options); + } + +} \ No newline at end of file diff --git a/Model/CoManagePerson.php b/Model/CoManagePerson.php index 8d999c9..c8f1cc3 100644 --- a/Model/CoManagePerson.php +++ b/Model/CoManagePerson.php @@ -2,7 +2,7 @@ App::uses('CoPersonRole', 'Model/'); -class CoManagePerson extends GrouperLiteAppModel +class CoManagePerson extends GrouperLiteWidgetAppModel { public $name = "CoManagePerson"; diff --git a/Model/GrouperAttribute.php b/Model/GrouperAttribute.php index 7e00fe1..4df4c4d 100644 --- a/Model/GrouperAttribute.php +++ b/Model/GrouperAttribute.php @@ -25,14 +25,14 @@ * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ -App::uses('GrouperApiAccess', 'GrouperLite.Lib/'); +App::uses('GrouperApiAccess', 'GrouperLiteWidget.Lib/'); /** * Class GrouperAttribute * * Model class to get attributes from Grouper */ -class GrouperAttribute extends GrouperLiteAppModel +class GrouperAttribute extends GrouperLiteWidgetAppModel { /** @var string $name used by CakePHP for locating model */ @@ -44,9 +44,9 @@ class GrouperAttribute extends GrouperLiteAppModel /** * Used to instantiate API class */ - private function initApi() { + private function initApi(array $cfg) { if ($this->grouperAPI == null) { - $this->grouperAPI = new GrouperApiAccess(); + $this->grouperAPI = new GrouperApiAccess($cfg); } } @@ -55,10 +55,10 @@ private function initApi() { * * @param string $groupName Name of Group * @return array Attributes in Grouper for this Group - * @throws GrouperLiteException + * @throws GrouperLiteWidgetException */ public function getGroupAttributes(string $groupName) { - $this->initApi(); + $this->initApi($cfg); try { $args = array(); diff --git a/Model/GrouperGroup.php b/Model/GrouperGroup.php index 9bff932..e06c1b8 100644 --- a/Model/GrouperGroup.php +++ b/Model/GrouperGroup.php @@ -25,30 +25,24 @@ * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ -App::uses('GrouperApiAccess', 'GrouperLite.Lib/'); -App::uses('GrouperAttribute', 'GrouperLite.Model/'); +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 GrouperLiteAppModel +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"; + public $name = 'GrouperGroup'; /** @var GrouperApiAccess $grouperAPI */ - public $grouperAPI = null; - - private $totalRecords = 0; - - /** @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 $grouperAPI = null; private $wgStemsTopLevel = array( 'ref:incommon-collab', @@ -65,46 +59,37 @@ class GrouperGroup extends GrouperLiteAppModel '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. + * 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 - * @return String T or F - * @throws GrouperLiteException - * - * @see GrouperGroup::resetUserOwner() + * @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 isUserOwner(string $userId) + public function isUserGroupOwner(string $userId, string $actorUserId, array $cfg): bool { - if (CakeSession::check('Plugin.Grouper.isUserOwner')) { - return CakeSession::read('Plugin.Grouper.isUserOwner'); - } + $this->initApi($cfg); - $this->initApi(); + if(empty($userId) || empty($actorUserId)) { + return false; + } try { - $args = array(); - $args['userId'] = $userId; - - $ownGroups = $this->grouperAPI->getOwnedGroups($args); - - if (count($ownGroups) > 0) { - CakeSession::write('Plugin.Grouper.isUserOwner', 'T'); - return 'T'; - } - CakeSession::write('Plugin.Grouper.isUserOwner', 'F'); - return 'F'; - + $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; } /** @@ -112,262 +97,114 @@ public function isUserOwner(string $userId) * * @param string $userId Id of User * @return String T or F - * @throws GrouperLiteException + * @throws GrouperLiteWidgetException * + * @since COmanage Registry v4.4.0 */ - public function isGrouperVisible(string $userId) + public function isGrouperVisible(string $userId, array $cfg): string { - if (CakeSession::check('Plugin.Grouper.isGrouperVisible')) { - return CakeSession::read('Plugin.Grouper.isGrouperVisible'); - } - - $this->initApi(); + $this->initApi($cfg); try { - $args = array(); - $args['userId'] = $userId; - - $memberOfGroups = $this->grouperAPI->getGrouperMemberOfGroups($args); - - $memberOfGroups = $this->grouperAPI->getGrouperMemberOfGroups($args); - - //now cycle through and see if part of correct group to be able to use template - $member = 'F'; - foreach ($memberOfGroups as $memberOfGroup) { - if ($memberOfGroup['name'] == $this->grouperVisibleGroup) { - $member = 'T'; - break; - } - } - - if ($member == 'T') { - CakeSession::write('Plugin.Grouper.isGrouperVisible', 'T'); - return 'T'; - } else { - CakeSession::write('Plugin.Grouper.isGrouperVisible', 'F'); - return 'F'; - } - + $isMember = $this->grouperAPI->isMemberOfGroup(GrouperSpecialGroups::GROUPER_VISIBLE_GROUP, $userId); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } - } - /** - * Used to instantiate API class - * - * Tried to use the constructor for GrouperGroup to set this but it was triggering the GrouperAPIAccess class - * to instantiate before the Controller was able populate the connection settings from the DB into the Session, - * so error was being thrown. - * Now will call this before each function call to verify set. - */ - private function initApi() - { - if ($this->grouperAPI == null) { - $this->grouperAPI = new GrouperApiAccess(); - } - } - - /** - * Return all Groups that a User belongs to in Grouper. - * Will also add OptOut Groups and flag them as joined so can display Optout option in UI. - * - * @param array $conditions Listing of conditions for display of records, including UserId - * @return array Records of Groups from Grouper that the User belongs to - * @throws GrouperLiteException - * - */ - public function filteredMemberOfGroups(array $conditions) - { - $this->initApi(); - - try { - - $memberOfGroups = $this->memberOfGroups($conditions); - - $conditions['groupType'] = 'optouts'; - // Determine which groups can be left by user, if want. - $optOutGroups = $this->grouperAPI->getOptionalGroups($conditions); - - foreach ($memberOfGroups as &$memberOfGroup) { - $memberOfGroup['optOut'] = false; - foreach ($optOutGroups as $key => $value) { - if ($value['name'] == $memberOfGroup['name']) { - //Match! - $memberOfGroup['optOut'] = true; - //Remove Optin group since already found and now less loops - unset($optOutGroups[$key]); - break; - } - } - } - - $groupResults = array_values($memberOfGroups); - return $this->getFriendlyWorkingGroupName($groupResults, 'member'); - - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); - throw $e; - } + return $isMember ? 'T' : 'F'; } /** - * Internal process used by other functions to fetch Groups the User is a member of + * Grouper API Instance Singleton * - * @param array $conditions Listing of conditions for display of records, including UserId - * @return array Records of Groups from Grouper that the User belongs to - * @throws GrouperLiteException + * @throws GrouperLiteWidgetException * + * @since COmanage Registry v4.4.0 */ - private function memberOfGroups(array $conditions) - { - - try { - return $this->grouperAPI->getGrouperMemberOfGroups($conditions); - - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); - throw $e; + private function initApi(array $cfg) { + if ($this->grouperAPI === null) { + $this->grouperAPI = new GrouperApiAccess($cfg); } } - /** - * Process for User to Leave a Group + * Add a member to a specific Grouper Group * - * @param string $userId Id of User - * @param string $groupName Name of Group Leaving, do not confuse with DisplayName field! - * @return bool True|False - * @throws GrouperLiteException + * @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 leaveGroup(string $userId, string $groupName) + public function addGroupMember(string $actorUserId, string $groupName, string $addUserId, array $cfg) { - $this->initApi(); + $this->initApi($cfg); - try { - $args = array(); - $args['LeaveJoin'] = 'Leave'; - $args['userId'] = $userId; - $args['groupName'] = $groupName; - - return $this->grouperAPI->grouperGroupLeaveOrJoin($args); - - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); - throw $e; - } + return $this->grouperAPI->addGroupMember($actorUserId, $groupName, $addUserId); } /** - * Process for User to Join a Group + * Breakout Working Groups from AdHoc Groups. * - * @param string $userId Id of User - * @param string $groupName Name of Group Joining, do not confuse with DisplayName field! - * @return bool True|False - * @throws GrouperLiteException + * @param array $recordSet + * @return array[] * */ - public function joinGroup(string $userId, string $groupName) + public function breakoutWrkFromAdHocGroups(array $recordSet): array { - $this->initApi(); - - try { - $args = array(); - $args['LeaveJoin'] = 'Join'; - $args['userId'] = $userId; - $args['groupName'] = $groupName; - - return $this->grouperAPI->grouperGroupLeaveOrJoin($args); - - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); - throw $e; + $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 all Grouper Groups that the User has a role of owner/admin - * - * @param array $conditions Listing of conditions for display of records, including UserId - * @return array - * @throws GrouperLiteException - * - */ - public function ownerGroups(array $conditions) - { - $this->initApi(); - - try { - $resultSet = $this->grouperAPI->getOwnedGroups($conditions); - return $this->getFriendlyName(array_values($resultSet)); - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); - throw $e; - } + return [ + 'adhoc' => $notWGData, + 'working' => $wgData + ]; } /** - * 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 + * Create a new Grouper Group using the Template methodology in Grouper * - * @param array $conditions Listing of conditions for display of records * @param string $userId Id of User - * @return array Listing of members in requested Grouper Group - * @throws GrouperLiteException Captured in Controller + * @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 membersInGroup(array $conditions, string $userId) + public function createGroupWithTemplate(string $userId, array $groupData, array $cfg) { - $this->initApi(); - - $conditions['userId'] = $userId; - - try { - $groupMembers = $this->grouperAPI->getMembersInGroup($conditions); - - if (count($groupMembers) < 1) { - return $groupMembers; - } - - $finalMembers = array(); - foreach ($groupMembers as $member) { - if ($member['sourceId'] !== 'g:gsa') { - $finalMembers[] = $member; - } - - } - return $finalMembers; - - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); - throw $e; + $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'; } - } - - /** - * Add a member to a specific Grouper Group - * - * @param array $conditions Listing of conditions for display of records - * @param string $userId Id of User - * @return string success of Request - * @throws GrouperLiteException Captured in Controller - * - */ - public function addMemberToGroup(array $conditions, string $userId) - { - $this->initApi(); - - $conditions['userId'] = $userId; + $args = array(); + $args['userId'] = $userId; + $args['data'] = $groupData; try { - $resultAdd = $this->grouperAPI->addMemberToGroup($conditions); - - return $resultAdd; - + return $this->grouperAPI->createGroupWithTemplate($args); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; @@ -375,221 +212,173 @@ public function addMemberToGroup(array $conditions, string $userId) } /** - * Remove a member from a specific Grouper Group + * Construct picker(like) response data based on mode and term * - * @param array $conditions Listing of conditions for display of records - * @param string $userId Id of User - * @return string success of Request - * @throws GrouperLiteException Captured in Controller + * @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 removeMemberToGroup(array $conditions, string $userId) + public function dataConstructForPicker(int $coId, string $mode, array $coPersonIds): array { - $this->initApi(); - - $conditions['userId'] = $userId; - - try { - $resultRemove = $this->grouperAPI->removeMemberToGroup($conditions); - - return $resultRemove; - - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); - throw $e; + if(empty($coPersonIds)) { + return []; } - } + $this->CoPerson = ClassRegistry::init('CoPerson'); + $this->Co = ClassRegistry::init('Co'); - /** - * 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 array $conditions Listing of conditions for display of records, including UserId - * @return array Listing of Optin groups available in Grouper - * @throws GrouperLiteException Captured in Controller - */ - public function optinGroups(array $conditions) - { - $this->initApi(); + $matches = []; - try { - $conditions['groupType'] = 'optins'; - - $joinOrLeave = $this->grouperAPI->getOptionalGroups($conditions); - $userGroups = $this->memberOfGroups($conditions); - - //See if Optin group match any of the groups user already belongs to. - foreach ($joinOrLeave as $key => $value) { - foreach ($userGroups as $userGroup) { - if ($value['name'] == $userGroup['name']) { - //Match!! - //Removing from array since already a member of. - unset($joinOrLeave[$key]); - break; + 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; + } } } - } - - return $this->getFriendlyName(array_values($joinOrLeave)); - - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); - throw $e; - } - } - - /** - * Determine if User can use the Grouper Template to create a Working Group. - * - * @param string $userId Id of User - * @return string T for True and F for False - * @throws GrouperLiteException - */ - public function isTemplateUser(string $userId) - { - if (CakeSession::check('Plugin.Grouper.isTemplateUser')) { - return CakeSession::read('Plugin.Grouper.isTemplateUser'); - } - - $this->initApi(); - - try { - $args = array(); - $args['userId'] = $userId; - - $memberOfGroups = $this->grouperAPI->getGrouperMemberOfGroups($args); - //now cycle through and see if part of correct group to be able to use template - $member = 'F'; - foreach ($memberOfGroups as $memberOfGroup) { - if ($memberOfGroup['name'] == $this->templateCreationGroup) { - $member = 'T'; - 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; + } + } } - } - if ($member == 'T') { - CakeSession::write('Plugin.Grouper.isTemplateUser', 'T'); - return 'T'; + // 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 + ); + } } - CakeSession::write('Plugin.Grouper.isTemplateUser', 'F'); - return 'F'; - - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); - throw $e; } + return $matches; } /** - * Create a new Grouper Group using the Template methodology in Grouper + * 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 Id of User - * @param array $groupData Data needed to create new Grouper Group via Template - * @return array status and error message, if applicable - * @throws GrouperLiteException + * @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 createGroupWithTemplate(string $userId, array $groupData) + public function filteredMemberOfGroups(string $userId, string $actorUserId, array $cfg): array { - $this->initApi(); + $this->initApi($cfg); try { - //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; + $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'); - // Reset Session variable that shows if User is an owner or not - $this->resetUserOwner(); + foreach ($memberOfGroups as &$memberOfGroup) { + $memberOfGroup['optOut'] = isset($optOutGroupsNames[$memberOfGroup['name']]); + } - return $this->grouperAPI->createGroupWithTemplate($args); + return $this->getFriendlyWorkingGroupName($memberOfGroups, 'member'); } catch (Exception $e) { CakeLog::write('error', __METHOD__ . ': An error occurred'); throw $e; } - } /** - * Private function used to reset the IsUserOwner Session variable, - * that maintains the status of a user being an owner/admin of a group + * Find People based on mode and term * - * @see GrouperGroup::isUserOwner() - */ - private function resetUserOwner() - { - if (CakeSession::check('Plugin.Grouper.isUserOwner')) { - CakeSession::delete('Plugin.Grouper.isUserOwner'); - } - } - - /** - * Search for Groups/Lists related to Search term. + * @param integer $coId CO ID + * @param string $mode Search mode to apply filters for + * @param string|null $term Search block * - * Will import all records the user can see and then do search in this code rather than call Grouper WS Search - * functionality. This is due to the fact that the grouperName is autogenerated and this app needs to search - * attributes which the Grouper WS does not do. - * - * @param array $conditions Listing of conditions for display of records, including UserId - * @return array Records that meet search criteria - * @throws Exception Captured in Controller + * @return array Array of CO Person records + * @since COmanage Registry v4.4.0 */ - public function getSearchedGroups(array $conditions) + public function findForPicker(int $coId, string $mode, ?string $term): array { - $this->initApi(); + $coPersonIds = []; + $this->CoPerson = ClassRegistry::init('CoPerson'); - try { - //Breakout page where search was called and forward to appropriate method for processing - $page = $conditions['searchpage']; - $pageResults = $this->$page($conditions); + // jquery Autocomplete sends the search as url?term=foo + if(!empty($term)) { + // Leverage-model-specific keyword search - $returnResults = array(); - $searchCriteria = $conditions['searchcriteria']; + // Note EmailAddress and Identifier don't support substring search + foreach(array('Name', 'EmailAddress', 'Identifier') as $m) { + $hits = $this->CoPerson->$m->search($coId, $term, 25); - 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; - } + $coPersonIds = array_merge($coPersonIds, Hash::extract($hits, '{n}.CoPerson.id')); } + } - if(isset($conditions['']) && $conditions['getSearchedGroups']){ - return $this->getFriendlyWorkingGroupName($returnResults, 'member'); - } else { - return $returnResults; - } + $coPersonIds = array_unique($coPersonIds); - } catch (Exception $e) { - CakeLog::write('error', __METHOD__ . ': An error occurred'); - throw $e; - } + // 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. + * 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. * @@ -598,10 +387,10 @@ public function getSearchedGroups(array $conditions) * * @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, $method) - { + private function getFriendlyWorkingGroupName(array $groups, string $method) { $arrayIndex = 0; $workingGroups = array(); @@ -612,7 +401,7 @@ private function getFriendlyWorkingGroupName(array $groups, $method) $len = strlen($stem); if (substr(strtolower($group['name']), 0, $len) === $stem) { $stemSections = explode(':', $group['name']); - //Get third section, since will always by after ref:something:here + //Get a third section, since will always by after ref:something:here if (in_array($stemSections[2], $topLevelWG) === false) { $topLevelWG[] = $stemSections[2]; } @@ -674,10 +463,6 @@ private function getFriendlyWorkingGroupName(array $groups, $method) } $tempGroup['WGApp'] = $appName; if ($method == 'member') { - // Screening out groups owned since this page show memberships not ownership! -// if ($tempGroup['WGRole'] !== 'admin' && $tempGroup['WGRole'] !== 'owner' && !$mainGroup ) { -// $workingGroups[] = $tempGroup; -// } if(!$mainGroup) { $workingGroups[] = $tempGroup; unset($groups[$arrayIndex]); @@ -719,7 +504,7 @@ private function getFriendlyWorkingGroupName(array $groups, $method) } } - $friendlyGroups = $this->getFriendlyName(array_values($groups)); + $friendlyGroups = array_values($groups); //Now need to add the groups back together for one set foreach ($friendlyGroups as $friendlyGroup) { @@ -729,25 +514,405 @@ private function getFriendlyWorkingGroupName(array $groups, $method) 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; + } /** - * Determine if result set contains friendly name, if so add as a new attribute in result set - * NOTE: Used by Ad-Hoc groups only, since WG are listed under a Grouping + * 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 array $groups Current result set of Groups - * @return array Same result set with added param for friendly name + * @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 */ - private function getFriendlyName(array $groups) + public function getGroupMembers(string $actorUserId, string $groupName, array $cfg): array { + $this->initApi($cfg); - //According to Jira ICPCO-200, will only display the last section of the stem in UI and then hover over for rest. - foreach ($groups as &$group) { - $friendlySections = explode(':', $group['displayName']); - $group['friendlyName'] = end($friendlySections); + 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; } - return $groups; + // 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); + } } diff --git a/Model/GrouperLite.php b/Model/GrouperLiteWidget.php similarity index 77% rename from Model/GrouperLite.php rename to Model/GrouperLiteWidget.php index dec2df3..d5b8aa3 100644 --- a/Model/GrouperLite.php +++ b/Model/GrouperLiteWidget.php @@ -25,34 +25,27 @@ * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ -class GrouperLite extends AppModel { +class GrouperLiteWidget extends AppModel { // Define class name for cake - public $name = "GrouperLite"; + public $name = 'GrouperLiteWidget'; // Required by COmanage Plugins - public $cmPluginType = "dashboardwidget"; + public $cmPluginType = 'dashboardwidget'; + + // Document foreign keys + public $cmPluginHasMany = []; // Association rules from this model to other models - public $belongsTo = array( - ); + public $belongsTo = []; - public $hasMany = array( - ); + public $hasMany = [ + 'ActAsPerson' => ['dependent' => true] + ]; // Validation rules for table elements - public $validate = array( - ); + public $validate = []; public function cmPluginMenus() { - return array( - /*"cogroups" => array( - 'Grouper groups' => array( - 'controller' => "groupergroups", - 'action' => "groupoptin" - ) - )*/ - ); + return []; } - - } \ No newline at end of file diff --git a/Model/GrouperLiteAppModel.php b/Model/GrouperLiteWidgetAppModel.php similarity index 95% rename from Model/GrouperLiteAppModel.php rename to Model/GrouperLiteWidgetAppModel.php index 18cbb6d..be6cb29 100644 --- a/Model/GrouperLiteAppModel.php +++ b/Model/GrouperLiteWidgetAppModel.php @@ -27,6 +27,6 @@ App::uses('AppModel', 'Model'); -class GrouperLiteAppModel extends AppModel { +class GrouperLiteWidgetAppModel extends AppModel { } diff --git a/PlantUML/1.puml b/PlantUML/1.puml deleted file mode 100644 index 86c136e..0000000 --- a/PlantUML/1.puml +++ /dev/null @@ -1,75 +0,0 @@ -# Data from API call to get all Groups belong to -@startjson -#highlight "WsGetGroupsLiteResult" / "wsGroups" -#highlight "WsGetGroupsLiteResult" / "wsGroups" / "0" / "name" -#highlight "WsGetGroupsLiteResult" / "wsGroups" / "0" / "displayName" -#highlight "WsGetGroupsLiteResult" / "wsGroups" / "0" / "description" -#highlight "WsGetGroupsLiteResult" / "wsGroups" / "1" / "name" -#highlight "WsGetGroupsLiteResult" / "wsGroups" / "1" / "displayName" -#highlight "WsGetGroupsLiteResult" / "wsGroups" / "1" / "description" -#highlight "WsGetGroupsLiteResult" / "wsGroups" / "2" / "name" -#highlight "WsGetGroupsLiteResult" / "wsGroups" / "2" / "displayName" -#highlight "WsGetGroupsLiteResult" / "wsGroups" / "2" / "description" -#highlight "WsGetGroupsLiteResult" / "wsGroups" / "3" / "name" -#highlight "WsGetGroupsLiteResult" / "wsGroups" / "3" / "displayName" -#highlight "WsGetGroupsLiteResult" / "wsGroups" / "3" / "description" -{ - "WsGetGroupsLiteResult": { - "resultMetadata": { - "success": "T", - "resultCode": "SUCCESS" - }, - "wsSubject": { - "...": "..." - }, - "responseMetadata": { - "...": "..." - }, - "wsGroups": [ - { - "extension": "CO_members_active", - "displayName": "app:comanage-provision:CO_members_active", - "description": "Internet2 Collaborations Active Members", - "uuid": "f5dae468b9d9429993992781712c2f83", - "enabled": "T", - "displayExtension": "CO_members_active", - "name": "app:comanage-provision:CO_members_active", - "typeOfGroup": "group", - "idIndex": "11104" - }, - { - "extension": "grouperUiUserData", - "displayName": "etc:grouperUi:grouperUiUserData", - "description": "Internal group for grouper which has ...", - "uuid": "2748e23e51174145a4dc4d9e115c59da", - "enabled": "T", - "displayExtension": "grouperUiUserData", - "name": "etc:grouperUi:grouperUiUserData", - "typeOfGroup": "group", - "idIndex": "10015" - }, - { - "extension": "AdministeredByUniconFolks", - "displayName": "sandbox:UniconTest:Administered By Unicon Folks", - "description": "Description goes here", - "uuid": "0b26aa411a99405b9440be3d0b18dafa", - "enabled": "T", - "displayExtension": "Administered By Unicon Folks", - "name": "sandbox:UniconTest:AdministeredByUniconFolks", - "typeOfGroup": "group", - "idIndex": "18451" - }, - { - "extension": "MembersFromUniconAZ", - "displayName": "sandbox:UniconTest:Members At Unicon in AZ", - "uuid": "7a9f3b9837024a56b12a2a6259d520e0", - "enabled": "T", - "displayExtension": "Members At Unicon in AZ", - "name": "sandbox:UniconTest:MembersFromUniconAZ", - "typeOfGroup": "group", - "idIndex": "18447" - } - ] - } -} -@endjson \ No newline at end of file diff --git a/PlantUML/2.puml b/PlantUML/2.puml deleted file mode 100644 index 2cfc608..0000000 --- a/PlantUML/2.puml +++ /dev/null @@ -1,25 +0,0 @@ -@startuml -skinparam titleBorderRoundCorner 15 -skinparam titleBorderThickness 2 -skinparam titleBorderColor red -skinparam titleBackgroundColor Aqua-CadetBlue - -title Mapping of //My Membership// page to API results - -map "** API Grouper (with example data) => My Membership Page **" as CC { - extension = CO_members_active => NA - displayName = app:comanage-provision:CO_members_active => Name - description = Internet2 Collaborations Active Members => Description - uuid = f5dae468b9d9429993992781712c2f83 => NA - enabled = T => NA - displayExtension = CO_members_active => NA - name = app:comanage-provision:CO_members_active => Name - typeOfGroup = group => NA - idIndex = 11104 => NA -} - -note right of CC - Name Fields are used to determine Friendly Name -end note - -@enduml \ No newline at end of file diff --git a/PlantUML/3.puml b/PlantUML/3.puml deleted file mode 100644 index 16c8503..0000000 --- a/PlantUML/3.puml +++ /dev/null @@ -1,46 +0,0 @@ -@startuml -skinparam titleBorderRoundCorner 15 -skinparam titleBorderThickness 2 -skinparam titleBorderColor red -skinparam titleBackgroundColor Aqua-CadetBlue - -title Current Friendly name process for\nNon Working Groups - -start -:GET "Groups Member Of"; -:Group 1 -{ - "extension": "UniconMemberGroup", - "displayName": "sandbox:UniconTest:Unicon Members Group", - "uuid": "35c1ae4d9529492aac8cb2acb970279b", - "enabled": "T", - "displayExtension": "Unicon Members Group", - "name": "sandbox:UniconTest:UniconMemberGroup", - "typeOfGroup": "group", - "idIndex": "18446" -}; -partition "Compare Displayname and Name Params for Group 1" { -:Data: -**displayName** = "sandbox:UniconTest:Unicon Members Group" -**name** = "sandbox:UniconTest:UniconMemberGroup"; -:**Logic: Compare each Stem section**; - -if (Does "sandbox" == "sandbox"?) then (yes) - if (Does "UniconTest" == "UniconTest"?) then (yes) - if (Does "Unicon Members Group" == "UniconMemberGroup"?) then (yes) - :**FriendlyName** = "sandbox:UniconTest:UniconMemberGroup" - (No variance between **name** and **displayName**); - else (no) - :**FriendlyName** = "Unicon Members Group"; - endif - else (no) - #red:**FriendlyName** = "Unicon Members Group"; - note right: Not possible - endif -else (no) - #red:**FriendlyName** = "UniconTest:Unicon Members Group"; - note: Not possible -endif -stop -} -@enduml \ No newline at end of file diff --git a/PlantUML/4.puml b/PlantUML/4.puml deleted file mode 100644 index 37a5912..0000000 --- a/PlantUML/4.puml +++ /dev/null @@ -1,64 +0,0 @@ -# Current Friendly name process for Working Groups that is broken -@startuml -skinparam titleBorderRoundCorner 15 -skinparam titleBorderThickness 2 -skinparam titleBorderColor red -skinparam titleBackgroundColor Aqua-CadetBlue - -title Current Friendly name process for\na Working Groups that is broken - -start -:GET "Groups Member Of"; -fork -:Group 1 -{ - "extension": "admins", - "displayName": "app:confluence:Axel Working Group:admins", - "description": "Admins of confluence space for working group. Axel's working group for testing", - "uuid": "35eaa34fd2d443e5a66a0a355505f69e", - "enabled": "T", - "displayExtension": "admins", - "name": "app:confluence:AxelWorkingGroup:admins", - "typeOfGroup": "group", - "idIndex": "19010" -}; -fork again -:Group 2 -{ - "extension": "users", - "displayName": "app:confluence:Axel Working Group:users", - "description": "Users of confluence space for working group. Axel's working group for testing", - "uuid": "163dc892fa8e484a9262e6e9fa619791", - "enabled": "T", - "displayExtension": "users", - "name": "app:confluence:AxelWorkingGroup:users", - "typeOfGroup": "group", - "idIndex": "19011" -}; -end fork -partition "Compare Displayname and Name Params from Group 2" { -:Group 2 Data: -**displayName** = "app:confluence:Axel Working Group:users" -**name** = "app:confluence:AxelWorkingGroup:users"; -:**Logic: Compare each Stem section**; - -if (Does "app" == "app"?) then (yes) - if (Does "confluence" == "confluence"?) then (yes) - if (Does "Axel Working Group" == "AxelWorkingGroup"?) then (yes) - :**FriendlyName** = "app:confluence:AxelWorkingGroup:users" - (No variance between **name** and **displayName**); - else (no) - #red:**FriendlyName** = "Axel Working Group"; - note right: Missing **:users** - endif - else (no) - #red:**FriendlyName** = "Axel Working Group:users"; - note right: Not possible - endif -else (no) - #red:**FriendlyName** = "confluence:Axel Working Group:users"; - note: Not possible -endif -stop -} -@enduml \ No newline at end of file diff --git a/PlantUML/5.puml b/PlantUML/5.puml deleted file mode 100644 index 3856ac4..0000000 --- a/PlantUML/5.puml +++ /dev/null @@ -1,121 +0,0 @@ -# Removed "enabled"", "typeOfGroup", ""uuid"" and "idIndex" from returned records for brevity -@startjson -{ -"wsGroups": [ - { - "extension": "CO_members_active", - "displayName": "app:comanage-provision:CO_members_active", - "description": "Internet2 Collaborations Active Members", - "displayExtension": "CO_members_active", - "name": "app:comanage-provision:CO_members_active", - "typeOfGroup": "group", - "idIndex": "11104" - }, - { - "extension": "admins", - "displayName": "app:confluence:Axel Working Group:admins", - "description": "Admins of confluence space for working group. Axel's working group for testing", - "displayExtension": "admins", - "name": "app:confluence:AxelWorkingGroup:admins" - }, - { - "extension": "users", - "displayName": "app:confluence:Axel Working Group:users", - "description": "Users of confluence space for working group. Axel's working group for testing", - "displayExtension": "users", - "name": "app:confluence:AxelWorkingGroup:users" - }, - { - "extension": "admins", - "displayName": "app:confluence:NewWorkingGroupTest1155:admins", - "description": "Admins of confluence space for working group. NewWorkingGroupTest1155", - "displayExtension": "admins", - "name": "app:confluence:NewWorkingGroupTest1155:admins" - }, - { - "extension": "admins", - "displayName": "app:jira:AxelWorkingGroup:admins", - "description": "Users of jira project for working group. Axel's working group for testing", - "displayExtension": "admins", - "name": "app:jira:AxelWorkingGroup:admins" - }, - { - "extension": "users", - "displayName": "app:jira:AxelWorkingGroup:users", - "description": "Subscribers list receives working group emails. Axel's working group for testing", - "displayExtension": "users", - "name": "app:jira:AxelWorkingGroup:users" - }, - { - "extension": "admins", - "displayName": "app:jira:NewWorkingGroupTest1155:admins", - "description": "Users of jira project for working group. NewWorkingGroupTest1155", - "displayExtension": "admins", - "name": "app:jira:NewWorkingGroupTest1155:admins" - }, - { - "extension": "owners", - "displayName": "app:sympa:internet2:AxelWorkingGroup:owners", - "description": "Owners list manages the email list for the working group. Axel's working group for testing", - "displayExtension": "owners", - "name": "app:sympa:internet2:AxelWorkingGroup:owners" - }, - { - "extension": "subscribers", - "displayName": "app:sympa:internet2:AxelWorkingGroup:subscribers", - "description": "Subscribers list receives working group emails. Axel's working group for testing", - "displayExtension": "subscribers", - "name": "app:sympa:internet2:AxelWorkingGroup:subscribers" - }, - { - "extension": "owners", - "displayName": "app:sympa:internet2:NewWorkingGroupTest1155:owners", - "description": "Owners list manages the email list for the working group. NewWorkingGroupTest1155", - "displayExtension": "owners", - "name": "app:sympa:internet2:NewWorkingGroupTest1155:owners" - }, - { - "extension": "grouperUiUserData", - "displayName": "etc:grouperUi:grouperUiUserData", - "description": "Internal group for grouper which has user data stored ...", - "displayExtension": "grouperUiUserData", - "name": "etc:grouperUi:grouperUiUserData" - }, - { - "extension": "sysadmingroup", - "displayName": "etc:sysadmingroup", - "description": "system administrators with all privileges", - "displayExtension": "sysadmingroup", - "name": "etc:sysadmingroup" - }, - { - "extension": "admins", - "displayName": "ref:InCommon-collab:AxelWorkingGroup:AxelWorkingGroup admins", - "description": "Admins role means can manage / attest the working group. Axel's working group for testing", - "displayExtension": "AxelWorkingGroup admins", - "name": "ref:incommon-collab:AxelWorkingGroup:admins" - }, - { - "extension": "users", - "displayName": "ref:InCommon-collab:AxelWorkingGroup:AxelWorkingGroup users", - "description": "Users role means members of the working group with access to collaboration tools. Axel's working group for testing", - "displayExtension": "AxelWorkingGroup users", - "name": "ref:incommon-collab:AxelWorkingGroup:users" - }, - { - "extension": "admins", - "displayName": "ref:InCommon-collab:NewWorkingGroupTest1155:NewWorkingGroupTest1155 admins", - "description": "Admins role means can manage / attest the working group. NewWorkingGroupTest1155", - "displayExtension": "NewWorkingGroupTest1155 admins", - "name": "ref:incommon-collab:NewWorkingGroupTest1155:admins" - }, - { - "extension": "workinggroupadmins", - "displayName": "ref:workinggroupadmins", - "description": "Being a member of this group enables you to create collaboration groups under ...", - "displayExtension": "workinggroupadmins", - "name": "ref:workinggroupadmins" - } - ] -} -@endjson \ No newline at end of file diff --git a/PlantUML/6.puml b/PlantUML/6.puml deleted file mode 100644 index 0999820..0000000 --- a/PlantUML/6.puml +++ /dev/null @@ -1,27 +0,0 @@ -# Info taken from https://spaces.at.internet2.edu/display/Grouper/Grouper+custom+template+via+GSH+Internet2+example -@startuml -skinparam titleBorderRoundCorner 15 -skinparam titleBorderThickness 2 -skinparam titleBorderColor red -skinparam titleBackgroundColor Aqua-CadetBlue - -title Process groups to verify if part of\na Working Group - -start -:Working Group Stems: ----- -* "app:jira" -* "app:confluence" -* "app:sympa:internet2" -* "app:sympa:incommon" -* "ref:InCommon-collab"; - -partition "For Each Group" { -if (Does "app:jira:AxelWorkingGroup:users" start with any **Working Group Stem**?) then (yes) - :Hold as part of a Working Group; -else (no) - :Next record"; -endif -} -stop -@enduml \ No newline at end of file diff --git a/PlantUML/7.puml b/PlantUML/7.puml deleted file mode 100644 index 24616f8..0000000 --- a/PlantUML/7.puml +++ /dev/null @@ -1,63 +0,0 @@ -@startuml -skinparam titleBorderRoundCorner 15 -skinparam titleBorderThickness 2 -skinparam titleBorderColor red -skinparam titleBackgroundColor Aqua-CadetBlue - -title With records held as part of\na Working Group - -start -:GET "Groups Member Of"; -fork -:Group 1 -{ - "extension": "admins", - "displayName": "app:confluence:Bill Working Group:admins", - "description": "Admins of confluence space for working group." - "displayExtension": "admins", - "name": "app:confluence:BillWorkingGroup:admins" -}; -fork again -:Group 2 -{ - "extension": "users", - "displayName": "app:confluence:Axel Working Group:users", - "description": "Users of confluence space for working group." - "displayExtension": "users", - "name": "app:confluence:AxelWorkingGroup:users" -}; -fork again -:Group 3 -{ - "extension": "admins", - "displayName": "app:jira:AxelWorkingGroup:admins", - "description": "Users of jira project for working group." - "displayExtension": "admins", - "name": "app:jira:AxelWorkingGroup:admins" -}; -end fork -partition "Compare Each Group to see if same Working Group" { -:Compare Group 1 and Group 2 -**Group 1 name** = "app:confluence:BillWorkingGroup:admins" -**Group 2 name** = "app:confluence:AxelWorkingGroup:users"; -:Assumption = Last section of name value is **ALWAYS** Members (users, admins) , therefore second to last section is Working Group Name; -if (Does "BillWorkingGroup" == "AxelWorkingGroup"?) then (yes) - :Save to BillWorkingGroup; -endif -:Compare Group 2 and Group 3; -if (Does "AxelWorkingGroup" == "AxelWorkingGroup"?) then (yes) - :Save to AxelWorkingGroup; -endif -} -stop - -:=//My Membership:// ----- -**BillWorkingGroup:** -* Confluence Admins ----- -**Axel Working Group:** -* Confluence Users -* Jira Admins; - -@enduml \ No newline at end of file diff --git a/PlantUML/GroupsBelongTo.puml b/PlantUML/GroupsBelongTo.puml deleted file mode 100644 index b8c4fc9..0000000 --- a/PlantUML/GroupsBelongTo.puml +++ /dev/null @@ -1,219 +0,0 @@ - -# Removed "enabled"", "typeOfGroup", ""uuid"" and "idIndex" from returned records for brevity -@startjson -{ -"wsGroups": [ - { - "extension": "CO_members_active", - "displayName": "app:comanage-provision:CO_members_active", - "description": "Internet2 Collaborations Active Members", - "displayExtension": "CO_members_active", - "name": "app:comanage-provision:CO_members_active", - "typeOfGroup": "group", - "idIndex": "11104" - }, - { - "extension": "admins", - "displayName": "app:confluence:Axel Working Group:admins", - "description": "Admins of confluence space for working group. Axel's working group for testing", - "displayExtension": "admins", - "name": "app:confluence:ExampleWGName:admins" - }, - { - "extension": "users", - "displayName": "app:confluence:Axel Working Group:users", - "description": "Users of confluence space for working group. Axel's working group for testing", - "displayExtension": "users", - "name": "app:confluence:ExampleWGName:users" - }, - { - "extension": "admins", - "displayName": "app:confluence:NewWorkingGroupTest1155:admins", - "description": "Admins of confluence space for working group. NewWorkingGroupTest1155", - "displayExtension": "admins", - "name": "app:confluence:NewWorkingGroupTest1155:admins" - }, - { - "extension": "admins", - "displayName": "app:jira:ExampleWGName:admins", - "description": "Users of jira project for working group. Axel's working group for testing", - "displayExtension": "admins", - "name": "app:jira:ExampleWGName:admins" - }, - { - "extension": "users", - "displayName": "app:jira:ExampleWGName:users", - "description": "Subscribers list receives working group emails. Axel's working group for testing", - "displayExtension": "users", - "name": "app:jira:ExampleWGName:users" - }, - { - "extension": "admins", - "displayName": "app:jira:NewWorkingGroupTest1155:admins", - "description": "Users of jira project for working group. NewWorkingGroupTest1155", - "displayExtension": "admins", - "name": "app:jira:NewWorkingGroupTest1155:admins" - }, - { - "extension": "owners", - "displayName": "app:sympa:internet2:ExampleWGName:owners", - "description": "Owners list manages the email list for the working group. Axel's working group for testing", - "displayExtension": "owners", - "name": "app:sympa:internet2:ExampleWGName:owners" - }, - { - "extension": "subscribers", - "displayName": "app:sympa:internet2:ExampleWGName:subscribers", - "description": "Subscribers list receives working group emails. Axel's working group for testing", - "displayExtension": "subscribers", - "name": "app:sympa:internet2:ExampleWGName:subscribers" - }, - { - "extension": "owners", - "displayName": "app:sympa:internet2:NewWorkingGroupTest1155:owners", - "description": "Owners list manages the email list for the working group. NewWorkingGroupTest1155", - "displayExtension": "owners", - "name": "app:sympa:internet2:NewWorkingGroupTest1155:owners" - }, - { - "extension": "grouperUiUserData", - "displayName": "etc:grouperUi:grouperUiUserData", - "description": "Internal group for grouper which has user data stored ...", - "displayExtension": "grouperUiUserData", - "name": "etc:grouperUi:grouperUiUserData" - }, - { - "extension": "sysadmingroup", - "displayName": "etc:sysadmingroup", - "description": "system administrators with all privileges", - "displayExtension": "sysadmingroup", - "name": "etc:sysadmingroup" - }, - { - "extension": "admins", - "displayName": "ref:InCommon-collab:ExampleWGName:ExampleWGName admins", - "description": "Admins role means can manage / attest the working group. Axel's working group for testing", - "displayExtension": "ExampleWGName admins", - "name": "ref:incommon-collab:ExampleWGName:admins" - }, - { - "extension": "users", - "displayName": "ref:InCommon-collab:ExampleWGName:ExampleWGName users", - "description": "Users role means members of the working group with access to collaboration tools. Axel's working group for testing", - "displayExtension": "ExampleWGName users", - "name": "ref:incommon-collab:ExampleWGName:users" - }, - { - "extension": "admins", - "displayName": "ref:InCommon-collab:NewWorkingGroupTest1155:NewWorkingGroupTest1155 admins", - "description": "Admins role means can manage / attest the working group. NewWorkingGroupTest1155", - "displayExtension": "NewWorkingGroupTest1155 admins", - "name": "ref:incommon-collab:NewWorkingGroupTest1155:admins" - }, - { - "extension": "workinggroupadmins", - "displayName": "ref:workinggroupadmins", - "description": "Being a member of this group enables you to create collaboration groups under ...", - "displayExtension": "workinggroupadmins", - "name": "ref:workinggroupadmins" - } - ] -} -@endjson - - -============================================================= - -# Info taken from https://spaces.at.internet2.edu/display/Grouper/Grouper+custom+template+via+GSH+Internet2+example -@startuml -skinparam titleBorderRoundCorner 15 -skinparam titleBorderThickness 2 -skinparam titleBorderColor red -skinparam titleBackgroundColor Aqua-CadetBlue - -title Process groups to verify if part of\na Working Group - -start -:Working Group Stems: ----- -* "app:jira" -* "app:confluence" -* "app:sympa:internet2" -* "app:sympa:incommon" -* "ref:InCommon-collab"; - -partition "For Each Group" { -if (Does "app:jira:ExampleWGName:users" start with any **Working Group Stem**?) then (yes) - :Hold as part of a Working Group; -else (no) - :Next record"; -endif -} -stop -@enduml - -============================================================= - -@startuml -skinparam titleBorderRoundCorner 15 -skinparam titleBorderThickness 2 -skinparam titleBorderColor red -skinparam titleBackgroundColor Aqua-CadetBlue - -title With records held as part of\na Working Group - -start -:GET "Groups Member Of"; -fork -:Group 1 -{ - "extension": "admins", - "displayName": "app:confluence:Bill Working Group:admins", - "description": "Admins of confluence space for working group." - "displayExtension": "admins", - "name": "app:confluence:BillWorkingGroup:admins" -}; -fork again -:Group 2 -{ - "extension": "users", - "displayName": "app:confluence:Axel Working Group:users", - "description": "Users of confluence space for working group." - "displayExtension": "users", - "name": "app:confluence:ExampleWGName:users" -}; -fork again -:Group 3 -{ - "extension": "admins", - "displayName": "app:jira:ExampleWGName:admins", - "description": "Users of jira project for working group." - "displayExtension": "admins", - "name": "app:jira:ExampleWGName:admins" -}; -end fork -partition "Compare Each Group to see if same Working Group" { -:Compare Group 1 and Group 2 -**Group 1 name** = "app:confluence:BillWorkingGroup:admins" -**Group 2 name** = "app:confluence:ExampleWGName:users"; -:Assumption = Last section of name value is **ALWAYS** Members (users, admins) , therefore second to last section is Working Group Name; -if (Does "BillWorkingGroup" == "ExampleWGName"?) then (yes) - :Save to BillWorkingGroup; -endif -:Compare Group 2 and Group 3; -if (Does "ExampleWGName" == "ExampleWGName"?) then (yes) - :Save to ExampleWGName; -endif -} -stop - -:=//My Membership:// ----- -**BillWorkingGroup:** -* Confluence Admins ----- -**Axel Working Group:** -* Confluence Users -* Jira Admins; - -@enduml \ No newline at end of file diff --git a/PlantUML/MyMemberProcess.puml b/PlantUML/MyMemberProcess.puml deleted file mode 100644 index 211ce24..0000000 --- a/PlantUML/MyMemberProcess.puml +++ /dev/null @@ -1,219 +0,0 @@ -# Data from API call to get all Groups belong to -@startjson -#highlight "WsGetGroupsLiteResult" / "wsGroups" -#highlight "WsGetGroupsLiteResult" / "wsGroups" / "0" / "name" -#highlight "WsGetGroupsLiteResult" / "wsGroups" / "0" / "displayName" -#highlight "WsGetGroupsLiteResult" / "wsGroups" / "0" / "description" -#highlight "WsGetGroupsLiteResult" / "wsGroups" / "1" / "name" -#highlight "WsGetGroupsLiteResult" / "wsGroups" / "1" / "displayName" -#highlight "WsGetGroupsLiteResult" / "wsGroups" / "1" / "description" -#highlight "WsGetGroupsLiteResult" / "wsGroups" / "2" / "name" -#highlight "WsGetGroupsLiteResult" / "wsGroups" / "2" / "displayName" -#highlight "WsGetGroupsLiteResult" / "wsGroups" / "2" / "description" -#highlight "WsGetGroupsLiteResult" / "wsGroups" / "3" / "name" -#highlight "WsGetGroupsLiteResult" / "wsGroups" / "3" / "displayName" -#highlight "WsGetGroupsLiteResult" / "wsGroups" / "3" / "description" -{ - "WsGetGroupsLiteResult": { - "resultMetadata": { - "success": "T", - "resultCode": "SUCCESS" - }, - "wsSubject": { - "...": "..." - }, - "responseMetadata": { - "...": "..." - }, - "wsGroups": [ - { - "extension": "CO_members_active", - "displayName": "app:comanage-provision:CO_members_active", - "description": "Internet2 Collaborations Active Members", - "uuid": "f5dae468b9d9429993992781712c2f83", - "enabled": "T", - "displayExtension": "CO_members_active", - "name": "app:comanage-provision:CO_members_active", - "typeOfGroup": "group", - "idIndex": "11104" - }, - { - "extension": "grouperUiUserData", - "displayName": "etc:grouperUi:grouperUiUserData", - "description": "Internal group for grouper which has ...", - "uuid": "2748e23e51174145a4dc4d9e115c59da", - "enabled": "T", - "displayExtension": "grouperUiUserData", - "name": "etc:grouperUi:grouperUiUserData", - "typeOfGroup": "group", - "idIndex": "10015" - }, - { - "extension": "AdministeredByUniconFolks", - "displayName": "sandbox:UniconTest:Administered By Unicon Folks", - "description": "Description goes here", - "uuid": "0b26aa411a99405b9440be3d0b18dafa", - "enabled": "T", - "displayExtension": "Administered By Unicon Folks", - "name": "sandbox:UniconTest:AdministeredByUniconFolks", - "typeOfGroup": "group", - "idIndex": "18451" - }, - { - "extension": "MembersFromUniconAZ", - "displayName": "sandbox:UniconTest:Members At Unicon in AZ", - "uuid": "7a9f3b9837024a56b12a2a6259d520e0", - "enabled": "T", - "displayExtension": "Members At Unicon in AZ", - "name": "sandbox:UniconTest:MembersFromUniconAZ", - "typeOfGroup": "group", - "idIndex": "18447" - } - ] - } -} -@endjson - -============================================================= - -@startuml -skinparam titleBorderRoundCorner 15 -skinparam titleBorderThickness 2 -skinparam titleBorderColor red -skinparam titleBackgroundColor Aqua-CadetBlue - -title Mapping of //My Membership// page to API results - -map "** API Grouper (with example data) => My Membership Page **" as CC { - extension = CO_members_active => NA - displayName = app:comanage-provision:CO_members_active => Name - description = Internet2 Collaborations Active Members => Description - uuid = f5dae468b9d9429993992781712c2f83 => NA - enabled = T => NA - displayExtension = CO_members_active => NA - name = app:comanage-provision:CO_members_active => Name - typeOfGroup = group => NA - idIndex = 11104 => NA -} - -note right of CC - Name Fields are used to determine Friendly Name -end note - -@enduml - -============================================================= - -@startuml -skinparam titleBorderRoundCorner 15 -skinparam titleBorderThickness 2 -skinparam titleBorderColor red -skinparam titleBackgroundColor Aqua-CadetBlue - -title Current Friendly name process for\nNon Working Groups - -start -:GET "Groups Member Of"; -:Group 1 -{ - "extension": "UniconMemberGroup", - "displayName": "sandbox:UniconTest:Unicon Members Group", - "uuid": "35c1ae4d9529492aac8cb2acb970279b", - "enabled": "T", - "displayExtension": "Unicon Members Group", - "name": "sandbox:UniconTest:UniconMemberGroup", - "typeOfGroup": "group", - "idIndex": "18446" -}; -partition "Compare Displayname and Name Params for Group 1" { -:Data: -**displayName** = "sandbox:UniconTest:Unicon Members Group" -**name** = "sandbox:UniconTest:UniconMemberGroup"; -:**Logic: Compare each Stem section**; - -if (Does "sandbox" == "sandbox"?) then (yes) - if (Does "UniconTest" == "UniconTest"?) then (yes) - if (Does "Unicon Members Group" == "UniconMemberGroup"?) then (yes) - :**FriendlyName** = "sandbox:UniconTest:UniconMemberGroup" - (No variance between **name** and **displayName**); - else (no) - :**FriendlyName** = "Unicon Members Group"; - endif - else (no) - #red:**FriendlyName** = "Unicon Members Group"; - note right: Not possible - endif -else (no) - #red:**FriendlyName** = "UniconTest:Unicon Members Group"; - note: Not possible -endif -stop -} -@enduml - -============================================================= - -# Current Friendly name process for Working Groups that is broken -@startuml -skinparam titleBorderRoundCorner 15 -skinparam titleBorderThickness 2 -skinparam titleBorderColor red -skinparam titleBackgroundColor Aqua-CadetBlue - -title Current Friendly name process for\na Working Groups that is broken - -start -:GET "Groups Member Of"; -fork -:Group 1 -{ - "extension": "admins", - "displayName": "app:confluence:Axel Working Group:admins", - "description": "Admins of confluence space for working group. Axel's working group for testing", - "uuid": "35eaa34fd2d443e5a66a0a355505f69e", - "enabled": "T", - "displayExtension": "admins", - "name": "app:confluence:AxelWorkingGroup:admins", - "typeOfGroup": "group", - "idIndex": "19010" -}; -fork again -:Group 2 -{ - "extension": "users", - "displayName": "app:confluence:Axel Working Group:users", - "description": "Users of confluence space for working group. Axel's working group for testing", - "uuid": "163dc892fa8e484a9262e6e9fa619791", - "enabled": "T", - "displayExtension": "users", - "name": "app:confluence:AxelWorkingGroup:users", - "typeOfGroup": "group", - "idIndex": "19011" -}; -end fork -partition "Compare Displayname and Name Params from Group 2" { -:Group 2 Data: -**displayName** = "app:confluence:Axel Working Group:users" -**name** = "app:confluence:AxelWorkingGroup:users"; -:**Logic: Compare each Stem section**; - -if (Does "app" == "app"?) then (yes) - if (Does "confluence" == "confluence"?) then (yes) - if (Does "Axel Working Group" == "AxelWorkingGroup"?) then (yes) - :**FriendlyName** = "app:confluence:AxelWorkingGroup:users" - (No variance between **name** and **displayName**); - else (no) - #red:**FriendlyName** = "Axel Working Group"; - note right: Missing **:users** - endif - else (no) - #red:**FriendlyName** = "Axel Working Group:users"; - note right: Not possible - endif -else (no) - #red:**FriendlyName** = "confluence:Axel Working Group:users"; - note: Not possible -endif -stop -} -@enduml diff --git a/README.md b/README.md index 80d5219..11242ca 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,14 @@ -# coManage Grouper +# COmanage Grouper Lite Widget Plugin ## Instructions: -### To use in coMananage: -* With your favorite command line tool, navigate to a directory you wish to use to house the coManage plugin. -* Copy the source files from the GrouperLite Plugin located at [https://github.internet2.edu/internet2/comanage-grouper-widget](https://github.internet2.edu/internet2/comanage-grouper-widget) +### To use in COManage: +* With your favorite command line tool, navigate to a directory you wish to use to house the COmanage plugin. +* Copy the source files from the GrouperLiteWidget Plugin located at [https://github.internet2.edu/internet2/comanage-grouper-widget](https://github.internet2.edu/internet2/comanage-grouper-widget) * You can either download the source files as a ZipFile or Clone the directory from GitHub repository. - * ZipFile: unzip into a new ```GrouperLite/``` directory. - * Clone: ```git clone https://github.internet2.edu/internet2/comanage-grouper-widget GrouperLite/``` + * ZipFile: unzip into a new `GrouperLiteWidget/` directory. + * Clone: `git clone https://github.internet2.edu/internet2/comanage-grouper-widget GrouperLiteWidget/` * Copy the application location for symlinking below. -* Navigate to your coManage source directory. -* From the root of your coManage installation, go to the ```local/Plugin/``` directory. -* Symlink your GrouperLite location under the Plugin directory. - * ```ln -s /GrouperLite /local/Plugin/GrouperLite``` - - +* Navigate to your COmanage source directory. +* From the root of your COmanage installation, go to the `local/Plugin/` directory. +* Symlink your GrouperLiteWidget location under the Plugin directory. + * `ln -s /GrouperLiteWidget /local/Plugin/GrouperLiteWidget` diff --git a/Test/HttpRequests/grouper.http b/Test/HttpRequests/grouper.http new file mode 100644 index 0000000..4fb3c28 --- /dev/null +++ b/Test/HttpRequests/grouper.http @@ -0,0 +1,16 @@ +# https://spaces.at.internet2.edu/display/Grouper/Grouper+Web+Services + +### GET groups the actAs is a member of +GET https://{{ host }}/grouper-ws/servicesRest/{{version}}/subjects/{{identifier}}/groups? + wsLiteObjectType=WsRestGetGroupsLiteRequest& + actAsSubjectId={{identifier}} +Authorization: Basic {{username}} {{password}} + + +### isMember of Group +GET https://{{ host }}/grouper-ws/servicesRest/{{version}}/groups/test%3AviewOnly/members/{{identifier}} +Authorization: Basic {{username}} {{password}} + +### Get memberships +GET https://{{ host }}/grouper-ws/servicesRest/{{version}}/subjects/{{identifier}}/groups/test%3AviewOnly/memberships +Authorization: Basic {{username}} {{password}} \ No newline at end of file diff --git a/Test/HttpRequests/http-client.env.json b/Test/HttpRequests/http-client.env.json new file mode 100644 index 0000000..9313194 --- /dev/null +++ b/Test/HttpRequests/http-client.env.json @@ -0,0 +1,6 @@ +{ + "grouper": { + "host": "grouper.dev.at.internet2.edu", + "version": "v2_5_000" + } +} \ No newline at end of file diff --git a/View/CoGrouperLites/display.ctp b/View/CoGrouperLiteWidgets/display.ctp similarity index 50% rename from View/CoGrouperLites/display.ctp rename to View/CoGrouperLiteWidgets/display.ctp index 480ee25..0cfb893 100644 --- a/View/CoGrouperLites/display.ctp +++ b/View/CoGrouperLiteWidgets/display.ctp @@ -29,67 +29,74 @@ * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ -// Figure out the widget ID so we can overwrite the dashboard's widget div - -$divid = $vv_config['CoGrouperLite']['co_dashboard_widget_id']; - -// $coid = $config['co']; -// $glid = $config['glid']; - $this->extend('/GrouperGroups/base'); -echo $this->element('GrouperLite.base-styles'); +// Figure out the widget ID so we can overwrite the dashboard's widget div +$divid = $vv_config['CoGrouperLiteWidget']['co_dashboard_widget_id']; +$pl = Inflector::underscore(filter_var($vv_codw['CoDashboardWidget']['plugin'], FILTER_SANITIZE_SPECIAL_CHARS)); $idsuffix = rand(); ?> -
+
-
+
- Html->image('GrouperLite.grouper-logo.png', array('class' => 'logo-fluid')); ?> + Html->image('GrouperLiteWidget.grouper-logo.png', array('class' => 'logo-fluid')); ?>
-

 

+

 

diff --git a/View/CoGrouperLites/edit.ctp b/View/CoGrouperLiteWidgets/edit.ctp similarity index 100% rename from View/CoGrouperLites/edit.ctp rename to View/CoGrouperLiteWidgets/edit.ctp diff --git a/View/CoGrouperLites/fields.inc b/View/CoGrouperLiteWidgets/fields.inc similarity index 64% rename from View/CoGrouperLites/fields.inc rename to View/CoGrouperLiteWidgets/fields.inc index 4927142..1d96d86 100644 --- a/View/CoGrouperLites/fields.inc +++ b/View/CoGrouperLiteWidgets/fields.inc @@ -52,23 +52,54 @@ $args = array(); $args['plugin'] = null; $args['controller'] = 'co_dashboards'; $args['action'] = 'edit'; -$args[] = $co_grouper_lites[0]['CoDashboardWidget']['co_dashboard_id']; +$args[] = $co_grouper_lite_widgets[0]['CoDashboardWidget']['co_dashboard_id']; $this->Html->addCrumb(_txt('ct.co_dashboards.1'), $args); $args = array(); $args['plugin'] = null; $args['controller'] = 'co_dashboard_widgets'; $args['action'] = 'index'; -$args['codashboard'] = $co_grouper_lites[0]['CoDashboardWidget']['co_dashboard_id']; +$args['codashboard'] = $co_grouper_lite_widgets[0]['CoDashboardWidget']['co_dashboard_id']; $this->Html->addCrumb(_txt('ct.co_dashboard_widgets.1'), $args); -$crumbTxt = _txt('op.' . $this->action . '-a', array(filter_var($co_grouper_lites[0]['CoDashboardWidget']['description'], FILTER_SANITIZE_SPECIAL_CHARS))); +$crumbTxt = _txt('op.' . $this->action . '-a', array(filter_var($co_grouper_lite_widgets[0]['CoDashboardWidget']['description'], FILTER_SANITIZE_SPECIAL_CHARS))); $this->Html->addCrumb($crumbTxt); -print $this->Form->hidden('co_dashboard_widget_id', array('default' => $vv_dwid)) . "\n"; +if(isset($vv_dwid)) { + print $this->Form->hidden('co_dashboard_widget_id', array('default' => $vv_dwid)) . "\n"; +} ?>
    +
  • +
    +
    + Form->label('identifier_type', _txt('pl.grouperlite.config.identifier')) : _txt('pl.grouperlite.config.identifier')); ?> + * +
    +
    + +
    +
    +
    + Form->select('identifier_type', + $vv_available_types, + $attrs); + + if($this->Form->isFieldError('identifier_type')) { + print $this->Form->error('identifier_type'); + } + } elseif (!empty($vv_config['CoGrouperLiteWidget']['identifier_type'])) { + print filter_var($vv_available_types[ $vv_config['CoGrouperLiteWidget']['identifier_type'] ],FILTER_SANITIZE_SPECIAL_CHARS); + } + ?> +
    +
  • @@ -82,7 +113,7 @@ print $this->Form->hidden('co_dashboard_widget_id', array('default' => $vv_dwid)
    Form->hidden('co_dashboard_widget_id', array('default' => $vv_dwid) if ($this->Form->isFieldError('grouper_url')) { print $this->Form->error('grouper_url'); } - } else { - if (!empty($co_grouper_lites[0]['CoGrouperLite']['grouper_url'])) { - print filter_var($co_grouper_lites[0]['CoGrouperLite']['grouper_url'], FILTER_SANITIZE_SPECIAL_CHARS); - } + } elseif (!empty($vv_config['CoGrouperLiteWidget']['grouper_url'])) { + print filter_var($vv_config['CoGrouperLiteWidget']['grouper_url'], FILTER_SANITIZE_SPECIAL_CHARS); } ?>
    @@ -110,7 +139,7 @@ print $this->Form->hidden('co_dashboard_widget_id', array('default' => $vv_dwid)
    Form->hidden('co_dashboard_widget_id', array('default' => $vv_dwid) if ($this->Form->isFieldError('conn_url')) { print $this->Form->error('conn_url'); } - } else { - if (!empty($co_grouper_lites[0]['CoGrouperLite']['conn_url'])) { - print filter_var($co_grouper_lites[0]['CoGrouperLite']['conn_url'], FILTER_SANITIZE_SPECIAL_CHARS); - } + } elseif (!empty($vv_config['CoGrouperLiteWidget']['conn_url'])) { + print filter_var($vv_config['CoGrouperLiteWidget']['conn_url'], FILTER_SANITIZE_SPECIAL_CHARS); } ?>
    @@ -131,14 +158,14 @@ print $this->Form->hidden('co_dashboard_widget_id', array('default' => $vv_dwid)
  • - Form->label('conn_ver', _txt('pl.grouperlite.config.grouper-ws-version')) : _txt('pl.grouperlite.config.grouper-version')); ?> + Form->label('conn_ver', _txt('pl.grouperlite.config.grouper-ws-version')) : _txt('pl.grouperlite.config.grouper-ws-version')); ?> *
    Form->hidden('co_dashboard_widget_id', array('default' => $vv_dwid) if ($this->Form->isFieldError('conn_ver')) { print $this->Form->error('conn_ver'); } - } else { - if (!empty($co_grouper_lites[0]['CoGrouperLite']['conn_ver'])) { - print filter_var($co_grouper_lites[0]['CoGrouperLite']['conn_ver'], FILTER_SANITIZE_SPECIAL_CHARS); - } + } elseif (!empty($vv_config['CoGrouperLiteWidget']['conn_ver'])) { + print filter_var($vv_config['CoGrouperLiteWidget']['conn_ver'], FILTER_SANITIZE_SPECIAL_CHARS); } ?>
    @@ -159,14 +184,14 @@ print $this->Form->hidden('co_dashboard_widget_id', array('default' => $vv_dwid)
  • - Form->label('conn_user', _txt('pl.grouperlite.config.grouper-ws-un')) : _txt('pl.grouperlite.config.grouper-un')); ?> + Form->label('conn_user', _txt('pl.grouperlite.config.grouper-ws-un')) : _txt('pl.grouperlite.config.grouper-ws-un')); ?> *
    Form->hidden('co_dashboard_widget_id', array('default' => $vv_dwid) if ($this->Form->isFieldError('conn_user')) { print $this->Form->error('conn_user'); } - } else { - if (!empty($co_grouper_lites[0]['CoGrouperLite']['conn_user'])) { - print filter_var($co_grouper_lites[0]['CoGrouperLite']['conn_user'], FILTER_SANITIZE_SPECIAL_CHARS); - } + } elseif (!empty($vv_config['CoGrouperLiteWidget']['conn_user'])) { + print filter_var($vv_config['CoGrouperLiteWidget']['conn_user'], FILTER_SANITIZE_SPECIAL_CHARS); } ?>
    @@ -187,14 +210,14 @@ print $this->Form->hidden('co_dashboard_widget_id', array('default' => $vv_dwid)
  • - Form->label('conn_pass', _txt('pl.grouperlite.config.grouper-ws-pw')) : _txt('pl.grouperlite.config.grouper-pw')); ?> + Form->label('conn_pass', _txt('pl.grouperlite.config.grouper-ws-pw')) : _txt('pl.grouperlite.config.grouper-ws-pw')); ?> *
    Form->hidden('co_dashboard_widget_id', array('default' => $vv_dwid) if ($this->Form->isFieldError('conn_pass')) { print $this->Form->error('conn_pass'); } - } else { - if (!empty($co_grouper_lites[0]['CoGrouperLite']['conn_pass'])) { - print filter_var($co_grouper_lites[0]['CoGrouperLite']['conn_pass'], FILTER_SANITIZE_SPECIAL_CHARS); - } + } elseif (!empty($vv_config['CoGrouperLiteWidget']['conn_pass'])) { + // XXX We do not print the password + print ''; } ?>
  • -
  • @@ -222,7 +243,7 @@ print $this->Form->hidden('co_dashboard_widget_id', array('default' => $vv_dwid)
    Form->hidden('co_dashboard_widget_id', array('default' => $vv_dwid) if ($this->Form->isFieldError('adhoc_heading')) { print $this->Form->error('adhoc_heading'); } - } else { - if (!empty($co_grouper_lites[0]['CoGrouperLite']['adhoc_heading'])) { - print filter_var($co_grouper_lites[0]['CoGrouperLite']['adhoc_heading'], FILTER_SANITIZE_SPECIAL_CHARS); - } + } elseif (!empty($vv_config['CoGrouperLiteWidget']['adhoc_heading'])) { + print filter_var($vv_config['CoGrouperLiteWidget']['adhoc_heading'], FILTER_SANITIZE_SPECIAL_CHARS); } ?>
  • -
  • @@ -250,7 +268,7 @@ print $this->Form->hidden('co_dashboard_widget_id', array('default' => $vv_dwid)
    Form->hidden('co_dashboard_widget_id', array('default' => $vv_dwid) if ($this->Form->isFieldError('wg_heading')) { print $this->Form->error('wg_heading'); } + } elseif (!empty($vv_config['CoGrouperLiteWidget']['wg_heading'])) { + print filter_var($vv_config['CoGrouperLiteWidget']['wg_heading'], FILTER_SANITIZE_SPECIAL_CHARS); + } + ?> +
    +
  • +
  • +
    +
    + Form->label('act_as_grp_name', _txt('pl.grouperlite.config.act-as-grp-name')) : _txt( + 'pl.grouperlite.config.act-as-grp-name' + )) ?> +
    +
    + +
    +
    +
    + Form->input('act_as_grp_name', $attrs); + + if ($this->Form->isFieldError('act_as_grp_name')) { + print $this->Form->error('act_as_grp_name'); + } } else { - if (!empty($co_grouper_lites[0]['CoGrouperLite']['wg_heading'])) { - print filter_var($co_grouper_lites[0]['CoGrouperLite']['wg_heading'], FILTER_SANITIZE_SPECIAL_CHARS); + if (!empty($vv_config['CoGrouperLiteWidget']['act_as_grp_name'])) { + print filter_var($vv_config['CoGrouperLiteWidget']['act_as_grp_name'], FILTER_SANITIZE_SPECIAL_CHARS); } } ?>
  • -
  • -
    Form->label('default_collapse', _txt('pl.grouperlite.config.default-collapse')) : _txt('pl.grouperlite.config.default-collapse')); ?> @@ -278,13 +324,12 @@ print $this->Form->hidden('co_dashboard_widget_id', array('default' => $vv_dwid)
    Form->radio('default_collapse', array( 'collapsed' => 'True', 'expanded' => 'False' ), array( - 'value' => $co_grouper_lites[0]['CoGrouperLite']['default_collapse'], + 'value' => $vv_config['CoGrouperLiteWidget']['default_collapse'], 'legend' => false, 'separator' => '  ' )); @@ -293,8 +338,8 @@ print $this->Form->hidden('co_dashboard_widget_id', array('default' => $vv_dwid) print $this->Form->error('default_collapse'); } } else { - if (!empty($co_grouper_lites[0]['CoGrouperLite']['default_collapse'])) { - print filter_var($co_grouper_lites[0]['CoGrouperLite']['default_collapse'], FILTER_SANITIZE_SPECIAL_CHARS); + if (!empty($vv_config['CoGrouperLiteWidget']['default_collapse'])) { + print filter_var($vv_config['CoGrouperLiteWidget']['default_collapse'], FILTER_SANITIZE_SPECIAL_CHARS); } } ?> @@ -312,8 +357,3 @@ print $this->Form->hidden('co_dashboard_widget_id', array('default' => $vv_dwid)
-
-
-

-
-
\ No newline at end of file diff --git a/View/CoGrouperLiteWidgets/view.ctp b/View/CoGrouperLiteWidgets/view.ctp new file mode 100644 index 0000000..5ccb31d --- /dev/null +++ b/View/CoGrouperLiteWidgets/view.ctp @@ -0,0 +1,118 @@ +name; + $req = Inflector::singularize($model); + $modelpl = Inflector::tableize($req); + $modelu = Inflector::underscore($req); + + // Get a pointer to our data + $d = $$modelpl; + + // Add page title + $params = array(); + $params['title'] = $title_for_layout; + + // Add top links + $params['topLinks'] = array(); + + // If user has edit permission, offer an edit button in the sidebar + if(!empty($permissions['edit']) && $permissions['edit']) { + + // special case co_people + $editAction = 'edit'; + if ($modelpl == 'co_people') { + $editAction = 'canvas'; + } + + $a = array('controller' => $modelpl, 'action' => $editAction, $d[0][$req]['id']); + + if(empty($d[0]['OrgIdentity']['OrgIdentitySourceRecord']['id']) + && empty($d[0][$req]['source_'.$modelu.'_id'])) { + // Add edit button to the top links, except for attributes attached to + // an Org Identity that came from an Org Identity Source. + $params['topLinks'][] = $this->Html->link( + _txt('op.edit'), + $a, + array('class' => 'editbutton') + ); + } + } + + // Add locally configured page buttons + if(!empty($this->plugin)) { + if(file_exists(APP . "Plugin/" . $this->plugin . "/View/" . $model . "/buttons.inc")) { + include(APP . "Plugin/" . $this->plugin . "/View/" . $model . "/buttons.inc"); + } elseif(file_exists(LOCAL . "Plugin/" . $this->plugin . "/View/" . $model . "/buttons.inc")) { + include(LOCAL . "Plugin/" . $this->plugin . "/View/" . $model . "/buttons.inc"); + } + } else { + if(file_exists(APP . "View/" . $model . "/buttons.inc")) { + include(APP . "View/" . $model . "/buttons.inc"); + } + } + + print $this->element("pageTitleAndButtons", $params); + if(file_exists(APP . "View/" . $model . "/tabs.inc")) { + include(APP . "View/" . $model . "/tabs.inc"); + } +?> + +
+ info +
+ +
+
+
+ +
+ info +
+ +
+
+
+ +'; + if(!empty($this->plugin)) { + if(file_exists(APP . "Plugin/" . $this->plugin . "/View/" . $model . "/fields.inc")) { + include(APP . "Plugin/" . $this->plugin . "/View/" . $model . "/fields.inc"); + } elseif(file_exists(LOCAL . "Plugin/" . $this->plugin . "/View/" . $model . "/fields.inc")) { + include(LOCAL . "Plugin/" . $this->plugin . "/View/" . $model . "/fields.inc"); + } + } else { + include(APP . "View/" . $model . "/fields.inc"); + } + print '
'; + +?> diff --git a/View/Elements/ActAsPeopleAutocomplete.ctp b/View/Elements/ActAsPeopleAutocomplete.ctp new file mode 100644 index 0000000..ed73ecb --- /dev/null +++ b/View/Elements/ActAsPeopleAutocomplete.ctp @@ -0,0 +1,183 @@ + 0 ? '?time=' . time() : ''; + + +?> + + + + +
diff --git a/View/Elements/ActionItem.ctp b/View/Elements/ActionItem.ctp new file mode 100644 index 0000000..73d45f9 --- /dev/null +++ b/View/Elements/ActionItem.ctp @@ -0,0 +1,186 @@ + 0 ? '?time=' . time() : ''; + +?> + + + + + diff --git a/View/Elements/ActionSideBar.ctp b/View/Elements/ActionSideBar.ctp new file mode 100644 index 0000000..588fe63 --- /dev/null +++ b/View/Elements/ActionSideBar.ctp @@ -0,0 +1,42 @@ + + + + + diff --git a/View/Elements/Components/groupattributes.ctp b/View/Elements/Components/groupattributes.ctp deleted file mode 100644 index 91f246f..0000000 --- a/View/Elements/Components/groupattributes.ctp +++ /dev/null @@ -1,29 +0,0 @@ - 0) : ?> - - - - - - - - - - - - - - - - - -
- - - -
- - -
-

-
- \ No newline at end of file diff --git a/View/Elements/Components/groupproperties.ctp b/View/Elements/Components/groupproperties.ctp deleted file mode 100644 index c21099a..0000000 --- a/View/Elements/Components/groupproperties.ctp +++ /dev/null @@ -1,42 +0,0 @@ -
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
\ No newline at end of file diff --git a/View/Elements/Components/navigation-emaillists.ctp b/View/Elements/Components/navigation-emaillists.ctp deleted file mode 100644 index 9a1b34a..0000000 --- a/View/Elements/Components/navigation-emaillists.ctp +++ /dev/null @@ -1,68 +0,0 @@ - \ No newline at end of file diff --git a/View/Elements/Components/navigation-groups.ctp b/View/Elements/Components/navigation-groups.ctp deleted file mode 100644 index f437f4c..0000000 --- a/View/Elements/Components/navigation-groups.ctp +++ /dev/null @@ -1,86 +0,0 @@ -
- */ ?> - \ No newline at end of file diff --git a/View/Elements/Components/optAction.ctp b/View/Elements/Components/optAction.ctp deleted file mode 100644 index aeb0ea0..0000000 --- a/View/Elements/Components/optAction.ctp +++ /dev/null @@ -1,12 +0,0 @@ -Form->create(false, array( - 'url' => array('controller' => 'grouper_groups', 'action' => $action), - 'class' => 'd-flex justify-content-center', - 'id' => 'join-group.' . $idx -)); ?> -Form->hidden('GroupName', array('default' => $group, 'id' => 'groupName.' . $idx)); ?> -Form->hidden('GroupDisplayName', array('default' => $groupDisplay, 'id' => 'groupDisplayName.' . $idx)); ?> - -Form->end(); ?> \ No newline at end of file diff --git a/View/Elements/Components/search.ctp b/View/Elements/Components/search.ctp deleted file mode 100644 index ab982b6..0000000 --- a/View/Elements/Components/search.ctp +++ /dev/null @@ -1,38 +0,0 @@ -Form->create(false, array( - 'url' => array('controller' => 'grouper_groups', 'action' => $active), - 'class' => 'search mb-4' -)); ?> -
-
- - -
- -
-Form->end(); ?> \ No newline at end of file diff --git a/View/Elements/Components/subscriberList.ctp b/View/Elements/Components/subscriberList.ctp deleted file mode 100644 index 100673e..0000000 --- a/View/Elements/Components/subscriberList.ctp +++ /dev/null @@ -1,305 +0,0 @@ -
- - - - -
\ No newline at end of file diff --git a/View/Elements/Components/vue-table.ctp b/View/Elements/Components/vue-table.ctp deleted file mode 100644 index af8e203..0000000 --- a/View/Elements/Components/vue-table.ctp +++ /dev/null @@ -1,135 +0,0 @@ - - - - -
- element("pagination", array( - 'goto' => false, - 'limit' => false, - 'numbers' => false, - 'counter' => true, - 'class' => 'counter' - )); ?> -
- - - -
- - element("pagination", array( - 'goto' => false, - 'limit' => true, - 'numbers' => true, - 'counter' => false - )); ?> -
diff --git a/View/Elements/base-styles.ctp b/View/Elements/base-styles.ctp deleted file mode 100644 index 48ea5e2..0000000 --- a/View/Elements/base-styles.ctp +++ /dev/null @@ -1,140 +0,0 @@ - \ No newline at end of file diff --git a/View/Elements/empty b/View/Elements/empty deleted file mode 100644 index e69de29..0000000 diff --git a/View/Elements/pagination.ctp b/View/Elements/pagination.ctp deleted file mode 100644 index a182dbd..0000000 --- a/View/Elements/pagination.ctp +++ /dev/null @@ -1,141 +0,0 @@ - - - - -
- -
- Paginator->counter(array( - 'format' => _txt(is_string($counter) ? $counter : 'pl.grouperlite.pagination.counter') - )); - ?> -
- - - Paginator->hasPage(2)) : ?> -
- - -
- Paginator->numbers(array( - 'separator' => '', - 'class' => 'pagination-numbers-item' - )); - ?> -
- - -
- - - Paginator->hasPage(2)) : ?> - -
- - - -
- - - - - -
- - -

- - -
- -
\ No newline at end of file diff --git a/View/GrouperGroups/base.ctp b/View/GrouperGroups/base.ctp index d1b2780..85df70d 100644 --- a/View/GrouperGroups/base.ctp +++ b/View/GrouperGroups/base.ctp @@ -1,33 +1,41 @@ Html->meta( - array('http-equiv' => 'Cache-Control', 'content' => 'no-cache, no-store, must-revalidate'), - null, - array('inline' => false) +print $this->Html->meta( + ['http-equiv' => 'Cache-Control', 'content' => 'no-cache, no-store, must-revalidate'], + null, + ['inline' => false] ); -echo $this->Html->meta( - array('http-equiv' => 'Pragma', 'content' => 'no-cache'), - null, - array('inline' => false) +print $this->Html->meta( + ['http-equiv' => 'Pragma', 'content' => 'no-cache'], + null, + ['inline' => false] ); -echo $this->Html->meta( - array('http-equiv' => 'Expires', 'content' => '0'), - null, - array('inline' => false) +print $this->Html->meta( + ['http-equiv' => 'Expires', 'content' => '0'], + null, + ['inline' => false] ); -print $this->element('GrouperLite.base-styles'); -print $this->Html->css('GrouperLite.co-grouper-plugin') . "\n "; +print $this->Html->script('GrouperLiteWidget.autocomplete.grouperplugin') . PHP_EOL; +print $this->Html->css('GrouperLiteWidget.co-grouper-base') . PHP_EOL; +print $this->Html->css('GrouperLiteWidget.co-grouper-plugin') . PHP_EOL; - -?> - -Html->addCrumb(_txt('pl.grouperlite.crumb.root'), array('controller' => 'grouper_groups', 'action' => 'groupmember'), array('prepend' => true)); +if(isset($this->viewVars['vv_coid'])) { + $this->Html->addCrumb(_txt('pl.grouperlite.crumb.root'), + [ + 'plugin' => Inflector::underscore(filter_var($this->plugin, FILTER_SANITIZE_SPECIAL_CHARS)), + 'controller' => 'grouper_groups', + 'action' => 'groupmember', + 'co' => $this->viewVars['vv_coid'], + 'glid' => $this->viewVars['vv_config']['CoGrouperLiteWidget']['id'] + ], + ['prepend' => true] + ); +} ?>
- fetch('content'); ?> + fetch('content') ?>
\ No newline at end of file diff --git a/View/GrouperGroups/groupinfo.ctp b/View/GrouperGroups/groupinfo.ctp deleted file mode 100644 index 9e74792..0000000 --- a/View/GrouperGroups/groupinfo.ctp +++ /dev/null @@ -1,46 +0,0 @@ - -extend('/GrouperGroups/base'); ?> -Html->addCrumb(_txt('pl.grouperlite.title.groupinfo')); ?> - - -
-
-

-
- -
-
-
-
-
-

- - - -
- element('Components/groupproperties', array( - 'group' => $groupergroupsdetail - )); ?> -
-
-
-

- element('Components/groupattributes', array( - 'attributes' => $groupergroupsdetail['attributes'], - 'baseUrl' => $attrUrlBase - )); ?> -
-
-
-
- \ No newline at end of file diff --git a/View/GrouperGroups/groupmember.ctp b/View/GrouperGroups/groupmember.ctp deleted file mode 100644 index 7bbee89..0000000 --- a/View/GrouperGroups/groupmember.ctp +++ /dev/null @@ -1,30 +0,0 @@ - -extend('/GrouperGroups/base'); ?> -Html->addCrumb(_txt('pl.grouperlite.nav.memberships')); ?> - -element('GrouperLite.Components/vue-table', array( - 'groupData' => json_encode(array( - 'adhoc' => $groupmemberships, - 'working' => $wgmemberships - )), - 'treatAsOwner' => $isuserowner === 'T' ? 'true' : 'false', - 'columns' => json_encode(array( - array('value' => 'name', 'label' => _txt('pl.grouperlite.table.name')), - array('value' => 'role', 'label' => _txt('pl.grouperlite.table.role')), - array('value' => 'description', 'label' => _txt('pl.grouperlite.table.description')), - array('value' => 'action', 'label' => _txt('pl.grouperlite.table.action')), - )), - 'optAction' => "leavegroup", - 'actionUrl' => $this->Html->url([ - 'controller' => 'grouper_groups', 'action' => 'leavegroup' - ]), - 'members' => true, - 'addSubscribers' => false, -)); ?> - -
-

-
- - - \ No newline at end of file diff --git a/View/GrouperGroups/groupoptin.ctp b/View/GrouperGroups/groupoptin.ctp deleted file mode 100644 index 713ac04..0000000 --- a/View/GrouperGroups/groupoptin.ctp +++ /dev/null @@ -1,26 +0,0 @@ - -extend('/GrouperGroups/base'); ?> -Html->addCrumb(_txt('pl.grouperlite.nav.groups-can-join')); ?> -element('GrouperLite.Components/navigation-groups', array('active' => 'groupoptin')); ?> -element('GrouperLite.Components/search', array('active' => 'groupoptin')); ?> -element('GrouperLite.Components/vue-table', array( - 'groupData' => json_encode(array( - 'adhoc' => $groupoptins, - 'working' => array() - )), - 'treatAsOwner' => false, - 'columns' => json_encode(array( - array('value' => 'name', 'label' => _txt('pl.grouperlite.table.name')), - array('value' => 'description', 'label' => _txt('pl.grouperlite.table.description')), - array('value' => 'action', 'label' => _txt('pl.grouperlite.table.action')), - )), - 'optAction' => "joingroup", - 'actionUrl' => $this->Html->url([ - 'controller' => 'grouper_groups', 'action' => 'joingroup' - ]), -)); ?> - -
-

-
- \ No newline at end of file diff --git a/View/GrouperGroups/groupowner.ctp b/View/GrouperGroups/groupowner.ctp deleted file mode 100644 index 8abe4f1..0000000 --- a/View/GrouperGroups/groupowner.ctp +++ /dev/null @@ -1,37 +0,0 @@ - -extend('/GrouperGroups/base'); ?> -Html->addCrumb(_txt('pl.grouperlite.nav.groups-presided')); ?> -element('Components/navigation-groups', array('active' => 'groupowner')); ?> -element('Components/search', array('active' => 'groupowner')); ?> - 'name', 'label' => _txt('pl.grouperlite.table.name')), - array('value' => 'role', 'label' => _txt('pl.grouperlite.table.role')), - array('value' => 'description', 'label' => _txt('pl.grouperlite.table.description')), - array('value' => 'status', 'label' => _txt('pl.grouperlite.table.status')), - array('value' => 'action', 'label' => _txt('pl.grouperlite.table.action')), -); - -if ($isuserowner !== 'T') { - array_splice($columns, 1, 1); -} -?> - -element('GrouperLite.Components/vue-table', array( - 'groupData' => json_encode(array( - 'adhoc' => $groupsowners, - 'working' => array() - )), - 'treatAsOwner' => $isuserowner === 'T' ? 'true' : 'false', - 'columns' => json_encode($columns), - 'addSubscribers' => true, - 'members' => true -)); ?> - - - -
-

-
- - \ No newline at end of file diff --git a/View/GrouperGroups/index.ctp b/View/GrouperGroups/index.ctp index 8d28873..16261ea 100644 --- a/View/GrouperGroups/index.ctp +++ b/View/GrouperGroups/index.ctp @@ -1,27 +1,34 @@ -extend('/GrouperGroups/base'); ?> -Html->script('GrouperLite.vue-router.js'); ?> -Html->addCrumb(_txt('pl.grouperlite.nav.memberships')); ?> - +extend('/GrouperGroups/base'); + +// Add javascript +print $this->Html->script('GrouperLiteWidget.vue-router.js'); + +// Add Breadcrumb +$this->Html->addCrumb(_txt('pl.grouperlite.title.dashboard')); + + +$baseUrl = $vv_config['CoGrouperLiteWidget']['grouper_url']; +$path = '/grouper/grouperUi/app/UiV2Main.index'; +$groupOperation = '?operation=UiV2Group.viewGroup&groupId='; +$grouperUrlBase = $baseUrl . $path . $groupOperation; + +$htmlId = 'act-as-picker'; +$suffix = Configure::read('debug') > 0 ? '?time=' . time() : ''; +?>
- Html->image('GrouperLite.Grouper.jpg', array('class' => 'img-fluid mr-2', 'style' => 'height: 50px')); ?> -

+ Html->image('GrouperLiteWidget.Grouper.jpg', ['class' => 'img-fluid mr-2', 'style' => 'height: 50px']) ?> +

- - -
-
+ +
+
+ + + element('ActionSideBar', + compact('vv_config', + 'vv_act_as_people', + 'vv_coid', + 'vv_is_user_owner', + 'htmlId') + ) ?> +
\ No newline at end of file diff --git a/View/GrouperGroups/joingroup.ctp b/View/GrouperGroups/joingroup.ctp deleted file mode 100644 index e4d9672..0000000 --- a/View/GrouperGroups/joingroup.ctp +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/View/GrouperGroups/leavegroup.ctp b/View/GrouperGroups/leavegroup.ctp deleted file mode 100644 index 2a3df51..0000000 --- a/View/GrouperGroups/leavegroup.ctp +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/View/GrouperGroups/templatefields.inc b/View/GrouperGroups/templatefields.inc deleted file mode 100644 index 5b4d338..0000000 --- a/View/GrouperGroups/templatefields.inc +++ /dev/null @@ -1,234 +0,0 @@ -
-
- Form->label(false, _txt('pl.grouperlite.form.template.work-group-extension.label'), array( - 'for' => 'gsh_input_workingGroupExtension', - 'class' => "col-sm-3 col-form-label d-flex align-items-center" - )); ?> -
- Form->input('gsh_input_workingGroupExtension', array( - 'label' => false, - 'class' => 'form-control', - 'required' => true, - 'maxlength' => '62', - 'id' => 'gsh_input_workingGroupExtension' - )); ?> -
-
-
- Form->label(false, _txt('pl.grouperlite.form.template.work-group-disp-extension.label'), array( - 'for' => 'gsh_input_workingGroupDisplayExtension', - 'class' => "col-sm-3 col-form-label d-flex align-items-center" - )); ?> -
- Form->input('gsh_input_workingGroupDisplayExtension', array( - 'label' => false, - 'class' => 'form-control', - 'maxlength' => '62', - 'id' => 'gsh_input_workingGroupDisplayExtension' - )); ?> -
-
-
- Form->label(false, _txt('pl.grouperlite.form.template.work-group-description.label'), array( - 'for' => 'gsh_input_workingGroupDescription', - 'class' => "col-sm-3 col-form-label d-flex align-items-center" - )); ?> -
- Form->input('gsh_input_workingGroupDescription', array( - 'label' => false, - 'class' => 'form-control', - 'maxlength' => '62', - 'id' => 'gsh_input_workingGroupDescription' - )); ?> -
-
-
- -
- Form->input( - 'gsh_input_isSympa', - array( - 'label' => true, - 'class' => 'form-check-input', - 'legend' => false, - 'id' => 'gsh_input_isSympa', - 'before' => '
', - 'separator' => '
', - 'after' => '
', - 'options' => array( - true => _txt('pl.grouperlite.form.template.value.positive'), - false => _txt('pl.grouperlite.form.template.value.negative') - ), - 'type' => 'radio', - 'default' => true - ) - ); ?> -
-
-
- -
- Form->input( - 'gsh_input_sympaDomain', - array( - 'label' => true, - 'class' => 'form-check-input', - 'legend' => false, - 'id' => 'gsh_input_sympaDomain', - 'before' => '
', - 'separator' => '
', - 'after' => '
', - 'options' => array( - 'internet2' => _txt('pl.grouperlite.form.template.value.internet2'), - 'incommon' => _txt('pl.grouperlite.form.template.value.incommon') - ), - 'type' => 'radio' - ) - ); ?> -
-
-
- -
- Form->input( - 'gsh_input_isSympaModerated', - array( - 'label' => true, - 'class' => 'form-check-input', - 'legend' => false, - 'id' => 'gsh_input_isSympaModerated', - 'before' => '
', - 'separator' => '
', - 'after' => '
', - 'options' => array( - true => _txt('pl.grouperlite.form.template.value.positive'), - false => _txt('pl.grouperlite.form.template.value.negative') - ), - 'type' => 'radio', - 'default' => false - ) - ); ?> -
-
-
- -
- Form->input( - 'gsh_input_isOptin', - array( - 'label' => true, - 'class' => 'form-check-input', - 'legend' => false, - 'id' => 'gsh_input_isOptin', - 'before' => '
', - 'separator' => '
', - 'after' => '
', - 'options' => array( - true => _txt('pl.grouperlite.form.template.value.positive'), - false => _txt('pl.grouperlite.form.template.value.negative') - ), - 'type' => 'radio', - 'default' => false - ) - ); ?> -
-
- -
- -
- Form->input( - 'gsh_input_isConfluence', - array( - 'label' => true, - 'class' => 'form-check-input', - 'legend' => false, - 'id' => 'gsh_input_isConfluence', - 'before' => '
', - 'separator' => '
', - 'after' => '
', - 'options' => array( - true => _txt('pl.grouperlite.form.template.value.positive'), - false => _txt('pl.grouperlite.form.template.value.negative') - ), - 'type' => 'radio', - 'default' => false - ) - ); ?> -
-
-
- -
- Form->input( - 'gsh_input_isJira', - array( - 'label' => true, - 'class' => 'form-check-input', - 'legend' => false, - 'id' => 'gsh_input_isJira', - 'before' => '
', - 'separator' => '
', - 'after' => '
', - 'options' => array( - true => _txt('pl.grouperlite.form.template.value.positive'), - false => _txt('pl.grouperlite.form.template.value.negative') - ), - 'type' => 'radio', - 'default' => false - ) - ); ?> -
-
-
-
- Form->button(_txt('pl.grouperlite.form.group.action.save'), array( - 'type' => 'submit', - 'class' => 'btn btn-grouper btn-primary btn-lg btn-raised', - 'id' => 'submit' - )); ?> -
-
-
- \ No newline at end of file diff --git a/View/GrouperGroups/users.json b/View/GrouperGroups/users.json deleted file mode 100644 index 6e08fee..0000000 --- a/View/GrouperGroups/users.json +++ /dev/null @@ -1,344 +0,0 @@ -[ - { - "name": "Galena Munoz", - "email": "ipsum.cursus.vestibulum@urnaconvalliserat.net", - "cou": "Active, Member", - "org": "Eu Corporation" - }, - { - "name": "Doris Santos", - "email": "vitae@non.org", - "cou": "Active, Filesource, Member", - "org": "Aliquam LLC" - }, - { - "name": "Alma English", - "email": "pede.Cum.sociis@telluseu.net", - "cou": "Member, Active, Writer, Filesource", - "org": "Consectetuer Corp." - }, - { - "name": "Halee Finley", - "email": "in.consectetuer.ipsum@parturientmontes.co.uk", - "cou": "Member", - "org": "Felis Ltd" - }, - { - "name": "Austin Hardy", - "email": "iaculis@Donecegestas.co.uk", - "cou": "Writer", - "org": "Nonummy LLP" - }, - { - "name": "Victor Powell", - "email": "nisi@eget.ca", - "cou": "Member, Active", - "org": "Praesent Interdum Ligula PC" - }, - { - "name": "Baxter Estrada", - "email": "in.consequat.enim@vestibulum.co.uk", - "cou": "", - "org": "In Molestie Tortor Corporation" - }, - { - "name": "Carlos Rose", - "email": "elit.elit@non.co.uk", - "cou": "Member, Writer, Filesource, Active", - "org": "Risus Nulla Eget LLP" - }, - { - "name": "Ishmael Patton", - "email": "leo.Morbi@mollisnoncursus.co.uk", - "cou": "Writer, Filesource", - "org": "A Enim Suspendisse Consulting" - }, - { - "name": "Ryder Tanner", - "email": "varius@rutrum.org", - "cou": "Writer, Filesource", - "org": "Ornare Elit Elit Consulting" - }, - { - "name": "Dane Rollins", - "email": "Curabitur.consequat.lectus@Naminterdum.ca", - "cou": "Member", - "org": "Ut Pellentesque Consulting" - }, - { - "name": "Brian Kaufman", - "email": "aliquet.vel@orciluctuset.edu", - "cou": "Filesource, Active", - "org": "Justo Eu Arcu Inc." - }, - { - "name": "Lester Price", - "email": "Integer@idlibero.edu", - "cou": "Writer, Member, Active, Filesource", - "org": "Dapibus Rutrum Justo Incorporated" - }, - { - "name": "Helen Cardenas", - "email": "Curabitur.egestas.nunc@quisdiam.org", - "cou": "Member, Active, Filesource", - "org": "Faucibus Morbi Vehicula Associates" - }, - { - "name": "Hop Holloway", - "email": "non.arcu.Vivamus@nibhvulputate.org", - "cou": "Filesource, Active, Member", - "org": "Nunc Sed Pede Ltd" - }, - { - "name": "Madeson Hendrix", - "email": "viverra@purus.com", - "cou": "", - "org": "Neque Venenatis Consulting" - }, - { - "name": "Amity Navarro", - "email": "tincidunt.Donec@inconsequat.com", - "cou": "Writer", - "org": "Pretium Aliquet Consulting" - }, - { - "name": "Cameron Booth", - "email": "nulla.magna@Quisqueporttitor.net", - "cou": "", - "org": "Fusce Associates" - }, - { - "name": "Sybil Burgess", - "email": "Fusce.feugiat@sem.org", - "cou": "Writer, Member, Filesource, Active", - "org": "Curabitur Corporation" - }, - { - "name": "Otto Cantrell", - "email": "Ut@nonarcu.org", - "cou": "", - "org": "Cursus PC" - }, - { - "name": "Mannix Obrien", - "email": "eu.neque@egestasDuisac.ca", - "cou": "", - "org": "Amet Faucibus Corp." - }, - { - "name": "Marcia Gill", - "email": "molestie.in.tempus@posuere.edu", - "cou": "", - "org": "Et Corp." - }, - { - "name": "Tanek Figueroa", - "email": "diam@arcuSed.org", - "cou": "Active", - "org": "Bibendum Donec Ltd" - }, - { - "name": "Ira Lynn", - "email": "aliquet@consequatpurusMaecenas.edu", - "cou": "Filesource", - "org": "Massa Suspendisse Eleifend Corporation" - }, - { - "name": "Lesley Pittman", - "email": "iaculis.odio@Phasellus.edu", - "cou": "Member, Writer", - "org": "Urna Et Arcu PC" - }, - { - "name": "Len Ryan", - "email": "ut.mi.Duis@sitamet.org", - "cou": "Writer, Filesource, Active", - "org": "Vestibulum Accumsan PC" - }, - { - "name": "Sarah Morton", - "email": "dis.parturient@rhoncusProin.org", - "cou": "Active", - "org": "Amet Consectetuer PC" - }, - { - "name": "Marny Hewitt", - "email": "dapibus@orciquis.ca", - "cou": "Active, Writer", - "org": "Ultricies Consulting" - }, - { - "name": "Abel Lyons", - "email": "Proin.eget.odio@enim.org", - "cou": "Writer", - "org": "Amet Lorem Corporation" - }, - { - "name": "Slade Schneider", - "email": "ante.dictum.cursus@turpisAliquamadipiscing.co.uk", - "cou": "", - "org": "Lacinia Company" - }, - { - "name": "Ashton Arnold", - "email": "velit.justo@velitin.edu", - "cou": "", - "org": "Diam Ltd" - }, - { - "name": "Phelan Goff", - "email": "ullamcorper.viverra.Maecenas@Donecporttitortellus.ca", - "cou": "", - "org": "Faucibus Id Libero Institute" - }, - { - "name": "Graham Underwood", - "email": "Nulla.dignissim.Maecenas@Donecsollicitudin.ca", - "cou": "Filesource, Member", - "org": "Duis Gravida Praesent Ltd" - }, - { - "name": "Alisa Scott", - "email": "ipsum.primis.in@erat.co.uk", - "cou": "Writer, Active", - "org": "Vivamus Non Lorem Inc." - }, - { - "name": "Asher Mccray", - "email": "semper.auctor@nibhAliquamornare.edu", - "cou": "Writer, Member", - "org": "Orci Corporation" - }, - { - "name": "Hanna Salas", - "email": "feugiat@purusactellus.net", - "cou": "", - "org": "Lectus Convallis Industries" - }, - { - "name": "Jeremy Riley", - "email": "Ut.sagittis@necante.ca", - "cou": "Writer", - "org": "Luctus Curabitur LLP" - }, - { - "name": "Violet Trevino", - "email": "vestibulum@nec.edu", - "cou": "", - "org": "Ligula Aenean Gravida Consulting" - }, - { - "name": "Carissa Myers", - "email": "non@rhoncusDonec.co.uk", - "cou": "Member, Filesource", - "org": "Tempor Corporation" - }, - { - "name": "Kadeem Osborn", - "email": "nulla.In@aliquameros.ca", - "cou": "Filesource, Active, Member, Writer", - "org": "Tempus Risus Donec PC" - }, - { - "name": "Kiayada England", - "email": "ultrices.posuere@lectus.net", - "cou": "", - "org": "Orci Institute" - }, - { - "name": "Connor Gardner", - "email": "diam.eu@tortorIntegeraliquam.org", - "cou": "Member, Active, Writer", - "org": "Vulputate Nisi Sem LLP" - }, - { - "name": "Alfonso Casey", - "email": "montes.nascetur.ridiculus@risus.net", - "cou": "Member", - "org": "Sed Eu Nibh Consulting" - }, - { - "name": "Avye Raymond", - "email": "mauris.sapien@tempusrisusDonec.co.uk", - "cou": "Active, Writer, Filesource, Member", - "org": "Amet Ante Incorporated" - }, - { - "name": "Quamar Cross", - "email": "elit.pretium@Sedid.org", - "cou": "", - "org": "Dapibus PC" - }, - { - "name": "Justine Kemp", - "email": "diam@ornare.org", - "cou": "Writer, Filesource", - "org": "Libero At Auctor Associates" - }, - { - "name": "Blair Rush", - "email": "at.velit.Cras@pulvinar.ca", - "cou": "", - "org": "Nulla Tempor Associates" - }, - { - "name": "Jael Travis", - "email": "ut.dolor.dapibus@vestibulumneceuismod.ca", - "cou": "", - "org": "Facilisis Lorem LLC" - }, - { - "name": "Donovan Patel", - "email": "bibendum.ullamcorper@Sedeu.co.uk", - "cou": "Member, Filesource, Writer, Active", - "org": "Ipsum Sodales Incorporated" - }, - { - "name": "Sylvester Brady", - "email": "amet@consequatauctornunc.net", - "cou": "", - "org": "Semper Nam Limited" - }, - { - "name": "Yuri Eaton", - "email": "Nullam.lobortis.quam@Etiamligulatortor.com", - "cou": "", - "org": "Eu Elit Nulla LLP" - }, - { - "name": "Bree Harmon", - "email": "libero.Integer@maurisaliquam.ca", - "cou": "Filesource, Active, Member, Writer", - "org": "Dui In Sodales PC" - }, - { - "name": "Tanek Tucker", - "email": "arcu@Craslorem.org", - "cou": "Active, Member, Filesource", - "org": "Nisl Arcu Company" - }, - { - "name": "Jermaine Stevens", - "email": "luctus@Donec.org", - "cou": "Active, Writer", - "org": "Pellentesque Sed Dictum Corp." - }, - { - "name": "Ira Robinson", - "email": "malesuada.vel@lobortisClass.ca", - "cou": "Active, Member", - "org": "Ac Sem Ut Consulting" - }, - { - "name": "Jocelyn Fulton", - "email": "tristique.senectus.et@faucibusorci.co.uk", - "cou": "Active, Member, Filesource, Writer", - "org": "Ante Ipsum Institute" - }, - { - "name": "Tatyana Kelly", - "email": "a.facilisis.non@tortorat.ca", - "cou": "", - "org": "Donec Feugiat Limited" - } -] \ No newline at end of file diff --git a/View/Layouts/Emails/html/default.ctp b/View/Layouts/Emails/html/default.ctp deleted file mode 100644 index 747994a..0000000 --- a/View/Layouts/Emails/html/default.ctp +++ /dev/null @@ -1,27 +0,0 @@ - - - - - <?php echo $this->fetch('title'); ?> - - - fetch('content'); ?> - -

This email was sent using the CakePHP Framework

- - \ No newline at end of file diff --git a/View/Layouts/Emails/text/default.ctp b/View/Layouts/Emails/text/default.ctp deleted file mode 100644 index 82a1e19..0000000 --- a/View/Layouts/Emails/text/default.ctp +++ /dev/null @@ -1,19 +0,0 @@ - -fetch('content'); ?> - -This email was sent using the CakePHP Framework, https://cakephp.org. diff --git a/View/Layouts/empty b/View/Layouts/empty deleted file mode 100644 index e69de29..0000000 diff --git a/View/Layouts/error.ctp b/View/Layouts/error.ctp index 9eea68b..6c7db37 100644 --- a/View/Layouts/error.ctp +++ b/View/Layouts/error.ctp @@ -29,9 +29,9 @@ $cakeDescription = __d('cake_dev', 'CakePHP: the rapid development php framework echo $this->Html->css('cake.generic'); - echo $this->fetch('GrouperLite.meta'); - echo $this->fetch('GrouperLite.css'); - echo $this->fetch('GrouperLite.script'); + echo $this->fetch('GrouperLiteWidget.meta'); + echo $this->fetch('GrouperLiteWidget.css'); + echo $this->fetch('GrouperLiteWidget.script'); ?> diff --git a/View/Layouts/rss/default.ctp b/View/Layouts/rss/default.ctp deleted file mode 100644 index 60a5365..0000000 --- a/View/Layouts/rss/default.ctp +++ /dev/null @@ -1,13 +0,0 @@ -fetch('title'); -endif; - -echo $this->Rss->document( - $this->Rss->channel( - array(), $channel, $this->fetch('content') - ) -); diff --git a/View/Layouts/xml/default.ctp b/View/Layouts/xml/default.ctp deleted file mode 100644 index fbd5ee0..0000000 --- a/View/Layouts/xml/default.ctp +++ /dev/null @@ -1 +0,0 @@ -fetch('content'); ?> diff --git a/webroot/css/co-grouper-base.css b/webroot/css/co-grouper-base.css index 8080b7f..a1aab2e 100644 --- a/webroot/css/co-grouper-base.css +++ b/webroot/css/co-grouper-base.css @@ -1,28 +1,163 @@ :root { - --red: #CC3333; + --red: #B32D2D; --blue: #1D7AB4; - --green: #22AA22; + --green: #1D871D; + --orange: #faa732; --teal: #1c6070; + --darkteal: #003f59; + --link: #0076a5; + --link-hover: #003f59; --danger: var(--red); --success: var(--green); --primary: var(--teal); + --warning: var(--orange); + --secondary: var(--darkteal) +} + +.text-grouper { + color: var(--primary); +} + +.text-sml { + font-size: 0.8rem; +} + +button:disabled, +input[type=button]:disabled, +input[type=button][disabled], +button[disabled]{ + background:#999 !important; + color:#ffffff !important; + border: none !important; + cursor: not-allowed; +} + +#content .material-icons.lg { + font-size: 1.2rem; +} + +.grouper .btn:not(.btn-link) { + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 0 3px 1px -2px rgba(0, 0, 0, .2), 0 1px 5px 0 rgba(0, 0, 0, .12); + max-width: 220px; +} + +.grouper .btn:not([disabled]):hover { + text-decoration: none !important; + filter: brightness(1.1); } .grouper .btn.btn-primary { background-color: var(--primary); border-color: var(--primary); - box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 0 3px 1px -2px rgba(0, 0, 0, .2), 0 1px 5px 0 rgba(0, 0, 0, .12); } -.grouper .btn.btn-success.btn-grouper { +.grouper .btn.btn-success { background-color: var(--success); border-color: var(--success); - box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 0 3px 1px -2px rgba(0, 0, 0, .2), 0 1px 5px 0 rgba(0, 0, 0, .12); } -.grouper .btn.btn-danger.btn-grouper { +.grouper .btn.btn-danger { background-color: var(--danger); border-color: var(--danger); - color: white; - box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 0 3px 1px -2px rgba(0, 0, 0, .2), 0 1px 5px 0 rgba(0, 0, 0, .12); -} \ No newline at end of file +} + +.grouper .btn.btn-secondary { + background-color: var(--secondary); + border-color: var(--secondary); +} + +.grouper .btn.btn-link, +.grouper a:not(.btn) { + color: var(--link); +} + +.grouper .btn.btn-link:hover, +.grouper a:not(.btn):hover { + color: var(--link-hover); +} + +.btn-grouper { + min-width: fit-content; +} + +#subscribers.loading .loader { + display: block; +} + +#subscribers.loading .subs, +#subscribers.error .subs { + display: none; +} + +#subscribers.error .msg { + display: block; +} + +#subscribers .loader, +#subscribers .msg { + display: none; +} + +.modal-open .ui-menu { + z-index: 2000; +} + +#grouper-loader { + pointer-events: none; + width: 100px; + display: block; + margin: 0 auto; + overflow: visible; + padding: 15px; + max-width: 100%; +} + +#grouper-loader circle { + transform-origin: center; + fill: var(--primary); + transition: ease; + animation-timing-function: ease-in-out !important; +} + +#search { + background-color: #F6F6F6; +} + +/* fade out half */ + +#grouper-loader.fade-out-half circle:nth-child(1) { + animation: fadeInHalf 0.9s -2.3s infinite reverse; +} + +#grouper-loader.fade-out-half circle:nth-child(2) { + animation: fadeInHalf 0.9s -1.3s infinite reverse; +} + +#grouper-loader.fade-out-half circle:nth-child(3) { + animation: fadeInHalf 0.9s -0.3s infinite reverse; +} + +.grouper_groups .person-item { + overflow-wrap: anywhere; +} + +.popover { + max-width: 400px; +} + +.ui-autocomplete { + max-height: 240px; + overflow-y: auto; + overflow-x: hidden; + padding-right: 20px; +} + +@keyframes fadeInHalf { + from { + opacity: 0.5; + } + + to { + opacity: 1; + } +} diff --git a/webroot/css/co-grouper-plugin.css b/webroot/css/co-grouper-plugin.css index bdc2c78..6e8f27e 100644 --- a/webroot/css/co-grouper-plugin.css +++ b/webroot/css/co-grouper-plugin.css @@ -14,6 +14,10 @@ border-radius: .2rem; } +.btn-fit { + height: fit-content; +} + a { color: var(--primary); } @@ -315,4 +319,24 @@ a.list-group-item-action:hover .fa { background-color: #ececec; border-bottom: 1px solid #FFF !important; margin-bottom: 1px; -} \ No newline at end of file +} + +.grouper_groups .co-loading-mini-input-container { + flex-grow: 1; +} + +.grouper_groups .co-loading-mini-input-container .co-loading-mini { + display: none; + position: absolute; + right: 38px; + bottom: 10px; + margin-right: -26px; +} + +.grouper_groups #co-loading span, +.grouper_groups #co-loading-redirect span, +.grouper_groups .co-loading-mini span { + animation: 1.2s linear infinite both loading; + background-color: var(--teal); + display: inline-block; +} diff --git a/webroot/js/autocomplete.grouperplugin.js b/webroot/js/autocomplete.grouperplugin.js new file mode 100644 index 0000000..0b37dc9 --- /dev/null +++ b/webroot/js/autocomplete.grouperplugin.js @@ -0,0 +1,21 @@ +$.widget( "ui.autocomplete", $.ui.autocomplete, { + _renderMenu: function( ul, items ) { + var that = this; + $.each( items, function( index, item ) { + that._renderItemData( ul, item ); + }); + }, + _renderItem: function( ul, item ) { + let itemMarkup = '
'; + itemMarkup += '
' + item.label + '
'; + if(item?.emailShort != '') { + itemMarkup += ''; + } + if(item?.identifierShort != '') { + itemMarkup += '
' + item.identifierLabel + '' + item.identifierShort + '
'; + } + itemMarkup += '
'; + + return $("
  • ").append(itemMarkup).appendTo(ul); + } +}); \ No newline at end of file diff --git a/webroot/js/autocomplete.js b/webroot/js/autocomplete.js index 284ff57..da2e95e 100644 --- a/webroot/js/autocomplete.js +++ b/webroot/js/autocomplete.js @@ -2,50 +2,136 @@ export default { props: { - group: String + action: { + type: String, + default: "addUser" + }, + icon: { + type: String, + default: 'add' + }, + forceDisable: { + type: Boolean, + default: false + }, + renderBtn: { + type: Boolean, + default: true + }, + preInfo: { + type: String, + default: '' + } }, - inject: ['txt', 'api'], + inject: ['txt', 'api', 'other'], data() { return { search: '', + enableBtn: false, val: '', item: null, + limit: 15, + minLength: 3, + url: '' }; }, methods: { - addUser(search) { - this.$emit('add', this.item); + performAction() { + this.$emit('callback', this.item); + }, + toKebabCase(str) { + return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); + }, + enableBtnFunc() { + // The minimum length that i start search is 3. So we only enable the button when + // the input text value has at least three characters + return this.enableBtn && !this.forceDisable + }, + }, + computed: { + btnTxt() { + return eval(`this.txt.${this.action}`) ?? eval(`this.txt.${this.icon}`) + }, + autcompleteId() { + return `autocomplete-search-container-${this.action}` } }, mounted(el) { - const input = $(this.$el).find('#add-user-input'); + const input = $(this.$el).find(`#${this.toKebabCase(this.action)}-input`); + const action = this.action + this.url = `${this.api.find}?co=${this.api.co}&mode=${this.api.mode}&page=${this.page}&limit=${this.limit}` input.autocomplete({ - source: `${this.api.find}?co=${this.api.co}&mode=${this.api.mode}`, - minLength: 1, - maxShowItems: 10, - focus: function( event, ui ) { - this.search = ui.item.label; - this.val = ui.item.identifier; - this.item = ui.item; - return false; + source: ( request, response ) => { + if(this.forceDisable) { + return ["Not allowed"] + } + $(`#autocomplete-search-container-${this.action} .co-loading-mini`).show(); + $.ajax({ + url: this.url, + type: 'GET', + dataType: "json", + data: { + // XXX Change the term key to any other query key that fit your needs. + term: request.term + }, + success: function (data) { + $(`#autocomplete-search-container-${action} .co-loading-mini`).hide(); + response( data ); + }, + error: function(data) { + $(`#autocomplete-search-container-${action} .co-loading-mini`).hide(); + console.log('Autocomplete ajax error:', data) + generateFlash('Find action failed', 'error'); + } + }); + }, + delay: 1000, + minLength: this.minLength, + maxShowItems: this.limit, + create: (event, ui) => { + $(`#${this.toKebabCase(this.action)}-input`).focus(); }, + // XXX We need access to the parent data object. + // As a result we have to use arrow function syntax (ES6) select: (event, ui) => { this.val = ui.item.identifier; - this.search = ui.item.label; this.item = ui.item; - $("#addUserbutton").prop('disabled', false).focus(); - return false; + this.search = `${ui.item.label} (${ui.item.value})`; + if(this.renderBtn) { + this.enableBtn = true; + $(`#${this.toKebabCase(this.action)}-btn`).focus(); + } else { + // Since we are not rendering any button, we will trigger the search + this.performAction() + } }, - }).autocomplete( "instance" )._renderItem = formatCoPersonAutoselectItem; + }) }, template: /*html*/` -
    - - -
    -
    diff --git a/webroot/js/grouper-groups-view.js b/webroot/js/grouper-groups-view.js index e2a508d..7d75678 100644 --- a/webroot/js/grouper-groups-view.js +++ b/webroot/js/grouper-groups-view.js @@ -25,6 +25,10 @@ export default { { path: `/groupowner/co:${this.api.co}/glid:${this.api.glid}`, label: this.txt.tabs.owner + }, + { + path: `/usermanager/co:${this.api.co}/glid:${this.api.glid}`, + label: this.txt.tabs.manager } ] } diff --git a/webroot/js/groups-table.js b/webroot/js/groups-table.js index f6581f2..f9f9304 100644 --- a/webroot/js/groups-table.js +++ b/webroot/js/groups-table.js @@ -6,8 +6,12 @@ export default { Table ], data() {}, - computed: { + methods: { + groupStatus(status) { + return status === 'T' ? 'Enabled' : 'Disabled' + } }, + inject: ['txt', 'api', 'other'], created() {}, template: /*html*/` @@ -20,44 +24,60 @@ export default { - + @@ -89,7 +109,7 @@ export default { @@ -119,7 +139,7 @@ export default { /*
    - {{ group.friendlyName || 'No Name' }} + {{ group.displayExtension || 'No Name' }} {{ group.description || txt.descrZeroState }}{{ group.enabled && group.enabled === 'T' ? 'Enabled' : 'Disabled' }}{{ groupStatus(group?.enabled) }} - + + + + + - + + {{ txt.grouper }}   - + @@ -75,7 +95,7 @@ export default { role="button" :aria-expanded="!show ? 'false' : 'true'"> {{ config.wgHeading || 'Working groups' }} - +