From b3ccb8057e21b9e82255e91eea0b8a5d9bbb150a Mon Sep 17 00:00:00 2001 From: Maxime Mulder Date: Wed, 18 Dec 2024 21:53:00 -0500 Subject: [PATCH] wip redcap config --- modules/redcap/CONFIGURATION.md | 54 +++ .../redcap/php/config/RedcapConfig.class.inc | 101 ++++++ .../RedcapConfigLorisCandidateId.class.inc | 30 ++ .../php/config/RedcapConfigParser.class.inc | 275 +++++++++++++++ .../RedcapConfigRedcapParticipantId.class.inc | 21 ++ .../php/config/RedcapConfigVisit.class.inc | 60 ++++ .../redcapconfiguration.class.inc | 239 ------------- .../redcapconfigurationinstance.class.inc | 208 ------------ .../redcapconfigurationparser.class.inc | 319 ------------------ .../redcapconfigurationproject.class.inc | 71 ---- .../php/endpoints/notifications.class.inc | 52 +-- modules/redcap/php/module.class.inc | 32 -- .../redcapnotificationhandler.class.inc | 14 +- 13 files changed, 582 insertions(+), 894 deletions(-) create mode 100644 modules/redcap/CONFIGURATION.md create mode 100644 modules/redcap/php/config/RedcapConfig.class.inc create mode 100644 modules/redcap/php/config/RedcapConfigLorisCandidateId.class.inc create mode 100644 modules/redcap/php/config/RedcapConfigParser.class.inc create mode 100644 modules/redcap/php/config/RedcapConfigRedcapParticipantId.class.inc create mode 100644 modules/redcap/php/config/RedcapConfigVisit.class.inc delete mode 100644 modules/redcap/php/configurations/redcapconfiguration.class.inc delete mode 100644 modules/redcap/php/configurations/redcapconfigurationinstance.class.inc delete mode 100644 modules/redcap/php/configurations/redcapconfigurationparser.class.inc delete mode 100644 modules/redcap/php/configurations/redcapconfigurationproject.class.inc diff --git a/modules/redcap/CONFIGURATION.md b/modules/redcap/CONFIGURATION.md new file mode 100644 index 00000000000..e2307137ffa --- /dev/null +++ b/modules/redcap/CONFIGURATION.md @@ -0,0 +1,54 @@ +# LORIS REDCap module configuration + +The LORIS REDCap module can be configured in the LORIS `config.xml` file. + +## Configuration template + +The configuration is of the following form: + +```xml + + REDCap + + https://www.example.net/redcap/ + + 1 + ABCDEFGGHIJKLMNOPQRSTUVWXYZ + record-id + psc-id + + visit_1 + arm_1 + event_1 + + + + +``` + +## Detailed description + +The configuration nodes are the following: +- `redcap` (required): Root node of the LORIS REDCap configuration. +- `issue-assignee` (required): The LORIS user ID of the user to whom to assign issues in case of a REDCap module malfunction. +- `instance` (required, multiple allowed): The list of instance entries to synchronize with LORIS. + +In an `instance` entry, the configuration parameters are the following: +- `redcap-url` (required): The URL of the REDCap instance. This is also the URL of the REDCap instance API without the `api` suffix. +- `project` (required, multiple allowed): The list of project entries in this REDCap instance to synchronize with LORIS. + +In a `project` entry, the configuration parameters are the following: +- `redcap-project-id` (required): The REDCap project ID of the REDCap project described by this entry. +- `redcap-api-token` (required): The REDCap API token used by LORIS to retrieve REDCap data for this project. +- `redcap-participant-id` (optional): The type of REDCap participant identifier used to map the REDCap participants with the LORIS candidates. The two options are `record-id` and `survey-participant-id`. If not present, `record-id` is used. +- `candidate-id` (optional): The type of LORIS candidate identifier used to map the REDCap participants with the LORIS candidates. The two options are `PSCID` and `DCCID`. If not present, `PSCID` is used. +- `visit` (optional, multiple allowed): The list of visit entries that describe how REDCap arms and events are mapped to LORIS visits. If not present, the REDCap arms are ignored and the REDCap event names are matched to LORIS visit labels with the same name. + +In a `visit` entry, the configuration parameters are the following: +- `visit-label` (required): The LORIS visit label of the visit to which to attach the instrument responses that match this entry. +- `redcap-arm-name` (optional): The REDCap arm name that the instrument responses must match to be attached to this visit. If not present, the arm name is ignored when filtering instrument responses. +- `redcap-event-name` (optional): The REDCap event name that the instrument responses must match to be attached to this visit. If not present, the event name is ignored when filtering instrument responses. + +## Check configuraion + +The tool `redcap_check_config.php` can be used to check that the module configuration is correct. diff --git a/modules/redcap/php/config/RedcapConfig.class.inc b/modules/redcap/php/config/RedcapConfig.class.inc new file mode 100644 index 00000000000..4393e30187d --- /dev/null +++ b/modules/redcap/php/config/RedcapConfig.class.inc @@ -0,0 +1,101 @@ +issue_assignee = $issue_assignee; + $this->redcap_project_id = $redcap_project_id; + $this->redcap_url = $redcap_url; + $this->redcap_api_token = $redcap_api_token; + $this->redcap_participant_id = $redcap_participant_id; + $this->candidate_id = $candidate_id; + $this->visits = $visits; + } +} diff --git a/modules/redcap/php/config/RedcapConfigLorisCandidateId.class.inc b/modules/redcap/php/config/RedcapConfigLorisCandidateId.class.inc new file mode 100644 index 00000000000..1a53829cd34 --- /dev/null +++ b/modules/redcap/php/config/RedcapConfigLorisCandidateId.class.inc @@ -0,0 +1,30 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ + +namespace LORIS\redcap\config; + +/** + * Enumeration that describes which LORIS candidate identifier should be used to + * map with the participant identifier obtained from REDCap. + */ +enum RedcapConfigLorisCandidateId +{ + /** + * The REDCap partcipant identifier should be mapped to a LORIS candidate PSCID. + */ + case PscId; + + /** + * The REDCap partcipant identifier should be mapped to a LORIS candidate CandID. + */ + case CandId; +} diff --git a/modules/redcap/php/config/RedcapConfigParser.class.inc b/modules/redcap/php/config/RedcapConfigParser.class.inc new file mode 100644 index 00000000000..ff11fca5aef --- /dev/null +++ b/modules/redcap/php/config/RedcapConfigParser.class.inc @@ -0,0 +1,275 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ + +namespace LORIS\redcap\config; + +use LORIS\redcap\config\RedcapConfig; +use LORIS\redcap\config\RedcapConfigLorisCandidateId; +use LORIS\redcap\config\RedcapConfigRedcapParticipantId; +use LORIS\redcap\config\RedcapConfigVisit; + +/** + * This represents a REDCap configuration parser. + * + * @category REDCap + * @package Main + * @author Regis Ongaro-Carcy + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class RedcapConfigParser +{ + /** + * The LORIS instance. + * + * @var \LORIS\LorisInstance + */ + private \LORIS\LorisInstance $_loris; + + /** + * The LORIS database. + * + * @var \Database + */ + private \Database $_db; + + /** + * Constructor. + * + * @param \LORIS\LorisInstance $loris a loris instance + * @param bool $verbose verbose mode + */ + private function __construct(\LORIS\LorisInstance $loris) { + $this->_loris = $loris; + $this->_db = $loris->getDatabaseConnection(); + } + + public function parse(string $redcap_url, string $redcap_project_id): ?RedcapConfig + { + // Get the LORIS configuration + $config = $this->_loris->getConfiguration(); + + // Get the REDCap LORIS configuration tree + $redcap_node = $config->getSetting('redcap'); + if (empty($redcap)) { + throw new \LorisException("[redcap][init] no REDCap configuration."); + } + + return $this->_parseRedcapNode($redcap_node, $redcap_url, $redcap_project_id); + } + + private function _parseRedcapNode( + array $redcap_node, + string $redcap_url, + string $redcap_project_id, + ): ?RedcapConfig { + $issue_assignee = $this->_getIssueAssignee($redcap_node); + + $instance_node = $this->_getInstanceNode($redcap_node, $redcap_url); + if ($instance_node === null) { + error_log("TODO."); + return null; + } + + $project_node = $this->_getProjectNode($instance_node, $redcap_project_id); + if ($project_node === null) { + error_log("TODO."); + return null; + } + + $redcap_api_token = $this->_parseRedcapApiToken($project_node); + $redcap_participant_id = $this->_parseRedcapParticipantId($project_node); + $candidate_id = $this->_parseCandidateId($project_node); + $visits = $this->_parseVisits($project_node); + + return new RedcapConfig( + $issue_assignee, + $redcap_project_id, + $redcap_url, + $redcap_api_token, + $redcap_participant_id, + $candidate_id, + $visits, + ); + } + + private function _getIssueAssignee(array $redcap_node): \User + { + $assignee_user_id = $redcap_node['issue-assignee'] ?? null; + if (!isset($assignee_user_id) || empty($assignee_user_id)) { + throw new \LorisException( + "TODO." + ); + } + + $assignee = $this->_db->pselectOne( + "SELECT u.userID + FROM users u + JOIN user_perm_rel upr ON (upr.userid = u.id) + JOIN permissions p ON (p.permid = upr.permid) + WHERE u.userID = :usid + AND u.Active = 'Y' + AND u.Pending_approval = 'N' + AND p.code = 'issue_tracker_developer' + ", + ['usid' => $assignee_user_id] + ); + + if ($assignee === null) { + throw new \LorisException( + "[redcap][init] assignee '$assignee_user_id' does not exist" + . " or does not have enough privilege." + ); + } + + return \User::factory($assignee_user_id); + } + + private function _getInstanceNode(array $redcap_node, string $redcap_url): ?array + { + $instance_entry = $redcap_node['instance']; + + // No instance described. + if (!isset($instance_entry)) { + throw new \LorisException( + "TODO." + ); + } + + if (array_key_exists(0, $instance_entry)) { + // Multiple instances. + $instance_nodes = $instance_entry; + } else { + // Single instance. + $instance_nodes = [$instance_entry]; + } + + // TODO: Replace with `array_find` in PHP 8.4 + return reset(array_filter( + $instance_nodes, + fn($instance_node) => $instance_node['redcap-url'] === $redcap_url, + )); + } + + private function _getProjectNode(array $instance_node, string $redcap_project_id): ?array + { + $project_entry = $instance_node['project']; + + if (!isset($project_entry)) { + throw new \LorisException( + "TODO." + ); + } + + if (array_key_exists(0, $project_entry)) { + // Multiple projects. + $project_nodes = $project_entry; + } else { + // Single project. + $project_nodes = [$project_entry]; + } + + // TODO: Replace with `array_find` in PHP 8.4 + return reset(array_filter( + $project_nodes, + fn($project_node) => $project_node['redcap-project-id'] === $redcap_project_id, + )); + } + + private function _parseRedcapApiToken(array $project_node): string { + $api_token = $project_node['api_token']; + + if (empty($api_token)) { + throw new \LorisException( + "TODO." + ); + } + + return $api_token; + } + + private function _parseRedcapParticipantId( + array $project_node, + ): RedcapConfigRedcapParticipantId { + $redcap_participant_id = $project_node['redcap-participant-id']; + + // Default value. + if (empty($redcap_participant_id)) { + return RedcapConfigRedcapParticipantId::RecordId; + } + + switch ($redcap_participant_id) { + case 'record-id': + return RedcapConfigRedcapParticipantId::RecordId; + case 'survery-participant-id': + return RedcapConfigRedcapParticipantId::SurveyParticipantId; + default: + throw new \LorisException( + "TODO." + ); + } + } + + private function _parseCandidateId( + array $project_node, + ): RedcapConfigLorisCandidateId { + $loris_candidate_id = $project_node['candidate-id']; + + // Default value. + if (empty($loris_candidate_id)) { + return RedcapConfigLorisCandidateId::PscId; + } + + switch ($loris_candidate_id) { + case 'DCCID': + return RedcapConfigLorisCandidateId::CandId; + case 'PSCID': + return RedcapConfigLorisCandidateId::PscId; + default: + throw new \LorisException( + "[redcap][init] TODO." + ); + } + } + + private function _parseVisits(array $project_node): ?array { + $visit_entry = $project_node['visit']; + + if (!isset($visit_entry)) { + return null; + } + + if (array_key_exists(0, $visit_entry)) { + // Multiple visits. + $visit_nodes = $visit_entry; + } else { + // Single visit. + $visit_nodes = [$visit_entry]; + } + + return array_map($this->_parseVisit(...), $visit_nodes); + } + + private function _parseVisit(array $visit_node): RedcapConfigVisit { + $visit_label = $visit_node['visit-label']; + $redcap_arm_name = $visit_node['redcap-arm-name']; + $redcap_event_name = $visit_node['redcap-event-name']; + + // TODO: Error handling. + + return new RedcapConfigVisit( + $visit_label, + $redcap_arm_name, + $redcap_event_name, + ); + } +} diff --git a/modules/redcap/php/config/RedcapConfigRedcapParticipantId.class.inc b/modules/redcap/php/config/RedcapConfigRedcapParticipantId.class.inc new file mode 100644 index 00000000000..9b50db63e44 --- /dev/null +++ b/modules/redcap/php/config/RedcapConfigRedcapParticipantId.class.inc @@ -0,0 +1,21 @@ +visit_label = $visit_label; + $this->redcap_arm_name = $redcap_arm_name; + $this->redcap_event_name = $redcap_event_name; + } +} diff --git a/modules/redcap/php/configurations/redcapconfiguration.class.inc b/modules/redcap/php/configurations/redcapconfiguration.class.inc deleted file mode 100644 index 9e3e64541c1..00000000000 --- a/modules/redcap/php/configurations/redcapconfiguration.class.inc +++ /dev/null @@ -1,239 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 - * @link https://www.github.com/aces/Loris/ - */ - -namespace LORIS\redcap\configurations; - - -/** - * This represents a REDCap configuration. - * - * @category REDCap - * @package Main - * @author Regis Ongaro-Carcy - * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 - * @link https://www.github.com/aces/Loris/ - */ -class RedcapConfiguration -{ - /** - * A LORIS User object. - * - * @var \User - */ - private \User $_assignee; - - /** - * A list of REDCap instances. - * - * @var array The list of instance-specific REDCap configurations, indexed by instance name. - */ - private array $_instances; - - /** - * Constructor. - * - * @param \User $user a LORIS User - */ - public function __construct(\User $user) - { - $this->_assignee = $user; - $this->_instances = []; - } - - /** - * Get the main assignee for all issues. - * - * @return \User - */ - public function getAssignee(): \User - { - return $this->_assignee; - } - - /** - * Add a new REDCap instance. - * - * @param string $name the instance name - * @param string $url the instance url - * - * @return void - */ - public function addInstance(string $name, string $url): void - { - if ($this->hasInstanceName($name)) { - throw new \LorisException( - "[redcap][config] instance '$name' already exists." - ); - } - $this->_instances[$name] = new RedcapConfigurationInstance($name, $url); - } - - /** - * Get a REDCap instance by name. - * - * @param string $instanceName the instance name - * - * @return ?RedcapConfigurationInstance a REDCap instance or null. - */ - public function getInstanceByName( - string $instanceName - ): ?RedcapConfigurationInstance { - return $this->_instances[$instanceName] ?? null; - } - - /** - * Get a REDCap instance by URL. - * - * @param string $instanceURL the instance URL - * - * @return ?RedcapConfigurationInstance a REDCap instance or null. - */ - public function getInstanceByURL( - string $instanceURL - ): ?RedcapConfigurationInstance { - // normalize the given URL - $searchedURL = self::_normalizeURL($instanceURL); - $i = null; - foreach ($this->_instances as $instance) { - // normalize instance URL - $iURL = self::_normalizeURL($instance->getURL()); - // compare - if ($iURL === $searchedURL) { - $i = $instance; - break; - } - } - return $i; - } - - /** - * Normalize URL by removing the trailing slash. - * - * @param string $url URL to normalize - * - * @return string normalized URL - */ - private static function _normalizeURL(string $url): string - { - return str_ends_with($url, '/') ? substr($url, 0, strlen($url)-1) : $url; - } - - /** - * Tells if a REDCap instance name exists. - * - * @param string $instanceName the instance name - * - * @return bool true if the instance exists, else false. - */ - public function hasInstanceName(string $instanceName): bool - { - return array_key_exists($instanceName, $this->_instances); - } - - /** - * Add a REDCap project to a REDCap instance. - * - * @param string $instanceName a REDCap instance name - * @param string $projectID a REDCap project ID - * @param string $projectToken a REDCap project token - * - * @throws \LorisException - * - * @return void - */ - public function addProjectToInstance( - string $instanceName, - string $projectID, - string $projectToken - ): void { - if (!$this->hasInstanceName($instanceName)) { - throw new \LorisException( - "[redcap][config] instance '$instanceName' does not exist." - ); - } - $this->_instances[$instanceName]->addProject($projectID, $projectToken); - } - - /** - * Iterator on REDCap instances. - * - * @return \Iterator - */ - public function yieldInstances(): \Iterator - { - yield from $this->_instances; - } - - /** - * Tells if at least one instance is defined. - * - * @return bool true if at least one instance is defined, else false. - */ - public function hasInstances(): bool - { - return count($this->_instances) !== 0; - } - - /** - * Removes an instance by its name. - * - * @param string $instanceName an instance name - * - * @return void - */ - public function removeInstance(string $instanceName): void - { - if (!$this->hasInstanceName($instanceName)) { - throw new \LorisException( - "[redcap][config] instance '$instanceName' does not exist." - ); - } - // cannot remove an instance with projects - if ($this->_instances[$instanceName]->hasProjects()) { - throw new \LorisException( - "[redcap][config] cannot remove instance" - ." '$instanceName' because it has projects." - ); - } - unset($this->_instances[$instanceName]); - } - - /** - * Removes a project from an instance. If the instance is empty after - * removing the project, the instance is also removed. - * - * @param string $instanceName an instance name - * @param string $projectID a project ID - * - * @throws \LorisException - * - * @return void - */ - public function removeProjectFromInstance( - string $instanceName, - string $projectID - ): void { - if (!$this->hasInstanceName($instanceName)) { - throw new \LorisException( - "[redcap][config] instance '$instanceName' does not exist." - ); - } - - // - $this->_instances[$instanceName]->removeProject($projectID); - - // if no project left in instance, remove the instance. - if (!$this->_instances[$instanceName]->hasProjects()) { - $this->removeInstance($instanceName); - } - } -} \ No newline at end of file diff --git a/modules/redcap/php/configurations/redcapconfigurationinstance.class.inc b/modules/redcap/php/configurations/redcapconfigurationinstance.class.inc deleted file mode 100644 index 1ece4f7f080..00000000000 --- a/modules/redcap/php/configurations/redcapconfigurationinstance.class.inc +++ /dev/null @@ -1,208 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 - * @link https://www.github.com/aces/Loris/ - */ - -namespace LORIS\redcap\configurations; - -/** - * This reprensents a REDCap instance configuration. - * - * @category REDCap - * @package Main - * @author Regis Ongaro-Carcy - * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 - * @link https://www.github.com/aces/Loris/ - */ -class RedcapConfigurationInstance -{ - /** - * General API endpoint for REDCap API access. - * - * @var string - */ - const API_ENDPOINT = "api/"; - - /** - * The instance name. - * - * @var string - */ - private string $_name; - - /** - * The instance URL. - * - * @var string - */ - private string $_url; - - /** - * A list of projects accessible on this REDCap instance. - * - * @var array The list of project-specific REDCap configurations, indexed by project ID. - */ - private array $_projects; - - /** - * Constructor. - * - * @param string $name a REDCap instance name - * @param string $url a REDCap isntance URL - */ - public function __construct(string $name, string $url) - { - $this->_name = $name; - $this->_url = self::_cleanURL($url); - $this->_projects = []; - } - - /** - * Add a project in the list of project for this instance. - * - * @param string $pid the project ID - * @param string $token the project token - * - * @return void - */ - public function addProject(string $pid, string $token): void - { - if ($this->hasProject($pid)) { - $n = $this->_name; - throw new \LorisException( - "[redcap][config] project ID '$pid' already exists in instance '$n'." - ); - } - $this->_projects[$pid] = new RedcapConfigurationProject($pid, $token); - } - - /** - * Get the instance name. - * - * @return string - */ - public function getName(): string - { - return $this->_name; - } - - /** - * Get the instance URL. - * - * @return string - */ - public function getURL(): string - { - return $this->_url; - } - - /** - * Get the instance API URL. - * - * @return string - */ - public function getAPI(): string - { - return $this->_url . self::API_ENDPOINT; - } - - /** - * Clean an given URL. - * - * @param string $url an URL - * - * @return string a cleaned URL. - */ - private static function _cleanURL(string $url): string - { - // if last char is not '/', add it - $n = $url; - if (substr($url, -1) != '/') { - $n .= '/'; - } - - // - return $n; - } - - /** - * Get the list of project for this instance. - * - * @return RedcapConfigurationProject[] - */ - public function getProjects(): array - { - return $this->_projects; - } - - /** - * Tells if this instance has projects. - * - * @return bool true if this instance has projects, else false. - */ - public function hasProjects(): bool - { - return count($this->_projects) !== 0; - } - - /** - * Tells if a project ID exists in this REDCap instance. - * - * @param string $pid the project ID - * - * @return ?RedcapConfigurationProject a REDCap project if it exists, else null. - */ - public function getProject(string $pid): ?RedcapConfigurationProject - { - return $this->_projects[$pid] ?? null; - } - - /** - * Tells if a project ID exists in this REDCap instance. - * - * @param string $pid the project ID - * - * @return bool true if the project ID exists, else false - */ - public function hasProject(string $pid): bool - { - return !is_null($this->getProject($pid)); - } - - /** - * Iterator on projects. - * - * @return \Iterator - */ - public function yieldProjects(): \Iterator - { - yield from $this->_projects; - } - - /** - * Removes a project by its ID. - * - * @param string $pid a REDCap project ID - * - * @throws \LorisException - * - * @return void - */ - public function removeProject(string $pid): void - { - if (!$this->hasProject($pid)) { - $n = $this->_name; - throw new \LorisException( - "[redcap][config] project ID '$pid' does not exist in instance '$n'." - ); - } - unset($this->_projects[$pid]); - } -} \ No newline at end of file diff --git a/modules/redcap/php/configurations/redcapconfigurationparser.class.inc b/modules/redcap/php/configurations/redcapconfigurationparser.class.inc deleted file mode 100644 index f6ff2636b1f..00000000000 --- a/modules/redcap/php/configurations/redcapconfigurationparser.class.inc +++ /dev/null @@ -1,319 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 - * @link https://www.github.com/aces/Loris/ - */ - -namespace LORIS\redcap\configurations; - -use LORIS\redcap\RedcapHTTPClient; -use LORIS\redcap\configurations\RedcapConfiguration; - -/** - * This represents a REDCap configuration parser. - * - * @category REDCap - * @package Main - * @author Regis Ongaro-Carcy - * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 - * @link https://www.github.com/aces/Loris/ - */ -class RedcapConfigurationParser -{ - /** - * REDCap configuration. - * - * @var RedcapConfiguration - */ - private RedcapConfiguration $_config; - - /** - * A Loris instance. - * - * @var \LORIS\LorisInstance - */ - private \LORIS\LorisInstance $_loris; - - /** - * Singleton. - * - * @var - */ - private static $_instance; - - /** - * Constructor. - * - * @param \LORIS\LorisInstance $loris a loris instance - * @param bool $verbose verbose mode - */ - private function __construct( - \LORIS\LorisInstance $loris, - bool $verbose = false - ) { - $this->_loris = $loris; - $this->_loadREDCapConfig(); - $this->_checkAllInstances($verbose); - } - - /** - * Factory for singleton instance. - * - * @param \LORIS\LorisInstance $loris a loris instance - * @param bool $verbose verbose mode - * - * @return RedcapConfigurationParser - */ - public static function &factory( - \LORIS\LorisInstance $loris, - bool $verbose = false - ): RedcapConfigurationParser { - if (!isset(self::$_instance)) { - self::$_instance = new self($loris, $verbose); - } - return self::$_instance; - } - - /** - * Get REDCap configuration from 'config.xml' file. - * - * Structure: - * assignee => \User - * instance_name_aaa => [ - * url => https://someURL.com - * project => [ - * 111 => token_1 - * 222 => token_2 - * ] - * instance_name_bbb => [ - * url => https://someOtherURL.com - * project => [ - * 333 => token_3 - * ] - * ] - * - * @return RedcapConfiguration - */ - public function getConfig(): RedcapConfiguration - { - return $this->_config; - } - - /** - * Build REDCap configuration from 'config.xml' file. - * - * Structure: - * assignee => \User - * instance_name_aaa => [ - * url => https://someURL.com - * project => [ - * 111 => token_1 - * 222 => token_2 - * ] - * instance_name_bbb => [ - * url => https://someOtherURL.com - * project => [ - * 333 => token_3 - * ] - * ] - * - * @return void - */ - private function _loadREDCapConfig(): void - { - // config and db - $config = $this->_loris->getConfiguration(); - $db = $this->_loris->getDatabaseConnection(); - - // redcap config - $redcap = $config->getSetting("REDCap"); - if (empty($redcap)) { - throw new \LorisException("[redcap][init] no REDCap configuration."); - } - - // add main assignee - $assignee = $redcap['issuesAssignee'] ?? null; - if (!isset($assignee) || empty($assignee)) { - throw new \LorisException( - "[redcap][init] no REDCap 'issuesAssignee' in configuration." - ); - } - $assigneeUserID = $db->pselectOne( - "SELECT u.userID - FROM users u - JOIN user_perm_rel upr ON (upr.userid = u.id) - JOIN permissions p ON (p.permid = upr.permid) - WHERE u.userID = :usid - AND u.Active = 'Y' - AND u.Pending_approval = 'N' - AND p.code = 'issue_tracker_developer' - ", - ['usid' => $assignee] - ); - if (is_null($assigneeUserID)) { - throw new \LorisException( - "[redcap][init] assignee '$assignee' does not exist" - . " or does not have enough privilege." - ); - } - $u = \User::factory($assigneeUserID); - $this->_config = new RedcapConfiguration($u); - - // no instance described - if (!isset($redcap['instance'])) { - throw new \LorisException( - "[redcap][init] no REDCap instance in configuration." - ); - } - - // multiple instances - $instances = null; - if (array_key_exists(0, $redcap['instance'])) { - $instances = $redcap['instance']; - } else if (array_key_exists('url', $redcap['instance'])) { - // only one instances - $instances = [$redcap['instance']]; - } - - if (is_null($instances)) { - throw new \LorisException( - "[redcap][init] wrong REDCap instance structure in configuration." - ); - } - - // iterate on instances - foreach ($instances as $instance) { - // - $name = $instance['name']; - $url = $instance['url']; - - // check name/url - if (empty($name)) { - throw new \LorisException( - "[redcap][init] wrong REDCap configuration:" - ." missing instance 'name'." - ); - } - if (empty($url)) { - throw new \LorisException( - "[redcap][init] wrong REDCap configuration:" - ." missing instance 'url'." - ); - } - - // multiple projects - $projectTag = $instance['project'] ?? null; - if (is_null($projectTag)) { - throw new \LorisException( - "[redcap][init] wrong REDCap configuration:" - ." no projects for instance '$name'." - ); - } - - $projects = null; - if (array_key_exists(0, $projectTag)) { - $projects = $projectTag; - } else if (array_key_exists('projectID', $projectTag)) { - // only one projects - $projects = [$projectTag]; - } - - // add instance - $this->_config->addInstance($name, $url); - - // iterate on instances - foreach ($projects as $project) { - $pid = $project['projectID']; - $token = $project['token']; - - // check pid/token - if (empty($pid)) { - throw new \LorisException( - "[redcap][init] wrong REDCap configuration:" - ." missing a 'projectID' for instance '$name'." - ); - } - if (empty($token)) { - throw new \LorisException( - "[redcap][init] wrong REDCap configuration:" - ." missing a 'token' for instance '$name'." - ); - } - - // add the list of projects - $this->_config->addProjectToInstance($name, $pid, $token); - } - } - } - - /** - * Test instances and remove those triggering a connction error. - * - * @param bool $verbose verbose mode - * - * @return void - */ - private function _checkAllInstances(bool $verbose = false): void - { - // check all configuration instances and projects. - foreach ($this->_config->yieldInstances() as $instance) { - $iname = $instance->getName(); - foreach ($instance->yieldProjects() as $project) { - $pid = $project->getProjectId(); - $url = $instance->getAPI(); - - // check the connection - $rc = new REDCapHTTPClient( - $this->_loris, - $url, - $pid, - $project->getToken(), - $verbose - ); - - // - try { - $rc->checkConnection(); - } catch (\LorisException $le) { - error_log( - "[redcap][config] cannot connect to REDCap" - . " instance '$iname', project '$pid'." - . " Check configuration and connection." - . $le->getMessage() - ); - $this->_config->removeProjectFromInstance($iname, $pid); - } - } - } - - // check if at least one cnofiguration is accessible - if (!$this->_config->hasInstances()) { - throw new \LorisException( - "[redcap][config] none of the REDCap configurations declared" - ." in 'config.xml' file can be accessed." - ); - } - - // log - if ($verbose) { - error_log("[redcap][config] can access the following REDCap projects:"); - foreach ($this->_config->yieldInstances() as $instance) { - $iname = $instance->getName(); - foreach ($instance->yieldProjects() as $project) { - $pid = $project->getProjectId(); - $url = $instance->getURL(); - error_log( - "[redcap][config] - instance '$iname'" - ." at '$url', project '$pid'" - ); - } - } - } - } -} \ No newline at end of file diff --git a/modules/redcap/php/configurations/redcapconfigurationproject.class.inc b/modules/redcap/php/configurations/redcapconfigurationproject.class.inc deleted file mode 100644 index 072444a00d6..00000000000 --- a/modules/redcap/php/configurations/redcapconfigurationproject.class.inc +++ /dev/null @@ -1,71 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 - * @link https://www.github.com/aces/Loris/ - */ - -namespace LORIS\redcap\configurations; - -/** - * This represents a REDCap project configuration. - * - * @category REDCap - * @package Main - * @author Regis Ongaro-Carcy - * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 - * @link https://www.github.com/aces/Loris/ - */ -class RedcapConfigurationProject -{ - /** - * A REDCap project ID. - * - * @var string - */ - private string $_id; - - /** - * A REDcap token to access this project. - * - * @var string - */ - private string $_token; - - /** - * Constructor. - * - * @param string $projectID the projectID - * @param string $token the token - */ - public function __construct(string $projectID, string $token) - { - $this->_id = $projectID; - $this->_token = $token; - } - - /** - * Get the REDCap project ID. - * - * @return string - */ - public function getProjectId(): string - { - return $this->_id; - } - - /** - * Get the REDCap token. - * - * @return string - */ - public function getToken(): string - { - return $this->_token; - } -} \ No newline at end of file diff --git a/modules/redcap/php/endpoints/notifications.class.inc b/modules/redcap/php/endpoints/notifications.class.inc index f3e2e88d3d0..ecea5e3eff1 100644 --- a/modules/redcap/php/endpoints/notifications.class.inc +++ b/modules/redcap/php/endpoints/notifications.class.inc @@ -16,8 +16,11 @@ use LORIS\redcap\RedcapHTTPClientHandler; use \Psr\Http\Message\ServerRequestInterface; use \Psr\Http\Message\ResponseInterface; use \LORIS\Http\Endpoint; +use \LORIS\redcap\config\RedcapConfig; +use \LORIS\redcap\config\RedcapConfigParser; use \LORIS\redcap\notifications\RedcapNotification; use \LORIS\redcap\notifications\RedcapNotificationHandler; +use LORIS\redcap\REDCapHTTPClient; /** * This is the handler for redcap notifications @@ -30,13 +33,6 @@ use \LORIS\redcap\notifications\RedcapNotificationHandler; */ class Notifications extends Endpoint { - /** - * A REDCap client handler. - * - * @var RedcapHTTPClientHandler - */ - protected RedcapHTTPClientHandler $redcapClientHandler; - /** * Construct an endpoint * @@ -45,10 +41,8 @@ class Notifications extends Endpoint */ public function __construct( protected \LORIS\LorisInstance $loris, - protected RedcapHTTPClientHandler $rch ) { parent::__construct($loris); - $this->redcapClientHandler = $rch; } /** @@ -113,8 +107,15 @@ class Notifications extends Endpoint try { $notification = new RedcapNotification($data); + $config_parser = new RedcapConfigParser($this->loris); + + $config = $config_parser->parse( + $notification->getRedcapURL(), + $notification->getProjectId(), + ); + // should the notification be ignored? - if ($this->_ignoreNotification($notification)) { + if ($this->_ignoreNotification($notification, $config)) { return new \LORIS\Http\Response(); } @@ -151,13 +152,17 @@ class Notifications extends Endpoint // get a new redcap client based on the notification info try { - $rc = $this->redcapClientHandler->getClientByURL( - $notification->getRedcapURL(), + $rc = new REDCapHTTPClient( + $this->loris, + "{$config->redcap_url}/api/", $notification->getProjectId(), + $config->redcap_api_token, ); + $notification_handler = new RedcapNotificationHandler( $this->loris, - $rc + $config, + $rc, ); } catch (\LorisException $le) { $this->_createIssue( @@ -221,16 +226,14 @@ class Notifications extends Endpoint * * @return bool if the notification should be ignored, else false. */ - private function _ignoreNotification(RedcapNotification $notif): bool + private function _ignoreNotification( + RedcapNotification $notif, + ?RedcapConfig $redcap_config, + ): bool { $notifData = json_encode($notif->toDatabaseArray()); - // is that notification coming from a known instance/project? - $sourceExists = $this->redcapClientHandler->hasInstanceProjectByURL( - $notif->getRedcapURL(), - $notif->getProjectId() - ); - if (!$sourceExists) { + if ($redcap_config === null) { error_log( "[redcap][notification:skip] unknown source/project: $notifData" ); @@ -290,8 +293,11 @@ class Notifications extends Endpoint ); // get the main assignee from config - $assignee = $this->redcapClientHandler->getAssignee(); - $username = $assignee->getUsername(); + // $assignee = $this->redcapClientHandler->getAssignee(); + // $username = $assignee->getUsername(); + // TODO: Get the assignee from the module configuration. + // Change the parser to still return the assignee if no project is found. + $username = 'redcap'; // insert new issue $dtNow = new \DateTimeImmutable(); @@ -315,4 +321,4 @@ class Notifications extends Endpoint $db->insert('issues', $issueData); error_log("[redcap][issue:created] " . json_encode($issueData)); } -} \ No newline at end of file +} diff --git a/modules/redcap/php/module.class.inc b/modules/redcap/php/module.class.inc index 6a317d881b9..f30b6d91ded 100644 --- a/modules/redcap/php/module.class.inc +++ b/modules/redcap/php/module.class.inc @@ -17,7 +17,6 @@ namespace LORIS\redcap; use \Psr\Http\Message\ServerRequestInterface; use \Psr\Http\Message\ResponseInterface; -use \LORIS\redcap\RedcapHTTPClientHandler; /** * This sets specific bahaviours or the redcap module and provides @@ -33,13 +32,6 @@ use \LORIS\redcap\RedcapHTTPClientHandler; */ class Module extends \Module { - /** - * REDcap client handler. - * - * @var RedcapHTTPClientHandler - */ - private static RedcapHTTPClientHandler $_redcapClientHandler; - /** * This needs to be true since redcap wont authenticate. * @@ -60,26 +52,6 @@ class Module extends \Module return "REDCap"; } - /** - * Get the current Redcap client handler - * - * @param \LORIS\LorisInstance $loris a loris instance - * - * @return RedcapHTTPClientHandler - */ - protected static function getRedcapHandler( - \Loris\LORISInstance $loris - ): RedcapHTTPClientHandler { - // load the redcap client handler once - if (!isset(self::$_redcapClientHandler)) { - self::$_redcapClientHandler = RedcapHTTPClientHandler::factory( - $loris, - false - ); - } - return self::$_redcapClientHandler; - } - /** * Perform routing for this module's endpoints. * @@ -89,16 +61,12 @@ class Module extends \Module */ public function handle(ServerRequestInterface $request) : ResponseInterface { - // udpate redcap client handler - $redcapClientHandler = self::getRedcapHandler($this->loris); - // $path = trim($request->getURI()->getPath(), "/"); switch ($path) { case 'notifications': $handler = new Endpoints\Notifications( $request->getAttribute('loris'), - $redcapClientHandler ); break; // case 'dev_test': diff --git a/modules/redcap/php/notifications/redcapnotificationhandler.class.inc b/modules/redcap/php/notifications/redcapnotificationhandler.class.inc index c725f9736dd..5969380071a 100644 --- a/modules/redcap/php/notifications/redcapnotificationhandler.class.inc +++ b/modules/redcap/php/notifications/redcapnotificationhandler.class.inc @@ -14,6 +14,7 @@ namespace LORIS\redcap\notifications; use \LORIS\LorisInstance; use \LORIS\redcap\REDCapHTTPClient; +use \LORIS\redcap\config\RedcapConfig; use \LORIS\redcap\notifications\RedcapNotification; use \LORIS\redcap\models\records\IRedcapRecord; @@ -41,14 +42,21 @@ class RedcapNotificationHandler ]; /** - * Loris instance. + * THe LORIS instance. * * @var LorisInstance */ private LorisInstance $_loris; /** - * A REDCap client. + * The REDCap module configuration. + * + * @var RedcapConfig + */ + private RedcapConfig $_redcap_config; + + /** + * The REDCap HTTP client. * * @var REDCapHTTPClient */ @@ -62,9 +70,11 @@ class RedcapNotificationHandler */ public function __construct( LorisInstance $loris, + RedcapConfig $redcap_config, REDCapHTTPClient $client ) { $this->_loris = $loris; + $this->_redcap_config = $redcap_config; $this->_redcapClient = $client; }