Skip to content
Permalink
main
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
3 contributors

Users who have contributed to this file

@arlen @Ioannis @benno
429 lines (346 sloc) 14.9 KB
<?php
/**
* COmanage Match Matchgrids Controller
*
* Portions licensed to the University Corporation for Advanced Internet
* Development, Inc. ("UCAID") under one or more contributor license agreements.
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* UCAID licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @link https://www.internet2.edu/comanage COmanage Project
* @package match
* @since COmanage Match v1.0.0
* @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/
declare(strict_types = 1);
namespace App\Controller;
use \Cake\Log\Log;
use \Cake\Event\EventInterface;
use \App\Lib\Enum\PermissionEnum;
use \App\Lib\Enum\MatchgridActionEnum;
class MatchgridsController extends StandardController {
public $pagination = [
'order' => [
'Matchgrids.table_name' => 'asc'
]
];
/**
* Callback run prior to the request action.
*
* @since COmanage Match v1.0.0
* @param \Cake\Event\EventInterface $event Cake Event
*/
public function beforeFilter(EventInterface $event) {
// Only certain actions require a matchgrid ID
if(in_array($this->request->getParam('action'),
['build', 'manage', 'configure', 'pending', 'reconcile'])) {
$this->Matchgrids->setRequiresMatchgrid(true);
}
parent::beforeFilter($event);
}
/**
* Build the Matchgrid based on the current configuration.
*
* @since COmanage Match v1.0.0
* @param Integer $id Matchgrid ID
*/
public function build(string $id) {
try {
$this->Matchgrids->build((int)$id);
$this->Flash->success(__('match.rs.build'));
}
catch(Exception $e) {
$this->Flash->error(__('match.er.build', [$e->getMessage()]));
}
return $this->redirect(['action' => 'manage', $id]);
}
/**
* Handle a delete action for a Matchgrid object.
*
* @since COmanage Match v1.1.0
* @param Integer $id Object ID
*/
public function delete($id) {
// Deleting a Matchgrid is a bit more complicated than other objects.
try {
// First, we'll pull the requested object.
$obj = $this->Matchgrids->findById($id)->firstOrFail();
// We manually apply the status check rule here, since otherwise
// it won't get applied until we actually try to remove the Matchgrid,
// which could be _after_ we remove the table, below, which is confusing.
$err = $this->Matchgrids->ruleIsActive($obj, []);
if(is_string($err)) {
throw new \Exception($err);
}
// See if the Matchgrid table exists.
if(!$this->Matchgrids->tableExists($obj)) {
// Just execute the delete
parent::delete($id);
}
// Determine what the admin wants to do with the matchgrid table.
// The first time through this will be empty, so we provide the form.
$action = $this->request->getData('remove');
if($action === 'leave') {
// Leave the Matchgrid table in place
parent::delete($id);
} elseif($action === 'remove') {
// Remove the Matchgrid table, then execute the standard delete
$this->Matchgrids->removeTable($obj);
parent::delete($id);
} else {
// The table exists, require the admin to indicate whether or
// not the table should also be deleted. Set some view variables.
$this->set('vv_title', __('match.op.delete.matchgrid', [$obj->table_name]));
$this->set('vv_matchgrid', $obj->prefixed_table_name);
}
}
catch(\Exception $e) {
// findById throws Cake\Datasource\Exception\RecordNotFoundException
$this->Flash->error($e->getMessage());
return $this->generateRedirect();
}
// If we get here, we need to render the form for table deletion
}
/**
* Authorization for this Controller, called by Auth component
* - postcondition: $vv_permissions set with calculated permissions for this Controller
*
* @since COmanage Match v1.0.0
* @param Array $user Array of user data
* @return Boolean True if authorized for the current action, false otherwise
*/
public function isAuthorized(Array $user) {
$platformAdmin = $this->Authorization->isPlatformAdmin($user['username']);
$mgid = isset($this->cur_mg->id) ? $this->cur_mg->id : null;
$mgAdmin = $this->Authorization->isMatchAdmin($user['username'], $mgid);
$recMgr = $this->Authorization->isReconciliationManager($user['username'], $mgid);
$p = [
'add' => $platformAdmin,
'build' => $platformAdmin || $mgAdmin,
'delete' => $platformAdmin,
'display' => $platformAdmin || $mgAdmin,
'edit' => $platformAdmin,
'index' => $platformAdmin,
'manage' => $platformAdmin || $mgAdmin,
'configure' => $platformAdmin || $mgAdmin,
'pending' => $platformAdmin || $mgAdmin || $recMgr,
'reconcile' => $platformAdmin || $mgAdmin || $recMgr,
// We allow anyone to access select since we don't have a matchgrid context yet.
// If $user has no meaningful permissions, they'll get no menu options.
'select' => true,
'view' => false
];
$this->set('vv_permissions', $p);
return $p[$this->request->getParam('action')];
}
/**
* Manage a matchgrid. This is the main landing page for matchgrid related operations.
*
* @since COmanage Match v1.0.0
* @param String $id Matchgrid ID
*/
public function manage(string $id) {
$this->set('vv_title', __('match.op.manage.a', [$this->cur_mg->table_name]));
}
/**
* Configure a matchgrid. This is an index of items to configure for the matchgrid.
*
* @since COmanage Match v1.0.0
* @param String $id Matchgrid ID
*/
public function configure(string $id) {
$this->set('vv_title', __('match.op.configure.a', [$this->cur_mg->table_name]));
}
/**
* Display the set of pending requests.
*
* @since COmanage Match v1.0.0
* @param String $id Matchgrid ID
*/
public function pending(string $id) {
try {
$MatchService = new \App\Lib\Match\MatchService();
$MatchService->connectDatabase();
$MatchService->setConfig((int)$id);
$results = $MatchService->getRequests('pending');
$MatchService->disconnectDatabase();
// Although we're passing the $id as provided by the user, it has been
// vetted since MatchgridLinkTrait will pull the current Matchgrid from
// the database before we get here and throw an error if $id is invalid.
$this->set('vv_matchgrid_id', $id);
$this->set('vv_pending', $results->getRawResults());
}
catch(Exception $e) {
$this->Flash->error(__('match.er.reconcile', [$e->getMessage()]));
}
$this->set('vv_title', __('match.op.reconcile.a', [$this->cur_mg->table_name]));
}
/**
* Reconcile a pending request.
*
* @since COmanage Match v1.0.0
* @param String $id Matchgrid ID
*/
public function reconcile(string $id) {
// There's roughly similar logic in TierApiController::doViewMatchRequest,
// which could perhaps at some point be consolidated or moved to a trait...
try {
$MatchService = new \App\Lib\Match\MatchService();
$AttributeManager = new \App\Lib\Match\AttributeManager();
$MatchService->connectDatabase();
$MatchService->setConfig((int)$id);
if($this->request->is('post')) {
// We've got the requested resolution
$req = $this->request->getData();
if(empty($req['rowid']) || empty($req['referenceid'])) {
// Just throw an exception that we'll catch and render below
throw new \RuntimeException(__('match.er.args', ['reconcile']));
}
$rowid = (int)$req['rowid'];
// Pull the request so we know which sor+sorid we're working with.
// Plausibly this could also have been passed through the form, but it
// seems easier to just pass the row ID.
$results = $MatchService->getRequest($rowid);
$row = $results->getRawResults();
$sor = $row[$rowid]['sor'];
$sorid = $row[$rowid]['sorid'];
// Cake Form tampering protection should ensure that $req['referenceid']
// is valid and one we originally proposed.
$refId = $MatchService->attachReferenceId($sor, $sorid, $AttributeManager, $req['referenceid']);
$this->Flash->success(__('match.rs.refid.assigned', [$refId]));
// Record resolution in MatchgridHistory
$MatchgridHistory = $this->getTableLocator()->get('MatchgridHistoryRecords');
$MatchgridHistory->record((int)$id,
$sor,
$sorid,
MatchgridActionEnum::ResolvedPendingMatchUI,
__('match.en.MatchgridActionEnum.RPMU'),
$this->request->getEnv('REMOTE_ADDR'),
$this->request->getSession()->read('Auth.User.username'));
// If configured, send a resolution notification
$MatchgridSettings = $this->getTableLocator()->get('MatchgridSettings');
$endPoint = $MatchgridSettings->getNotificationEndpoint((int)$id);
if(!empty($endPoint)) {
$Http = new \Cake\Http\Client([
'auth' => [
'username' => $endPoint->username,
'password' => $endPoint->password
]
]);
$message = [
'meta' => [
'source' => "COmanage Match",
'event' => "match-resolution",
'format' => "1"
],
'sor' => (string)$sor,
'sorid' => (string)$sorid,
'matchRequest' => (string)$rowid,
'referenceId' => (string)$refId,
'resolutionTime' => gmdate('Y-m-d\TH:i:s\Z')
];
$response = $Http->post(
$endPoint->url,
json_encode($message),
['type' => 'json']
);
$statusCode = $response->getStatusCode();
// Create a suitable history record
if($statusCode >= 200 && $statusCode <= 299) {
// Success
$MatchgridHistory->record((int)$id,
$sor,
$sorid,
MatchgridActionEnum::ResolutionEndpointNotified,
__('match.en.MatchgridActionEnum.REPN', [$statusCode, $response->getReasonPhrase()]),
$this->request->getEnv('REMOTE_ADDR'),
$this->request->getSession()->read('Auth.User.username'));
} else {
// Anything else is treated as an error
$errorMessage = $response->getJson();
$MatchgridHistory->record((int)$id,
$sor,
$sorid,
MatchgridActionEnum::ResolutionEndpointNotified,
__('match.en.MatchgridActionEnum.REPN', [$statusCode, $errorMessage['error'] ?? $response->getReasonPhrase()]),
$this->request->getEnv('REMOTE_ADDR'),
$this->request->getSession()->read('Auth.User.username'));
}
}
$MatchService->disconnectDatabase();
// Redirect back to list of pending requests
return $this->redirect([
'action' => 'pending',
$this->cur_mg->id
]);
} else { // is get
// Find and render the candidates for the pending record
$rowId = (int)$this->request->getQuery('rowid');
if(!$rowId) {
// Throw an error to be caught below
throw new \RuntimeException(__('match.er.args', ['reconcile']));
}
$request = $MatchService->getRequest($rowId);
$candidates = [];
if($request->count() == 0) {
// No such request ID
throw new \RuntimeException(__('match.er.reconcile.notfound', [$rowId]));
}
// Parse the original request
$origReq = $request->getRawResults();
if(!empty($origReq[$rowId]['referenceId'])) {
// This is a resolved request
throw new \RuntimeException(__('match.er.reconcile.done', [$rowId]));
}
// Extract the SOR and SORID
$sor = $origReq[$rowId]['sor'];
$sorid = $origReq[$rowId]['sorid'];
// Use AttributeManager to parse the current record back into database format for searching
// Note we have the raw version, but this remunging of the json is "easier" than adding
// another interface to AttributeManager for the moment.
$AttributeManager = new \App\Lib\Match\AttributeManager();
// We have an array but parseFromJSON wants an object
$AttributeManager->parseFromJSON(json_decode(json_encode($request->getResultsForJson('current'))));
$results = $MatchService->searchReferenceId($sor, $sorid, $AttributeManager);
// Insert the original request as "new"
$candidates[] = $origReq[$rowId];
// Count could be 0 if we failed to match any rules at all (canonical or potential)
if($results->count() > 0) {
$candidates = array_merge($candidates,$results->getRawResults());
}
$this->set('vv_candidates', $candidates);
// Calculate which attributes differ, accounting for the configuration
// rules, for the frontend to render
$this->set('vv_candidate_diff', $MatchService->diffCandidates($origReq[$rowId], $candidates));
// Also set the original request separately to make it easier for the view
$this->set('vv_request', $origReq[$rowId]);
$this->set('vv_title', __('match.op.reconcile.request', [$sor, $sorid]));
$MatchService->disconnectDatabase();
} // is post
}
catch(Exception $e) {
$this->Flash->error(__('match.er.reconcile', [$e->getMessage()]));
}
}
/**
* Provide a set of Matchgrids to operate on.
*
* @since COmanage Match v1.0.0
* @param String $id Matchgrid ID
*/
public function select() {
$this->set('vv_title', __('match.op.select.mg'));
}
}